- 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)
223 lines
6.8 KiB
Python
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}"
|