From e873223091c09c067490f07c2c8bf1a29827077c Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Wed, 3 Dec 2025 16:09:21 +0000 Subject: [PATCH] Add test execution dashboard page --- src/py_dvt_ate/app/dashboard/app.py | 212 +++++++++++++++++++++++++++- 1 file changed, 210 insertions(+), 2 deletions(-) diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index 3b36e1c..8ed4810 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -14,9 +14,12 @@ from dataclasses import dataclass, field import streamlit as st +from py_dvt_ate.data.repository import SQLiteRepository +from py_dvt_ate.framework.runner import TestRunner from py_dvt_ate.instruments.factory import InstrumentConfig, InstrumentFactory, InstrumentSet from py_dvt_ate.simulation.physics.engine import PhysicsEngine from py_dvt_ate.simulation.server import ServerConfig, SimulationServer +from py_dvt_ate.tests.thermal.tempco import TempCoTest # History buffer size for charts HISTORY_SIZE = 500 @@ -105,12 +108,23 @@ def init_session_state() -> None: ) st.session_state.instruments = InstrumentFactory.create(config) + if "repository" not in st.session_state: + # Create test repository (in-memory for dashboard demo) + st.session_state.repository = SQLiteRepository(":memory:") + + if "test_runner" not in st.session_state: + st.session_state.test_runner = TestRunner(st.session_state.repository) + if "history" not in st.session_state: st.session_state.history = SimulationHistory() if "running" not in st.session_state: st.session_state.running = False if "last_update" not in st.session_state: st.session_state.last_update = time.time() + if "test_running" not in st.session_state: + st.session_state.test_running = False + if "test_run_id" not in st.session_state: + st.session_state.test_run_id = None # Note: time_multiplier, temp_setpoint, input_voltage, output_enabled, # load_current are managed by their respective widgets via keys @@ -457,6 +471,189 @@ self-heating effects! ) +def test_execution_page() -> None: + """Test execution page for running DVT tests.""" + st.header("Test Execution") + st.markdown("Run DVT characterisation tests using the virtual lab bench.") + + # Test selection + st.subheader("Select Test") + test_options = { + "TempCo (Temperature Coefficient)": TempCoTest(), + } + + selected_test_name = st.selectbox( + "Available Tests", + options=list(test_options.keys()), + disabled=st.session_state.test_running, + ) + + selected_test = test_options[selected_test_name] + + # Display test description + st.info(f"**{selected_test.name}**: {selected_test.description}") + + # Test configuration + st.subheader("Test Configuration") + + col1, col2 = st.columns(2) + + with col1: + if selected_test.name == "tempco": + input_voltage = st.number_input( + "Input Voltage (V)", + min_value=0.0, + max_value=12.0, + value=5.0, + step=0.1, + disabled=st.session_state.test_running, + ) + load_current = st.number_input( + "Load Current (mA)", + min_value=0.0, + max_value=500.0, + value=100.0, + step=10.0, + disabled=st.session_state.test_running, + ) + settle_time = st.number_input( + "Settle Time (s)", + min_value=0.0, + max_value=60.0, + value=5.0, + step=1.0, + disabled=st.session_state.test_running, + ) + + with col2: + if selected_test.name == "tempco": + temp_min = st.number_input( + "Min Temperature (C)", + min_value=-40.0, + max_value=125.0, + value=-40.0, + step=5.0, + disabled=st.session_state.test_running, + ) + temp_max = st.number_input( + "Max Temperature (C)", + min_value=-40.0, + max_value=125.0, + value=85.0, + step=5.0, + disabled=st.session_state.test_running, + ) + temp_step = st.number_input( + "Temperature Step (C)", + min_value=1.0, + max_value=50.0, + value=25.0, + step=5.0, + disabled=st.session_state.test_running, + ) + + # Generate temperature points + if selected_test.name == "tempco": + import numpy as np + temperatures = list(np.arange(temp_min, temp_max + temp_step / 2, temp_step)) + st.caption(f"Temperature points: {temperatures}") + + test_config = { + "temperatures": temperatures, + "input_voltage": input_voltage, + "load_current": load_current / 1000.0, # Convert mA to A + "settle_time": settle_time, + } + else: + test_config = {} + + # Run test button + st.subheader("Execution") + + col1, col2 = st.columns([1, 3]) + + with col1: + if st.button( + "Run Test", + type="primary", + use_container_width=True, + disabled=st.session_state.test_running, + ): + st.session_state.test_running = True + st.session_state.test_run_id = None + st.rerun() + + with col2: + if st.session_state.test_running: + st.info("๐Ÿ”„ Test is running...") + elif st.session_state.test_run_id: + st.success("โœ… Test completed!") + else: + st.caption("Click 'Run Test' to start") + + # Execute test if running + if st.session_state.test_running: + with st.spinner("Running test..."): + try: + runner: TestRunner = st.session_state.test_runner + instruments: InstrumentSet = st.session_state.instruments + + # Run the test + run_id = runner.run_test( + test=selected_test, + instruments=instruments, + config=test_config, + operator="dashboard_user", + description=f"Dashboard execution: {selected_test_name}", + ) + + st.session_state.test_run_id = run_id + st.session_state.test_running = False + st.rerun() + + except Exception as e: + st.error(f"Test execution failed: {e}") + st.session_state.test_running = False + + # Display results if available + if st.session_state.test_run_id: + st.subheader("Test Results") + + repository: SQLiteRepository = st.session_state.repository + run = repository.get_run(st.session_state.test_run_id) + + col1, col2, col3 = st.columns(3) + with col1: + status_color = {"PASSED": "๐ŸŸข", "FAILED": "๐Ÿ”ด", "ERROR": "๐ŸŸก"}.get( + run.status.value, "โšช" + ) + st.metric("Status", f"{status_color} {run.status.value}") + with col2: + if run.completed_at and run.started_at: + duration = (run.completed_at - run.started_at).total_seconds() + st.metric("Duration", f"{duration:.1f} s") + else: + st.metric("Duration", "N/A") + with col3: + results = repository.get_results(st.session_state.test_run_id) + st.metric("Results", len(results)) + + # Show detailed results + if results: + st.markdown("#### Detailed Results") + results_data = [] + for result in results: + results_data.append({ + "Parameter": result.parameter, + "Value": f"{result.value:.6f}", + "Unit": result.unit or "", + "Lower Limit": f"{result.lower_limit:.6f}" if result.lower_limit is not None else "N/A", + "Upper Limit": f"{result.upper_limit:.6f}" if result.upper_limit is not None else "N/A", + "Status": "โœ… PASS" if result.passed else "โŒ FAIL", + }) + st.table(results_data) + + def main() -> None: """Main entry point for the Streamlit dashboard.""" st.set_page_config( @@ -480,8 +677,19 @@ def main() -> None: # Sidebar controls (static - doesn't need fragment) display_controls() - # Dynamic simulation display (uses fragment for smooth updates) - simulation_display() + # Create tabs for different views + tab1, tab2, tab3 = st.tabs(["๐Ÿ”ฌ Lab Bench", "๐Ÿงช Test Execution", "๐Ÿ“Š Results Viewer"]) + + with tab1: + # Dynamic simulation display (uses fragment for smooth updates) + simulation_display() + + with tab2: + test_execution_page() + + with tab3: + st.header("Results Viewer") + st.info("Results viewer coming soon in Task 17.3!") if __name__ == "__main__":