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.
This commit is contained in:
222
src/py_dvt_ate/simulation/virtual/power_supply.py
Normal file
222
src/py_dvt_ate/simulation/virtual/power_supply.py
Normal file
@@ -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 <value> - Set output voltage in volts
|
||||
VOLT? - Query voltage setpoint
|
||||
CURR <value> - Set current limit in amps
|
||||
CURR? - Query current limit
|
||||
OUTP <ON|OFF|1|0> - 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}"
|
||||
Reference in New Issue
Block a user