From 1429bf34a7dd50cf72b86d6874751365c331dccb Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Tue, 2 Dec 2025 02:57:19 +0000 Subject: [PATCH] Implement physics engine stepping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/py_dvt_ate/simulation/physics/engine.py | 110 +++++++++++++++++--- 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/src/py_dvt_ate/simulation/physics/engine.py b/src/py_dvt_ate/simulation/physics/engine.py index 48cc094..37be793 100644 --- a/src/py_dvt_ate/simulation/physics/engine.py +++ b/src/py_dvt_ate/simulation/physics/engine.py @@ -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 - 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_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: