Files
py-dvt-ate/src/py_dvt_ate/simulation/physics/engine.py
Kai Chappell 568d1a6ca4 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
2025-03-11 19:23:10 +00:00

223 lines
7.0 KiB
Python

"""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