Add thermal chamber simulator tests

Tests for ThermalChamberSim SCPI command responses:
- Basic IEEE 488.2 commands (*IDN?, *RST, *OPC?)
- TEMP:SETPOINT set/query
- TEMP:ACTUAL? query
- TEMP:STAB? stability query
- Physics engine integration tests
This commit is contained in:
2025-12-02 13:27:41 +00:00
parent 6fae05f52d
commit ca48541b91

View File

@@ -0,0 +1,215 @@
"""Unit tests for thermal chamber simulator."""
import pytest
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
from py_dvt_ate.simulation.virtual.chamber import ThermalChamberSim
class TestThermalChamberSimBasic:
"""Tests for ThermalChamberSim without physics engine."""
@pytest.fixture
def chamber(self) -> ThermalChamberSim:
"""Create chamber instance without physics engine."""
return ThermalChamberSim()
def test_creation(self, chamber: ThermalChamberSim) -> None:
"""Test chamber can be created."""
assert chamber is not None
assert chamber.model == "TC-SIM-001"
assert chamber.manufacturer == "PyDVTATE"
def test_idn_query(self, chamber: ThermalChamberSim) -> None:
"""Test *IDN? returns identification string."""
response = chamber.process("*IDN?")
assert "PyDVTATE" in response
assert "TC-SIM-001" in response
def test_rst_command(self, chamber: ThermalChamberSim) -> None:
"""Test *RST resets to defaults."""
# Set non-default value
chamber.process("TEMP:SETPOINT 85.0")
assert chamber.process("TEMP:SETPOINT?") == "85.00"
# Reset
response = chamber.process("*RST")
assert response == ""
assert chamber.process("TEMP:SETPOINT?") == "25.00"
def test_opc_query(self, chamber: ThermalChamberSim) -> None:
"""Test *OPC? returns 1."""
response = chamber.process("*OPC?")
assert response == "1"
def test_unknown_command(self, chamber: ThermalChamberSim) -> None:
"""Test unknown command returns error."""
response = chamber.process("INVALID:CMD")
assert response.startswith("ERROR:")
assert "Unknown command" in response
class TestThermalChamberSetpoint:
"""Tests for TEMP:SETPOINT command."""
@pytest.fixture
def chamber(self) -> ThermalChamberSim:
"""Create chamber instance without physics engine."""
return ThermalChamberSim()
def test_setpoint_query_default(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:SETPOINT? returns default value."""
response = chamber.process("TEMP:SETPOINT?")
assert response == "25.00"
def test_setpoint_set(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:SETPOINT sets value."""
response = chamber.process("TEMP:SETPOINT 85.0")
assert response == ""
assert chamber.process("TEMP:SETPOINT?") == "85.00"
def test_setpoint_set_negative(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:SETPOINT accepts negative values."""
chamber.process("TEMP:SETPOINT -40.0")
assert chamber.process("TEMP:SETPOINT?") == "-40.00"
def test_setpoint_set_invalid_value(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:SETPOINT with invalid value returns error."""
response = chamber.process("TEMP:SETPOINT abc")
assert response.startswith("ERROR:")
assert "Invalid temperature" in response
def test_setpoint_set_no_argument(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:SETPOINT without argument returns error."""
response = chamber.process("TEMP:SETPOINT")
assert response.startswith("ERROR:")
assert "requires a value" in response
class TestThermalChamberActual:
"""Tests for TEMP:ACTUAL? query."""
@pytest.fixture
def chamber(self) -> ThermalChamberSim:
"""Create chamber instance without physics engine."""
return ThermalChamberSim()
def test_actual_query_without_engine(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:ACTUAL? returns setpoint when no physics engine."""
chamber.process("TEMP:SETPOINT 50.0")
response = chamber.process("TEMP:ACTUAL?")
# Without physics engine, returns setpoint
assert response == "50.00"
def test_actual_as_command_fails(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:ACTUAL (without ?) returns error."""
response = chamber.process("TEMP:ACTUAL 25.0")
assert response.startswith("ERROR:")
assert "query only" in response
class TestThermalChamberStability:
"""Tests for TEMP:STAB? query."""
@pytest.fixture
def chamber(self) -> ThermalChamberSim:
"""Create chamber instance without physics engine."""
return ThermalChamberSim()
def test_stab_query_without_engine(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:STAB? returns 1 when no physics engine."""
response = chamber.process("TEMP:STAB?")
# Without physics engine, assume stable
assert response == "1"
def test_stab_as_command_fails(self, chamber: ThermalChamberSim) -> None:
"""Test TEMP:STAB (without ?) returns error."""
response = chamber.process("TEMP:STAB 1")
assert response.startswith("ERROR:")
assert "query only" in response
class TestThermalChamberWithPhysicsEngine:
"""Tests for ThermalChamberSim with physics engine integration."""
@pytest.fixture
def engine(self) -> PhysicsEngine:
"""Create physics engine instance."""
return PhysicsEngine(update_rate_hz=100.0)
@pytest.fixture
def chamber(self, engine: PhysicsEngine) -> ThermalChamberSim:
"""Create chamber instance with physics engine."""
return ThermalChamberSim(physics_engine=engine)
def test_setpoint_updates_engine(
self, chamber: ThermalChamberSim, engine: PhysicsEngine
) -> None:
"""Test TEMP:SETPOINT updates physics engine."""
chamber.process("TEMP:SETPOINT 85.0")
# Step the engine and check thermal state
thermal = engine.get_thermal_state()
# Initial chamber temp is 25, will start moving towards 85
assert thermal.chamber_temperature == pytest.approx(25.0, abs=0.1)
# After many steps, should approach setpoint
for _ in range(10000): # 100 seconds at 100Hz
engine.step()
thermal = engine.get_thermal_state()
# Should be closer to setpoint (but not quite there due to time constant)
assert thermal.chamber_temperature > 80.0
def test_actual_query_returns_engine_temperature(
self, chamber: ThermalChamberSim, engine: PhysicsEngine
) -> None:
"""Test TEMP:ACTUAL? returns physics engine temperature."""
response = chamber.process("TEMP:ACTUAL?")
# Should match initial chamber temperature
assert response == "25.00"
def test_stability_when_at_setpoint(
self, chamber: ThermalChamberSim, engine: PhysicsEngine
) -> None:
"""Test TEMP:STAB? returns 1 when at setpoint."""
# Default setpoint is 25, engine starts at 25
response = chamber.process("TEMP:STAB?")
assert response == "1"
def test_stability_when_settling(
self, chamber: ThermalChamberSim, engine: PhysicsEngine
) -> None:
"""Test TEMP:STAB? returns 0 when settling."""
# Set new setpoint far from current temperature
chamber.process("TEMP:SETPOINT 85.0")
# Step once to ensure engine updates
engine.step()
# Should not be stable yet
response = chamber.process("TEMP:STAB?")
assert response == "0"
def test_reset_updates_engine(
self, chamber: ThermalChamberSim, engine: PhysicsEngine
) -> None:
"""Test *RST resets both chamber and engine setpoint."""
chamber.process("TEMP:SETPOINT 85.0")
chamber.process("*RST")
# Check setpoint is back to default
assert chamber.process("TEMP:SETPOINT?") == "25.00"