"""Streamlit dashboard application for physics simulation visualisation. This module provides an interactive dashboard for visualising the physics engine directly, demonstrating thermal-electrical coupling in real-time. """ from collections import deque from dataclasses import dataclass, field import streamlit as st from py_dvt_ate.simulation.physics.engine import PhysicsEngine # History buffer size for charts HISTORY_SIZE = 500 @dataclass class SimulationHistory: """Stores time series data for visualisation.""" time: deque[float] = field(default_factory=lambda: deque(maxlen=HISTORY_SIZE)) chamber_temp: deque[float] = field( default_factory=lambda: deque(maxlen=HISTORY_SIZE) ) case_temp: deque[float] = field(default_factory=lambda: deque(maxlen=HISTORY_SIZE)) junction_temp: deque[float] = field( default_factory=lambda: deque(maxlen=HISTORY_SIZE) ) output_voltage: deque[float] = field( default_factory=lambda: deque(maxlen=HISTORY_SIZE) ) power_dissipation: deque[float] = field( default_factory=lambda: deque(maxlen=HISTORY_SIZE) ) def init_session_state() -> None: """Initialise Streamlit session state.""" if "engine" not in st.session_state: st.session_state.engine = PhysicsEngine(update_rate_hz=100.0) if "history" not in st.session_state: st.session_state.history = SimulationHistory() if "running" not in st.session_state: st.session_state.running = False def step_simulation(steps: int = 10) -> None: """Advance the simulation by the given number of steps.""" engine: PhysicsEngine = st.session_state.engine history: SimulationHistory = st.session_state.history for _ in range(steps): engine.step() # Record current state in history thermal = engine.get_thermal_state() electrical = engine.get_electrical_state() history.time.append(thermal.timestamp) history.chamber_temp.append(thermal.chamber_temperature) history.case_temp.append(thermal.case_temperature) history.junction_temp.append(thermal.junction_temperature) history.output_voltage.append(electrical.output_voltage) history.power_dissipation.append(electrical.power_dissipation) def display_thermal_chart() -> None: """Display temperature chart.""" history: SimulationHistory = st.session_state.history if len(history.time) < 2: st.info("Start the simulation to see temperature data") return chart_data = { "Time (s)": list(history.time), "Chamber": list(history.chamber_temp), "Case": list(history.case_temp), "Junction": list(history.junction_temp), } st.line_chart( chart_data, x="Time (s)", y=["Chamber", "Case", "Junction"], color=["#1f77b4", "#ff7f0e", "#d62728"], ) def display_current_state() -> None: """Display current simulation state metrics.""" engine: PhysicsEngine = st.session_state.engine thermal = engine.get_thermal_state() electrical = engine.get_electrical_state() col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Chamber Temp", f"{thermal.chamber_temperature:.2f} °C") with col2: st.metric("Case Temp", f"{thermal.case_temperature:.2f} °C") with col3: st.metric("Junction Temp", f"{thermal.junction_temperature:.2f} °C") with col4: st.metric("Output Voltage", f"{electrical.output_voltage:.4f} V") col5, col6, col7, col8 = st.columns(4) with col5: st.metric("Input Voltage", f"{electrical.input_voltage:.2f} V") with col6: st.metric("Load Current", f"{electrical.load_current * 1000:.1f} mA") with col7: st.metric("Power Diss.", f"{electrical.power_dissipation * 1000:.2f} mW") with col8: st.metric("Sim Time", f"{engine.simulation_time:.2f} s") def display_controls() -> None: """Display simulation control panel in sidebar.""" engine: PhysicsEngine = st.session_state.engine st.sidebar.header("Simulation Controls") # Start/Stop button if st.session_state.running: if st.sidebar.button("Stop Simulation", type="primary", use_container_width=True): st.session_state.running = False st.rerun() else: if st.sidebar.button( "Start Simulation", type="primary", use_container_width=True ): st.session_state.running = True st.rerun() # Reset button if st.sidebar.button("Reset", use_container_width=True): st.session_state.engine = PhysicsEngine(update_rate_hz=100.0) st.session_state.history = SimulationHistory() st.session_state.running = False st.rerun() st.sidebar.divider() # Temperature setpoint st.sidebar.subheader("Thermal Chamber") temp_setpoint = st.sidebar.slider( "Temperature Setpoint (°C)", min_value=-40.0, max_value=125.0, value=25.0, step=5.0, key="temp_setpoint", ) engine.set_chamber_setpoint(temp_setpoint) st.sidebar.divider() # Power supply controls st.sidebar.subheader("Power Supply") input_voltage = st.sidebar.slider( "Input Voltage (V)", min_value=0.0, max_value=12.0, value=5.0, step=0.1, key="input_voltage", ) engine.set_input_voltage(input_voltage) output_enabled = st.sidebar.toggle( "Output Enabled", value=engine.is_output_enabled, key="output_enabled", ) engine.set_output_enabled(output_enabled) st.sidebar.divider() # Load controls st.sidebar.subheader("Electronic Load") load_current_ma = st.sidebar.slider( "Load Current (mA)", min_value=0.0, max_value=500.0, value=100.0, step=10.0, key="load_current", ) engine.set_load_current(load_current_ma / 1000.0) def main() -> None: """Main entry point for the Streamlit dashboard.""" st.set_page_config( page_title="py-dvt-ate Virtual Lab Bench", page_icon="🔬", layout="wide", ) st.title("py-dvt-ate Virtual Lab Bench") st.markdown( """ Interactive physics simulation demonstrating coupled thermal-electrical behaviour of an LDO voltage regulator. """ ) init_session_state() # Sidebar controls display_controls() # Current state display st.subheader("Current State") display_current_state() # Temperature chart st.subheader("Temperature History") display_thermal_chart() # Auto-refresh when running if st.session_state.running: step_simulation(steps=10) st.rerun() if __name__ == "__main__": main()