Files
py-dvt-ate/src/py_dvt_ate/simulation/virtual/power_supply.py
Kai Chappell e36349c853
All checks were successful
CI / Lint (push) Successful in 4s
CI / Type Check (push) Successful in 16s
CI / Test (push) Successful in 9s
CI / Release (push) Has been skipped
Fix linting and type errors for CI
- Use X | None syntax instead of Optional[X] (UP045)
- Sort imports in dashboard app (I001)
- Remove unnecessary UTF-8 encoding argument (UP012)
- Add 'from err' to exception re-raises (B904)
- Remove unused imports in integration tests (F401)
- Fix useless expression in test (B018)
- Cast **1.5 result to float in LDO model (mypy no-any-return)
- Use functools.partial instead of lambda in server (mypy misc)
2025-12-02 16:22:57 +00:00

223 lines
6.8 KiB
Python

"""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 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}"