Integration tests for thermal-electrical coupling: - Thermal settling (chamber, case, junction) - Self-heating effects with power dissipation - Temperature-dependent electrical behaviour - Complete thermal-electrical feedback loop
301 lines
10 KiB
Python
301 lines
10 KiB
Python
"""Integration tests for the physics engine with thermal-electrical coupling.
|
|
|
|
Tests the complete physics simulation including:
|
|
- Thermal settling behaviour (chamber, case, junction)
|
|
- Self-heating effects from power dissipation
|
|
- Temperature-dependent electrical behaviour
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
|
|
from py_dvt_ate.simulation.physics.models.ldo import LDOModel, LDOParameters
|
|
|
|
|
|
class TestThermalSettling:
|
|
"""Tests for thermal settling behaviour."""
|
|
|
|
def test_chamber_approaches_setpoint(self) -> None:
|
|
"""Test chamber temperature approaches setpoint over time."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Set a new setpoint
|
|
engine.set_chamber_setpoint(85.0)
|
|
|
|
# Initial state
|
|
initial_state = engine.get_thermal_state()
|
|
assert initial_state.chamber_temperature == pytest.approx(25.0)
|
|
|
|
# Simulate for 1 second (100 steps at 100Hz)
|
|
for _ in range(100):
|
|
engine.step()
|
|
|
|
state_1s = engine.get_thermal_state()
|
|
|
|
# Chamber should have moved towards 85°C but not reached it yet
|
|
# With tau=30s, after 1s: T = 25 + (85-25)*(1 - e^(-1/30)) ≈ 27.0°C
|
|
assert state_1s.chamber_temperature > 25.0
|
|
assert state_1s.chamber_temperature < 85.0
|
|
|
|
# Simulate for another 89 seconds (total 90s = 3*tau)
|
|
for _ in range(8900):
|
|
engine.step()
|
|
|
|
state_90s = engine.get_thermal_state()
|
|
|
|
# After 3 time constants, should be ~95% of the way there
|
|
# T = 25 + (85-25)*(1 - e^(-3)) ≈ 82.0°C
|
|
assert state_90s.chamber_temperature > 80.0
|
|
assert state_90s.chamber_temperature < 85.0
|
|
|
|
def test_case_follows_chamber(self) -> None:
|
|
"""Test case temperature follows chamber temperature."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Set chamber to high temperature (no power, so no self-heating)
|
|
engine.set_chamber_setpoint(85.0)
|
|
|
|
# Simulate for 200 seconds (well past both time constants)
|
|
for _ in range(20000):
|
|
engine.step()
|
|
|
|
state = engine.get_thermal_state()
|
|
|
|
# With no power, case should approach chamber temperature
|
|
assert state.case_temperature == pytest.approx(
|
|
state.chamber_temperature, abs=0.5
|
|
)
|
|
|
|
def test_junction_equals_case_with_no_power(self) -> None:
|
|
"""Test junction equals case temperature when no power dissipated."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# No power applied (output disabled by default)
|
|
state = engine.get_thermal_state()
|
|
|
|
# Junction should equal case (no θ_jc rise with zero power)
|
|
assert state.junction_temperature == pytest.approx(state.case_temperature)
|
|
|
|
|
|
class TestSelfHeating:
|
|
"""Tests for self-heating effects from power dissipation."""
|
|
|
|
def test_junction_higher_than_case_with_power(self) -> None:
|
|
"""Test junction temperature rises above case when power is dissipated."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Apply power: 5V input, 100mA load
|
|
engine.set_input_voltage(5.0)
|
|
engine.set_load_current(0.1)
|
|
engine.set_output_enabled(True)
|
|
|
|
# Let it settle
|
|
for _ in range(1000):
|
|
engine.step()
|
|
|
|
thermal_state = engine.get_thermal_state()
|
|
electrical_state = engine.get_electrical_state()
|
|
|
|
# Power dissipation = (Vin - Vout) * Iload + Vin * Iq
|
|
# With Vout ≈ 3.3V, P ≈ (5-3.3)*0.1 ≈ 0.17W
|
|
assert electrical_state.power_dissipation > 0
|
|
|
|
# Junction should be higher than case by P * θ_jc
|
|
# θ_jc = 15°C/W, so with 0.17W, ΔT ≈ 2.5°C
|
|
assert (
|
|
thermal_state.junction_temperature > thermal_state.case_temperature
|
|
)
|
|
|
|
def test_self_heating_raises_case_temperature(self) -> None:
|
|
"""Test self-heating raises case temperature above ambient."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Apply significant power: 5V input, 300mA load
|
|
engine.set_input_voltage(5.0)
|
|
engine.set_load_current(0.3)
|
|
engine.set_output_enabled(True)
|
|
|
|
# Let thermal state settle (many time constants)
|
|
for _ in range(50000): # 500 seconds at 100Hz
|
|
engine.step()
|
|
|
|
thermal_state = engine.get_thermal_state()
|
|
|
|
# Power ≈ (5-3.3)*0.3 ≈ 0.51W
|
|
# Steady-state case rise = P * θ_ca = 0.51 * 5 ≈ 2.5°C
|
|
# Case should be above chamber temperature
|
|
assert (
|
|
thermal_state.case_temperature > thermal_state.chamber_temperature
|
|
)
|
|
|
|
def test_self_heating_increases_with_load(self) -> None:
|
|
"""Test that self-heating increases with higher load current."""
|
|
engine1 = PhysicsEngine(update_rate_hz=100.0)
|
|
engine2 = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Both at 5V, but different loads
|
|
for engine, load in [(engine1, 0.1), (engine2, 0.3)]:
|
|
engine.set_input_voltage(5.0)
|
|
engine.set_load_current(load)
|
|
engine.set_output_enabled(True)
|
|
|
|
# Let settle
|
|
for _ in range(50000):
|
|
engine.step()
|
|
|
|
state1 = engine1.get_thermal_state()
|
|
state2 = engine2.get_thermal_state()
|
|
|
|
# Higher load should give higher junction temperature
|
|
assert (
|
|
state2.junction_temperature > state1.junction_temperature
|
|
)
|
|
|
|
|
|
class TestTemperatureDependentElectrical:
|
|
"""Tests for temperature-dependent electrical behaviour."""
|
|
|
|
def test_output_voltage_varies_with_temperature(self) -> None:
|
|
"""Test output voltage changes with junction temperature."""
|
|
# Create custom LDO with higher tempco for observable effect
|
|
params = LDOParameters(
|
|
nominal_output_voltage=3.3,
|
|
tempco_ppm_per_c=100.0, # 100 ppm/°C for visible effect
|
|
)
|
|
ldo = LDOModel(params=params)
|
|
|
|
# Test at different temperatures
|
|
vout_25 = ldo.calculate_output_voltage(25.0)
|
|
vout_85 = ldo.calculate_output_voltage(85.0)
|
|
|
|
# At 85°C with 100 ppm/°C:
|
|
# ΔV = 3.3 * 100e-6 * 60 = 19.8mV
|
|
delta_v = vout_85 - vout_25
|
|
expected_delta = 3.3 * 100e-6 * 60
|
|
|
|
assert delta_v == pytest.approx(expected_delta, rel=0.01)
|
|
|
|
def test_quiescent_current_varies_with_temperature(self) -> None:
|
|
"""Test quiescent current changes with junction temperature."""
|
|
ldo = LDOModel()
|
|
|
|
# Test at different temperatures
|
|
iq_25 = ldo.calculate_quiescent_current(25.0)
|
|
iq_85 = ldo.calculate_quiescent_current(85.0)
|
|
|
|
# Default tempco is 0.003/°C (0.3%/°C)
|
|
# At 85°C: Iq = Iq_25 * (1 + 0.003 * 60) = Iq_25 * 1.18
|
|
expected_ratio = 1.0 + 0.003 * 60
|
|
|
|
assert iq_85 / iq_25 == pytest.approx(expected_ratio, rel=0.01)
|
|
|
|
def test_power_dissipation_calculation(self) -> None:
|
|
"""Test power dissipation is calculated correctly."""
|
|
ldo = LDOModel()
|
|
ldo.input_voltage = 5.0
|
|
|
|
# P = (Vin - Vout) * Iload + Vin * Iq
|
|
# At 25°C: Vout ≈ 3.3V, Iq ≈ 50µA
|
|
# With 100mA load: P ≈ (5-3.3)*0.1 + 5*50e-6 ≈ 0.170W
|
|
p_diss = ldo.calculate_power_dissipation(
|
|
input_voltage=5.0,
|
|
load_current=0.1,
|
|
junction_temperature=25.0,
|
|
)
|
|
|
|
expected_p = (5.0 - 3.3) * 0.1 + 5.0 * 50e-6
|
|
assert p_diss == pytest.approx(expected_p, rel=0.01)
|
|
|
|
|
|
class TestPhysicsEngineCoupling:
|
|
"""Tests for complete thermal-electrical coupling in the engine."""
|
|
|
|
def test_thermal_electrical_feedback(self) -> None:
|
|
"""Test that thermal and electrical states are coupled.
|
|
|
|
Higher junction temperature affects Vout, which affects power
|
|
dissipation, which affects junction temperature - a feedback loop.
|
|
"""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Apply power at hot chamber
|
|
engine.set_chamber_setpoint(85.0)
|
|
engine.set_input_voltage(5.0)
|
|
engine.set_load_current(0.2)
|
|
engine.set_output_enabled(True)
|
|
|
|
# Let settle completely
|
|
for _ in range(100000): # 1000 seconds
|
|
engine.step()
|
|
|
|
thermal = engine.get_thermal_state()
|
|
electrical = engine.get_electrical_state()
|
|
|
|
# Verify the coupling:
|
|
# 1. Chamber should be near 85°C
|
|
assert thermal.chamber_temperature == pytest.approx(85.0, abs=0.5)
|
|
|
|
# 2. Case should be higher than chamber due to self-heating
|
|
assert thermal.case_temperature > thermal.chamber_temperature
|
|
|
|
# 3. Junction should be higher than case
|
|
assert thermal.junction_temperature > thermal.case_temperature
|
|
|
|
# 4. Output voltage should reflect temperature-adjusted value
|
|
assert electrical.output_voltage > 0
|
|
assert electrical.output_voltage < 5.0 # Less than input
|
|
|
|
# 5. Power should be non-zero
|
|
assert electrical.power_dissipation > 0
|
|
|
|
def test_engine_with_custom_dut_model(self) -> None:
|
|
"""Test engine works with custom DUT model parameters."""
|
|
params = LDOParameters(
|
|
nominal_output_voltage=1.8, # Different voltage
|
|
tempco_ppm_per_c=25.0,
|
|
)
|
|
ldo = LDOModel(params=params)
|
|
engine = PhysicsEngine(update_rate_hz=100.0, dut_model=ldo)
|
|
|
|
engine.set_input_voltage(3.3)
|
|
engine.set_load_current(0.1)
|
|
engine.set_output_enabled(True)
|
|
|
|
for _ in range(1000):
|
|
engine.step()
|
|
|
|
electrical = engine.get_electrical_state()
|
|
|
|
# Should output approximately 1.8V
|
|
assert electrical.output_voltage == pytest.approx(1.8, abs=0.01)
|
|
|
|
def test_simulation_time_accuracy(self) -> None:
|
|
"""Test simulation time accumulates correctly."""
|
|
engine = PhysicsEngine(update_rate_hz=1000.0) # 1ms timestep
|
|
|
|
for _ in range(1000):
|
|
engine.step()
|
|
|
|
# Should be exactly 1 second
|
|
assert engine.simulation_time == pytest.approx(1.0, abs=1e-6)
|
|
|
|
def test_multiple_setpoint_changes(self) -> None:
|
|
"""Test engine handles multiple setpoint changes correctly."""
|
|
engine = PhysicsEngine(update_rate_hz=100.0)
|
|
|
|
# Start at 25°C, go to 85°C
|
|
engine.set_chamber_setpoint(85.0)
|
|
for _ in range(10000): # 100 seconds
|
|
engine.step()
|
|
|
|
hot_state = engine.get_thermal_state()
|
|
assert hot_state.chamber_temperature > 75.0
|
|
|
|
# Now cool down to -40°C
|
|
engine.set_chamber_setpoint(-40.0)
|
|
for _ in range(20000): # 200 seconds
|
|
engine.step()
|
|
|
|
cold_state = engine.get_thermal_state()
|
|
assert cold_state.chamber_temperature < -30.0
|