Implement physics engine stepping

Full implementation of step() method with thermal-electrical coupling:
- Chamber temperature first-order response to setpoint
- Case temperature with self-heating via thermal calculations
- Junction temperature from θ_jc thermal resistance
- Electrical state from temperature-dependent DUT model
- Default LDO model when none provided
This commit is contained in:
2025-03-11 19:23:10 +00:00
parent f0938a4414
commit 04d7d38e43

View File

@@ -10,6 +10,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from py_dvt_ate.simulation.physics.state import ElectricalState, ThermalState
from py_dvt_ate.simulation.physics.thermal import (
calculate_junction_temperature,
update_case_temperature,
update_temperature,
)
if TYPE_CHECKING:
from py_dvt_ate.simulation.physics.models.base import DUTModel
@@ -21,6 +26,12 @@ class PhysicsEngine:
Runs at a fixed timestep, updating thermal and electrical state
based on the DUT model and environmental conditions.
The simulation models:
- Chamber temperature approaching setpoint (first-order response)
- Case temperature driven by chamber and self-heating
- Junction temperature from case temperature and thermal resistance
- Electrical behaviour from the DUT model (temperature-dependent)
Attributes:
dt: Simulation timestep in seconds.
"""
@@ -35,12 +46,19 @@ class PhysicsEngine:
Args:
update_rate_hz: Simulation update rate in Hz. Defaults to 100.
dut_model: DUT model to use for electrical calculations.
If None, a default model will be used when implemented.
If None, a default LDO model will be used.
"""
self.dt = 1.0 / update_rate_hz
# Lazily import to avoid circular dependencies
if dut_model is None:
from py_dvt_ate.simulation.physics.models.ldo import LDOModel
self._dut: DUTModel = LDOModel()
else:
self._dut = dut_model
# Thermal parameters (to be configured in Sprint 3)
# Thermal parameters
self._tau_chamber = 30.0 # seconds
self._tau_case = 5.0 # seconds
self._theta_jc = 15.0 # degC/W
@@ -59,9 +77,34 @@ class PhysicsEngine:
"""Advance simulation by one timestep.
Updates thermal and electrical state based on current conditions.
Full implementation in Sprint 3.
The thermal-electrical coupling works as follows:
1. Calculate current power dissipation from DUT model
2. Update chamber temperature towards setpoint
3. Update case temperature including self-heating
4. Advance simulation time
"""
# Stub: just advance time
# Calculate power dissipation (uses current junction temperature estimate)
p_diss = self._calculate_power_dissipation()
# Update chamber temperature (first-order response to setpoint)
self._t_chamber = update_temperature(
current_temperature=self._t_chamber,
target_temperature=self._t_setpoint,
time_constant=self._tau_chamber,
dt=self.dt,
)
# Update case temperature (driven by chamber + self-heating)
self._t_case = update_case_temperature(
case_temperature=self._t_case,
ambient_temperature=self._t_chamber,
power_dissipation=p_diss,
time_constant=self._tau_case,
theta_ca=self._theta_ca,
dt=self.dt,
)
# Advance simulation time
self._sim_time += self.dt
def get_thermal_state(self) -> ThermalState:
@@ -70,11 +113,17 @@ class PhysicsEngine:
Returns:
Immutable ThermalState with current temperatures.
"""
# Stub: return current state values
p_diss = self._calculate_power_dissipation()
t_junction = calculate_junction_temperature(
case_temperature=self._t_case,
power_dissipation=p_diss,
theta_jc=self._theta_jc,
)
return ThermalState(
chamber_temperature=self._t_chamber,
case_temperature=self._t_case,
junction_temperature=self._t_case, # Stub: junction = case
junction_temperature=t_junction,
timestamp=self._sim_time,
)
@@ -84,13 +133,50 @@ class PhysicsEngine:
Returns:
Immutable ElectricalState with current electrical values.
"""
# Stub: return placeholder values
p_diss = self._calculate_power_dissipation()
t_junction = calculate_junction_temperature(
case_temperature=self._t_case,
power_dissipation=p_diss,
theta_jc=self._theta_jc,
)
if self._output_enabled:
v_out = self._dut.calculate_output_voltage(t_junction)
i_q = self._dut.calculate_quiescent_current(t_junction)
i_load = self._i_load
else:
v_out = 0.0
i_q = 0.0
i_load = 0.0
return ElectricalState(
input_voltage=self._v_in,
output_voltage=0.0, # Stub: will be calculated from DUT model
load_current=self._i_load if self._output_enabled else 0.0,
quiescent_current=0.0, # Stub: will be calculated from DUT model
power_dissipation=0.0, # Stub: will be calculated from DUT model
output_voltage=v_out,
load_current=i_load,
quiescent_current=i_q,
power_dissipation=p_diss,
)
def _calculate_power_dissipation(self) -> float:
"""Calculate current power dissipation.
Uses the current case temperature as an approximation for junction
temperature in the power calculation. The true junction temperature
depends on power dissipation, creating a feedback loop that is
resolved iteratively through the simulation steps.
Returns:
Power dissipation in watts.
"""
if not self._output_enabled:
return 0.0
# Use case temperature as junction estimate for power calculation
# This avoids circular dependency in the calculation
return self._dut.calculate_power_dissipation(
input_voltage=self._v_in,
load_current=self._i_load,
junction_temperature=self._t_case,
)
def set_chamber_setpoint(self, temperature: float) -> None: