Compare commits
5 Commits
v0.1.0-alp
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c0d2ead54 | |||
| 2b78a75f51 | |||
| 15c9033153 | |||
| 0ab1181ec4 | |||
| bb3129e69b |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.0-alpha.2] - 2025-12-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Streamlit dashboard for interactive physics visualisation
|
||||||
|
- Real-time temperature charts (chamber, case, junction)
|
||||||
|
- Current state metrics display (voltages, currents, power, temperatures)
|
||||||
|
- Interactive controls in sidebar:
|
||||||
|
- Temperature setpoint slider (-40°C to 125°C)
|
||||||
|
- Input voltage slider (0-12V)
|
||||||
|
- Load current slider (0-500mA)
|
||||||
|
- Output enable toggle
|
||||||
|
- Start/Stop/Reset simulation buttons
|
||||||
|
- Self-heating demonstration panel with:
|
||||||
|
- Junction-case and case-ambient temperature rise display
|
||||||
|
- Power dissipation chart
|
||||||
|
- Thermal coupling explanation
|
||||||
|
|
||||||
## [0.1.0-alpha.1] - 2025-12-02
|
## [0.1.0-alpha.1] - 2025-12-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -43,6 +60,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
| 0.1.0-beta.2 | TBD | First DVT test runs |
|
| 0.1.0-beta.2 | TBD | First DVT test runs |
|
||||||
| 0.1.0-beta.1 | TBD | HAL complete |
|
| 0.1.0-beta.1 | TBD | HAL complete |
|
||||||
| 0.1.0-alpha.3 | TBD | Network ready |
|
| 0.1.0-alpha.3 | TBD | Network ready |
|
||||||
| 0.1.0-alpha.2 | TBD | Visual demo |
|
| 0.1.0-alpha.2 | 2025-12-02 | Visual demo |
|
||||||
| 0.1.0-alpha.1 | 2025-12-02 | Physics engine |
|
| 0.1.0-alpha.1 | 2025-12-02 | Physics engine |
|
||||||
| 0.0.1 | 2025-12-01 | Project scaffolding |
|
| 0.0.1 | 2025-12-01 | Project scaffolding |
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""py_dvt_ate: Coupled Physics DVT Simulation Platform."""
|
"""py_dvt_ate: Coupled Physics DVT Simulation Platform."""
|
||||||
|
|
||||||
__version__ = "0.1.0-alpha.1"
|
__version__ = "0.1.0-alpha.2"
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
Provides visualisation of instrument status, test progress,
|
Provides visualisation of instrument status, test progress,
|
||||||
and historical results.
|
and historical results.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from py_dvt_ate.app.dashboard.app import main
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
|
|||||||
298
src/py_dvt_ate/app/dashboard/app.py
Normal file
298
src/py_dvt_ate/app/dashboard/app.py
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
"""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()
|
||||||
Reference in New Issue
Block a user