"""Virtual power supply simulator. This module implements a SCPI-based virtual power supply that interfaces with the physics engine to provide realistic power supply 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 PowerSupplySim(BaseInstrument): """Virtual power supply simulator. Simulates a programmable DC power supply with SCPI control interface. The power supply provides input voltage to the DUT via the physics engine. SCPI Commands: VOLT - Set output voltage in volts VOLT? - Query voltage setpoint CURR - Set current limit in amps CURR? - Query current limit OUTP - Enable/disable output OUTP? - Query output state (1=on, 0=off) MEAS:VOLT? - Measure actual output voltage MEAS:CURR? - Measure actual output current Attributes: manufacturer: "PyDVTATE" model: "PS-SIM-001" """ manufacturer = "PyDVTATE" model = "PS-SIM-001" serial_number = "PSSIM001" firmware_version = "1.0.0" # Default values DEFAULT_VOLTAGE = 0.0 DEFAULT_CURRENT_LIMIT = 1.0 def __init__(self, physics_engine: PhysicsEngine | None = None) -> None: """Initialise the power supply simulator. Args: physics_engine: Reference to physics engine for electrical state. """ self._voltage_setpoint = self.DEFAULT_VOLTAGE self._current_limit = self.DEFAULT_CURRENT_LIMIT self._output_enabled = False super().__init__(physics_engine) def _setup_commands(self) -> None: """Register power supply SCPI commands.""" self.register_command("VOLT", self._handle_volt) self.register_command("CURR", self._handle_curr) self.register_command("OUTP", self._handle_outp) self.register_command("MEAS:VOLT", self._handle_meas_volt) self.register_command("MEAS:CURR", self._handle_meas_curr) def reset(self) -> None: """Reset power supply to default state.""" self._voltage_setpoint = self.DEFAULT_VOLTAGE self._current_limit = self.DEFAULT_CURRENT_LIMIT self._output_enabled = False if self._physics_engine is not None: self._physics_engine.set_input_voltage(0.0) self._physics_engine.set_output_enabled(False) def _handle_volt(self, command: SCPICommand) -> str: """Handle VOLT command/query. Args: command: Parsed SCPI command. Returns: Voltage setpoint for query, empty string for set command. Raises: ValueError: If voltage argument is invalid. """ if command.is_query: return f"{self._voltage_setpoint:.3f}" if not command.arguments: raise ValueError("VOLT requires a value") try: voltage = float(command.arguments[0]) except ValueError as err: raise ValueError(f"Invalid voltage value: {command.arguments[0]}") from err if voltage < 0: raise ValueError("Voltage cannot be negative") self._voltage_setpoint = voltage if self._physics_engine is not None and self._output_enabled: self._physics_engine.set_input_voltage(voltage) return "" def _handle_curr(self, command: SCPICommand) -> str: """Handle CURR command/query. Args: command: Parsed SCPI command. Returns: Current limit for query, empty string for set command. Raises: ValueError: If current argument is invalid. """ if command.is_query: return f"{self._current_limit:.3f}" if not command.arguments: raise ValueError("CURR requires a value") try: current = float(command.arguments[0]) except ValueError as err: raise ValueError(f"Invalid current value: {command.arguments[0]}") from err if current < 0: raise ValueError("Current limit cannot be negative") self._current_limit = current return "" def _handle_outp(self, command: SCPICommand) -> str: """Handle OUTP command/query. Args: command: Parsed SCPI command. Returns: "1" or "0" for query, empty string for set command. Raises: ValueError: If output argument is invalid. """ if command.is_query: return "1" if self._output_enabled else "0" if not command.arguments: raise ValueError("OUTP requires a value (ON, OFF, 1, or 0)") arg = command.arguments[0].upper() if arg in ("ON", "1"): self._output_enabled = True elif arg in ("OFF", "0"): self._output_enabled = False else: raise ValueError(f"Invalid output state: {command.arguments[0]}") if self._physics_engine is not None: self._physics_engine.set_output_enabled(self._output_enabled) if self._output_enabled: self._physics_engine.set_input_voltage(self._voltage_setpoint) else: self._physics_engine.set_input_voltage(0.0) return "" def _handle_meas_volt(self, command: SCPICommand) -> str: """Handle MEAS:VOLT? query. Args: command: Parsed SCPI command. Returns: Measured output voltage. Raises: ValueError: If used as command (not query). """ if not command.is_query: raise ValueError("MEAS:VOLT is query only") if not self._output_enabled: return "0.000" if self._physics_engine is None: return f"{self._voltage_setpoint:.3f}" electrical_state = self._physics_engine.get_electrical_state() return f"{electrical_state.input_voltage:.3f}" def _handle_meas_curr(self, command: SCPICommand) -> str: """Handle MEAS:CURR? query. Args: command: Parsed SCPI command. Returns: Measured output current. Raises: ValueError: If used as command (not query). """ if not command.is_query: raise ValueError("MEAS:CURR is query only") if not self._output_enabled: return "0.000" if self._physics_engine is None: return "0.000" electrical_state = self._physics_engine.get_electrical_state() # Total current is load current + quiescent current total_current = electrical_state.load_current + electrical_state.quiescent_current return f"{total_current:.3f}"