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-12-02 02:57:19 +00:00
parent 151ad8d673
commit 1429bf34a7

View File

@@ -10,6 +10,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from py_dvt_ate.simulation.physics.state import ElectricalState, ThermalState 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: if TYPE_CHECKING:
from py_dvt_ate.simulation.physics.models.base import DUTModel 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 Runs at a fixed timestep, updating thermal and electrical state
based on the DUT model and environmental conditions. 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: Attributes:
dt: Simulation timestep in seconds. dt: Simulation timestep in seconds.
""" """
@@ -35,12 +46,19 @@ class PhysicsEngine:
Args: Args:
update_rate_hz: Simulation update rate in Hz. Defaults to 100. update_rate_hz: Simulation update rate in Hz. Defaults to 100.
dut_model: DUT model to use for electrical calculations. 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 self.dt = 1.0 / update_rate_hz
self._dut = dut_model
# Thermal parameters (to be configured in Sprint 3) # 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
self._tau_chamber = 30.0 # seconds self._tau_chamber = 30.0 # seconds
self._tau_case = 5.0 # seconds self._tau_case = 5.0 # seconds
self._theta_jc = 15.0 # degC/W self._theta_jc = 15.0 # degC/W
@@ -59,9 +77,34 @@ class PhysicsEngine:
"""Advance simulation by one timestep. """Advance simulation by one timestep.
Updates thermal and electrical state based on current conditions. 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 self._sim_time += self.dt
def get_thermal_state(self) -> ThermalState: def get_thermal_state(self) -> ThermalState:
@@ -70,11 +113,17 @@ class PhysicsEngine:
Returns: Returns:
Immutable ThermalState with current temperatures. 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( return ThermalState(
chamber_temperature=self._t_chamber, chamber_temperature=self._t_chamber,
case_temperature=self._t_case, case_temperature=self._t_case,
junction_temperature=self._t_case, # Stub: junction = case junction_temperature=t_junction,
timestamp=self._sim_time, timestamp=self._sim_time,
) )
@@ -84,13 +133,50 @@ class PhysicsEngine:
Returns: Returns:
Immutable ElectricalState with current electrical values. 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( return ElectricalState(
input_voltage=self._v_in, input_voltage=self._v_in,
output_voltage=0.0, # Stub: will be calculated from DUT model output_voltage=v_out,
load_current=self._i_load if self._output_enabled else 0.0, load_current=i_load,
quiescent_current=0.0, # Stub: will be calculated from DUT model quiescent_current=i_q,
power_dissipation=0.0, # Stub: will be calculated from DUT model 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: def set_chamber_setpoint(self, temperature: float) -> None: