Put sidebar controls in fragment to prevent page blanking

Both controls and display are now fragments, so slider
changes don't trigger full page reruns.
This commit is contained in:
2025-04-13 18:47:37 +00:00
parent 7779128636
commit 6e33a9d441

View File

@@ -16,9 +16,6 @@ from py_dvt_ate.simulation.physics.engine import PhysicsEngine
# History buffer size for charts # History buffer size for charts
HISTORY_SIZE = 500 HISTORY_SIZE = 500
# Simulation speed settings
DEFAULT_TIME_MULTIPLIER = 10.0 # 10x faster than real-time
@dataclass @dataclass
class SimulationHistory: class SimulationHistory:
@@ -48,17 +45,17 @@ def init_session_state() -> None:
st.session_state.history = SimulationHistory() st.session_state.history = SimulationHistory()
if "running" not in st.session_state: if "running" not in st.session_state:
st.session_state.running = False st.session_state.running = False
if "time_multiplier" not in st.session_state:
st.session_state.time_multiplier = DEFAULT_TIME_MULTIPLIER
if "last_update" not in st.session_state: if "last_update" not in st.session_state:
st.session_state.last_update = time.time() st.session_state.last_update = time.time()
# Note: time_multiplier, temp_setpoint, input_voltage, output_enabled,
# load_current are managed by their respective widgets via keys
def step_simulation() -> None: def step_simulation() -> None:
"""Advance the simulation based on elapsed real time and multiplier.""" """Advance the simulation based on elapsed real time and multiplier."""
engine: PhysicsEngine = st.session_state.engine engine: PhysicsEngine = st.session_state.engine
history: SimulationHistory = st.session_state.history history: SimulationHistory = st.session_state.history
multiplier: float = st.session_state.time_multiplier multiplier: float = st.session_state.get("time_multiplier", 10)
# Calculate how much simulation time to advance # Calculate how much simulation time to advance
current_time = time.time() current_time = time.time()
@@ -87,10 +84,18 @@ def step_simulation() -> None:
history.power_dissipation.append(electrical.power_dissipation) history.power_dissipation.append(electrical.power_dissipation)
def display_controls() -> None: def sync_engine_from_session_state() -> None:
"""Display simulation control panel in sidebar.""" """Sync engine parameters from session state (called by fragment)."""
engine: PhysicsEngine = st.session_state.engine engine: PhysicsEngine = st.session_state.engine
engine.set_chamber_setpoint(st.session_state.get("temp_setpoint", 25.0))
engine.set_input_voltage(st.session_state.get("input_voltage", 5.0))
engine.set_output_enabled(st.session_state.get("output_enabled", False))
engine.set_load_current(st.session_state.get("load_current", 100.0) / 1000.0)
@st.fragment
def display_controls() -> None:
"""Display simulation control panel in sidebar (as fragment to avoid full reruns)."""
st.sidebar.header("Simulation Controls") st.sidebar.header("Simulation Controls")
# Start/Stop button # Start/Stop button
@@ -99,14 +104,12 @@ def display_controls() -> None:
"Stop Simulation", type="primary", use_container_width=True "Stop Simulation", type="primary", use_container_width=True
): ):
st.session_state.running = False st.session_state.running = False
st.rerun()
else: else:
if st.sidebar.button( if st.sidebar.button(
"Start Simulation", type="primary", use_container_width=True "Start Simulation", type="primary", use_container_width=True
): ):
st.session_state.running = True st.session_state.running = True
st.session_state.last_update = time.time() st.session_state.last_update = time.time()
st.rerun()
# Reset button # Reset button
if st.sidebar.button("Reset", use_container_width=True): if st.sidebar.button("Reset", use_container_width=True):
@@ -114,27 +117,27 @@ def display_controls() -> None:
st.session_state.history = SimulationHistory() st.session_state.history = SimulationHistory()
st.session_state.running = False st.session_state.running = False
st.session_state.last_update = time.time() st.session_state.last_update = time.time()
st.rerun()
st.sidebar.divider() st.sidebar.divider()
# Time multiplier # Time multiplier
st.sidebar.subheader("Simulation Speed") st.sidebar.subheader("Simulation Speed")
time_mult = st.sidebar.select_slider( st.sidebar.select_slider(
"Time Multiplier", "Time Multiplier",
options=[1, 2, 5, 10, 20, 50, 100], options=[1, 2, 5, 10, 20, 50, 100],
value=int(st.session_state.time_multiplier), value=10,
format_func=lambda x: f"{x}x", format_func=lambda x: f"{x}x",
key="time_mult_slider", key="time_multiplier",
)
st.sidebar.caption(
f"1 real second = {st.session_state.get('time_multiplier', 10)} simulation seconds"
) )
st.session_state.time_multiplier = float(time_mult)
st.sidebar.caption(f"1 real second = {time_mult} simulation seconds")
st.sidebar.divider() st.sidebar.divider()
# Temperature setpoint # Temperature setpoint
st.sidebar.subheader("Thermal Chamber") st.sidebar.subheader("Thermal Chamber")
temp_setpoint = st.sidebar.slider( st.sidebar.slider(
"Temperature Setpoint (C)", "Temperature Setpoint (C)",
min_value=-40.0, min_value=-40.0,
max_value=125.0, max_value=125.0,
@@ -142,13 +145,12 @@ def display_controls() -> None:
step=5.0, step=5.0,
key="temp_setpoint", key="temp_setpoint",
) )
engine.set_chamber_setpoint(temp_setpoint)
st.sidebar.divider() st.sidebar.divider()
# Power supply controls # Power supply controls
st.sidebar.subheader("Power Supply") st.sidebar.subheader("Power Supply")
input_voltage = st.sidebar.slider( st.sidebar.slider(
"Input Voltage (V)", "Input Voltage (V)",
min_value=0.0, min_value=0.0,
max_value=12.0, max_value=12.0,
@@ -156,20 +158,18 @@ def display_controls() -> None:
step=0.1, step=0.1,
key="input_voltage", key="input_voltage",
) )
engine.set_input_voltage(input_voltage)
output_enabled = st.sidebar.toggle( st.sidebar.toggle(
"Output Enabled", "Output Enabled",
value=engine.is_output_enabled, value=False,
key="output_enabled", key="output_enabled",
) )
engine.set_output_enabled(output_enabled)
st.sidebar.divider() st.sidebar.divider()
# Load controls # Load controls
st.sidebar.subheader("Electronic Load") st.sidebar.subheader("Electronic Load")
load_current_ma = st.sidebar.slider( st.sidebar.slider(
"Load Current (mA)", "Load Current (mA)",
min_value=0.0, min_value=0.0,
max_value=500.0, max_value=500.0,
@@ -177,7 +177,6 @@ def display_controls() -> None:
step=10.0, step=10.0,
key="load_current", key="load_current",
) )
engine.set_load_current(load_current_ma / 1000.0)
@st.fragment(run_every=0.1) @st.fragment(run_every=0.1)
@@ -186,6 +185,9 @@ def simulation_display() -> None:
engine: PhysicsEngine = st.session_state.engine engine: PhysicsEngine = st.session_state.engine
history: SimulationHistory = st.session_state.history history: SimulationHistory = st.session_state.history
# Sync engine parameters from UI controls
sync_engine_from_session_state()
# Step simulation if running # Step simulation if running
if st.session_state.running: if st.session_state.running:
step_simulation() step_simulation()
@@ -220,7 +222,7 @@ def simulation_display() -> None:
st.metric( st.metric(
"Sim Time", "Sim Time",
f"{engine.simulation_time:.1f} s", f"{engine.simulation_time:.1f} s",
delta=f"{status} @ {st.session_state.time_multiplier:.0f}x", delta=f"{status} @ {st.session_state.get('time_multiplier', 10):.0f}x",
) )
# Temperature chart # Temperature chart