From 3f47d8a5e47a3ffbb933a3b62d6155ffc217bb30 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 8 Apr 2025 13:08:15 +0000 Subject: [PATCH] Use st.fragment for smooth dashboard updates Replace st.rerun() with @st.fragment decorator to prevent full page reloads and eliminate UI greying out. --- src/py_dvt_ate/app/dashboard/app.py | 261 +++++++++++++--------------- 1 file changed, 120 insertions(+), 141 deletions(-) diff --git a/src/py_dvt_ate/app/dashboard/app.py b/src/py_dvt_ate/app/dashboard/app.py index 86f72c4..493519f 100644 --- a/src/py_dvt_ate/app/dashboard/app.py +++ b/src/py_dvt_ate/app/dashboard/app.py @@ -18,8 +18,6 @@ HISTORY_SIZE = 500 # Simulation speed settings DEFAULT_TIME_MULTIPLIER = 10.0 # 10x faster than real-time -STEPS_PER_UPDATE = 100 # Steps per UI refresh -UPDATE_INTERVAL_MS = 100 # Target UI refresh rate @dataclass @@ -89,124 +87,6 @@ def step_simulation() -> None: 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_self_heating_panel() -> None: - """Display self-heating demonstration panel.""" - engine: PhysicsEngine = st.session_state.engine - history: SimulationHistory = st.session_state.history - - thermal = engine.get_thermal_state() - electrical = engine.get_electrical_state() - - # Calculate temperature rises - delta_t_jc = thermal.junction_temperature - thermal.case_temperature - delta_t_ca = thermal.case_temperature - thermal.chamber_temperature - - col1, col2 = st.columns(2) - - with col1: - st.markdown("#### Self-Heating Analysis") - - # Display thermal resistance info - st.markdown( - f""" - | Parameter | Value | - |-----------|-------| - | Junction-Case Rise (ΔT_jc) | **{delta_t_jc:.2f} °C** | - | Case-Ambient Rise (ΔT_ca) | **{delta_t_ca:.2f} °C** | - | Power Dissipation | {electrical.power_dissipation * 1000:.1f} mW | - | θ_jc (junction-case) | 15 °C/W | - | θ_ca (case-ambient) | 5 °C/W | - """ - ) - - st.markdown( - """ - **Thermal Coupling:** The junction temperature rises above the case - temperature due to power dissipation. This is governed by: - - `T_junction = T_case + P_diss × θ_jc` - - Try increasing the load current or input voltage to see - self-heating effects! - """ - ) - - with col2: - st.markdown("#### Power Dissipation") - - if len(history.time) < 2: - st.info("Start the simulation to see power data") - return - - power_data = { - "Time (s)": list(history.time), - "Power (mW)": [p * 1000 for p in history.power_dissipation], - } - - st.line_chart( - power_data, - x="Time (s)", - y="Power (mW)", - color="#2ca02c", - ) - - -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:.1f} s", - delta=f"{st.session_state.time_multiplier:.0f}× speed", - ) - - def display_controls() -> None: """Display simulation control panel in sidebar.""" engine: PhysicsEngine = st.session_state.engine @@ -215,7 +95,9 @@ def display_controls() -> None: # Start/Stop button if st.session_state.running: - if st.sidebar.button("Stop Simulation", type="primary", use_container_width=True): + if st.sidebar.button( + "Stop Simulation", type="primary", use_container_width=True + ): st.session_state.running = False st.rerun() else: @@ -223,6 +105,7 @@ def display_controls() -> None: "Start Simulation", type="primary", use_container_width=True ): st.session_state.running = True + st.session_state.last_update = time.time() st.rerun() # Reset button @@ -241,7 +124,7 @@ def display_controls() -> None: "Time Multiplier", options=[1, 2, 5, 10, 20, 50, 100], value=int(st.session_state.time_multiplier), - format_func=lambda x: f"{x}×", + format_func=lambda x: f"{x}x", key="time_mult_slider", ) st.session_state.time_multiplier = float(time_mult) @@ -252,7 +135,7 @@ def display_controls() -> None: # Temperature setpoint st.sidebar.subheader("Thermal Chamber") temp_setpoint = st.sidebar.slider( - "Temperature Setpoint (°C)", + "Temperature Setpoint (C)", min_value=-40.0, max_value=125.0, value=25.0, @@ -297,6 +180,117 @@ def display_controls() -> None: engine.set_load_current(load_current_ma / 1000.0) +@st.fragment(run_every=0.1) +def simulation_display() -> None: + """Fragment that displays and updates simulation state.""" + engine: PhysicsEngine = st.session_state.engine + history: SimulationHistory = st.session_state.history + + # Step simulation if running + if st.session_state.running: + step_simulation() + + # Get current state + thermal = engine.get_thermal_state() + electrical = engine.get_electrical_state() + + # Current state metrics + st.subheader("Current 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: + status = "Running" if st.session_state.running else "Stopped" + st.metric( + "Sim Time", + f"{engine.simulation_time:.1f} s", + delta=f"{status} @ {st.session_state.time_multiplier:.0f}x", + ) + + # Temperature chart + st.subheader("Temperature History") + if len(history.time) < 2: + st.info("Start the simulation to see temperature data") + else: + 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"], + ) + + # Self-heating demonstration + st.subheader("Self-Heating Demonstration") + + delta_t_jc = thermal.junction_temperature - thermal.case_temperature + delta_t_ca = thermal.case_temperature - thermal.chamber_temperature + + col1, col2 = st.columns(2) + + with col1: + st.markdown("#### Self-Heating Analysis") + st.markdown( + f""" +| Parameter | Value | +|-----------|-------| +| Junction-Case Rise (dT_jc) | **{delta_t_jc:.2f} C** | +| Case-Ambient Rise (dT_ca) | **{delta_t_ca:.2f} C** | +| Power Dissipation | {electrical.power_dissipation * 1000:.1f} mW | +| theta_jc (junction-case) | 15 C/W | +| theta_ca (case-ambient) | 5 C/W | +""" + ) + st.markdown( + """ +**Thermal Coupling:** The junction temperature rises above the case +temperature due to power dissipation. This is governed by: + +`T_junction = T_case + P_diss x theta_jc` + +Try increasing the load current or input voltage to see +self-heating effects! +""" + ) + + with col2: + st.markdown("#### Power Dissipation") + if len(history.time) < 2: + st.info("Start the simulation to see power data") + else: + power_data = { + "Time (s)": list(history.time), + "Power (mW)": [p * 1000 for p in history.power_dissipation], + } + st.line_chart( + power_data, + x="Time (s)", + y="Power (mW)", + color="#2ca02c", + ) + + def main() -> None: """Main entry point for the Streamlit dashboard.""" st.set_page_config( @@ -315,26 +309,11 @@ def main() -> None: init_session_state() - # Sidebar controls + # Sidebar controls (static - doesn't need fragment) display_controls() - # Current state display - st.subheader("Current State") - display_current_state() - - # Temperature chart - st.subheader("Temperature History") - display_thermal_chart() - - # Self-heating demonstration - st.subheader("Self-Heating Demonstration") - display_self_heating_panel() - - # Auto-refresh when running - if st.session_state.running: - step_simulation() - time.sleep(0.05) # Small delay to prevent UI thrashing - st.rerun() + # Dynamic simulation display (uses fragment for smooth updates) + simulation_display() if __name__ == "__main__":