"""Physics engine for coupled thermal-electrical simulation. The physics engine maintains the simulation state and advances it in discrete time steps, modelling the thermal and electrical coupling between the DUT and its environment. """ 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 class PhysicsEngine: """Coupled thermal-electrical physics simulation. 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. """ def __init__( self, update_rate_hz: float = 100.0, dut_model: DUTModel | None = None, ) -> None: """Initialise the physics engine. 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 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 self._tau_chamber = 30.0 # seconds self._tau_case = 5.0 # seconds self._theta_jc = 15.0 # degC/W self._theta_ca = 5.0 # degC/W # State variables self._t_setpoint = 25.0 self._t_chamber = 25.0 self._t_case = 25.0 self._v_in = 0.0 self._i_load = 0.0 self._output_enabled = False self._sim_time = 0.0 def step(self) -> None: """Advance simulation by one timestep. Updates thermal and electrical state based on current conditions. 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 """ # 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: """Get current thermal state snapshot. Returns: Immutable ThermalState with current temperatures. """ 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=t_junction, timestamp=self._sim_time, ) def get_electrical_state(self) -> ElectricalState: """Get current electrical state snapshot. Returns: Immutable ElectricalState with current electrical 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=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: """Set chamber target temperature. Args: temperature: Target temperature in degrees Celsius. """ self._t_setpoint = temperature def set_input_voltage(self, voltage: float) -> None: """Set DUT input voltage. Args: voltage: Input voltage in volts. """ self._v_in = voltage def set_load_current(self, current: float) -> None: """Set DUT load current. Args: current: Load current in amps. """ self._i_load = current def set_output_enabled(self, enabled: bool) -> None: """Enable or disable DUT power. Args: enabled: True to enable output, False to disable. """ self._output_enabled = enabled @property def simulation_time(self) -> float: """Get current simulation time in seconds.""" return self._sim_time @property def is_output_enabled(self) -> bool: """Check if DUT output is enabled.""" return self._output_enabled