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
223 lines
7.0 KiB
Python
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
|