From 29bf3718348bef35bdc7378c28ea2b11a3cf5f9b Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 2 Dec 2025 03:09:58 +0000 Subject: [PATCH] Add physics visualisation panel --- src/py_dvt_ate/app/dashboard/app.py | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index 578f7be..5a12b76 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -4,8 +4,119 @@ 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 main() -> None: """Main entry point for the Streamlit dashboard.""" @@ -23,6 +134,21 @@ def main() -> None: """ ) + init_session_state() + + # 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()