"""Virtual thermal chamber simulator. This module implements a SCPI-based virtual thermal chamber that interfaces with the physics engine to provide realistic temperature control simulation. """ from __future__ import annotations from typing import TYPE_CHECKING from py_dvt_ate.instruments.scpi import SCPICommand from py_dvt_ate.simulation.virtual.base import BaseInstrument if TYPE_CHECKING: from py_dvt_ate.simulation.physics.engine import PhysicsEngine class ThermalChamberSim(BaseInstrument): """Virtual thermal chamber simulator. Simulates a thermal chamber with SCPI control interface. The chamber temperature behaviour is driven by the physics engine. SCPI Commands: TEMP:SETPOINT - Set target temperature in degrees C TEMP:SETPOINT? - Query current setpoint TEMP:ACTUAL? - Query actual chamber temperature TEMP:STAB? - Query temperature stability (1=stable, 0=settling) Attributes: manufacturer: "PyDVTATE" model: "TC-SIM-001" """ manufacturer = "PyDVTATE" model = "TC-SIM-001" serial_number = "TCSIM001" firmware_version = "1.0.0" # Stability threshold in degrees C STABILITY_THRESHOLD = 0.5 def __init__(self, physics_engine: PhysicsEngine | None = None) -> None: """Initialise the thermal chamber simulator. Args: physics_engine: Reference to physics engine for temperature state. """ self._setpoint = 25.0 # Default setpoint super().__init__(physics_engine) def _setup_commands(self) -> None: """Register thermal chamber SCPI commands.""" self.register_command("TEMP:SETPOINT", self._handle_temp_setpoint) self.register_command("TEMP:ACTUAL", self._handle_temp_actual) self.register_command("TEMP:STAB", self._handle_temp_stab) def reset(self) -> None: """Reset chamber to default state.""" self._setpoint = 25.0 if self._physics_engine is not None: self._physics_engine.set_chamber_setpoint(self._setpoint) def _handle_temp_setpoint(self, command: SCPICommand) -> str: """Handle TEMP:SETPOINT command/query. Args: command: Parsed SCPI command. Returns: Setpoint value for query, empty string for set command. Raises: ValueError: If setpoint argument is invalid. """ if command.is_query: return f"{self._setpoint:.2f}" # Set command requires one argument if not command.arguments: raise ValueError("TEMP:SETPOINT requires a value") try: setpoint = float(command.arguments[0]) except ValueError: raise ValueError(f"Invalid temperature value: {command.arguments[0]}") self._setpoint = setpoint if self._physics_engine is not None: self._physics_engine.set_chamber_setpoint(setpoint) return "" def _handle_temp_actual(self, command: SCPICommand) -> str: """Handle TEMP:ACTUAL? query. Args: command: Parsed SCPI command. Returns: Actual chamber temperature. Raises: ValueError: If used as command (not query). """ if not command.is_query: raise ValueError("TEMP:ACTUAL is query only") if self._physics_engine is None: # Return setpoint if no physics engine connected return f"{self._setpoint:.2f}" thermal_state = self._physics_engine.get_thermal_state() return f"{thermal_state.chamber_temperature:.2f}" def _handle_temp_stab(self, command: SCPICommand) -> str: """Handle TEMP:STAB? stability query. Temperature is considered stable when the actual chamber temperature is within STABILITY_THRESHOLD of the setpoint. Args: command: Parsed SCPI command. Returns: "1" if stable, "0" if settling. Raises: ValueError: If used as command (not query). """ if not command.is_query: raise ValueError("TEMP:STAB is query only") if self._physics_engine is None: # Assume stable if no physics engine connected return "1" thermal_state = self._physics_engine.get_thermal_state() error = abs(thermal_state.chamber_temperature - self._setpoint) if error <= self.STABILITY_THRESHOLD: return "1" return "0"