From 9a88a35cc5a240b521a0911d9122549c6f324535 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Fri, 9 May 2025 20:21:07 +0000 Subject: [PATCH] Add power supply simulator Implement SCPI-based virtual power supply with voltage/current control and output enable commands. Integrates with physics engine for DUT input voltage simulation. --- .../simulation/virtual/power_supply.py | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/py_dvt_ate/simulation/virtual/power_supply.py diff --git a/src/py_dvt_ate/simulation/virtual/power_supply.py b/src/py_dvt_ate/simulation/virtual/power_supply.py new file mode 100644 index 0000000..7045bca --- /dev/null +++ b/src/py_dvt_ate/simulation/virtual/power_supply.py @@ -0,0 +1,222 @@ +"""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: + raise ValueError(f"Invalid voltage value: {command.arguments[0]}") + + 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: + raise ValueError(f"Invalid current value: {command.arguments[0]}") + + 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}"