Files
py-dvt-ate/src/py_dvt_ate/app/dashboard/app.py

299 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_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:.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()
# Self-heating demonstration
st.subheader("Self-Heating Demonstration")
display_self_heating_panel()
# Auto-refresh when running
if st.session_state.running:
step_simulation(steps=10)
st.rerun()
if __name__ == "__main__":
main()