Source code for semi_cr.core.station.instruments.dmm

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from semi_cr.core.lab.station.graphing.ids import terminal_id
from semi_cr.core.station.graph import GraphEdge, GraphNode
from semi_cr.core.station.instruments.qcodes_proxy import QCoDeSInstrumentProxy, QCoDeSInstrumentSpec

if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping


[docs] @dataclass class DMMVoltageTerminal: """A station-level, read-only voltage input exposed by a QCoDeS DMM.""" parent_ref: DMMBlueprint name: str role: str | None = None @property def dmm(self) -> DMMBlueprint: return self.parent_ref @property def node_id(self) -> str: return terminal_id(self.dmm.name, self.name)
[docs] def graph_nodes(self) -> Iterable[GraphNode]: yield GraphNode( id=self.node_id, obj=self, kind="terminal", attrs={ "instrument": self.dmm.name, "terminal": self.name, "role": self.role, "quantity": "voltage", "modality": "dc", }, )
[docs] def graph_edges(self) -> Iterable[GraphEdge]: return ()
[docs] def read_voltage(self) -> float: """Read the selected terminal through the connected live QCoDeS DMM.""" instrument = self.dmm._qcodes_instrument if instrument is None: raise RuntimeError(f"{self.dmm.name} is not connected; call connect() first") terminal = getattr(instrument, self.name, None) if terminal is None: return self.dmm.read_voltage() for parameter_name in ("voltage", "volt"): parameter = getattr(terminal, parameter_name, None) if callable(parameter): return float(parameter()) raise RuntimeError( f"{self.dmm.name} terminal {self.name!r} exposes neither a voltage nor volt parameter" )
[docs] @dataclass class DMMBlueprint(QCoDeSInstrumentProxy): """Static station blueprint for a routable voltage-measuring DMM.""" name: str inputs: list[DMMVoltageTerminal] connections: dict[str, list[str]] @property def address(self) -> str: return str(self.qcodes_config[1].get("init", {}).get("address", ""))
[docs] def read_voltage(self) -> float: """Read the selected physical DMM through its live QCoDeS instrument.""" if self._qcodes_instrument is None: raise RuntimeError(f"{self.name} is not connected; call connect() first") return float(self._qcodes_instrument.volt())
[docs] @classmethod def from_instrument_spec(cls, spec: QCoDeSInstrumentSpec) -> DMMBlueprint: name, config = spec init: Mapping[str, Any] = config.get("init", {}) blueprint = cls(name=name, qcodes_config=spec, inputs=[], connections={}) connections = init.get("connections", []) blueprint.connections = { connection["name"]: list(connection.get("endpoints", [])) for connection in connections } blueprint.inputs = [ DMMVoltageTerminal( parent_ref=blueprint, name=connection["name"], role=connection.get("role", "meter"), ) for connection in connections ] return blueprint
[docs] def graph_nodes(self) -> Iterable[GraphNode]: for input_terminal in self.inputs: yield from input_terminal.graph_nodes()
[docs] def graph_edges(self) -> Iterable[GraphEdge]: for input_terminal in self.inputs: yield from input_terminal.graph_edges() local = input_terminal.node_id for endpoint in self.connections.get(input_terminal.name, []): yield GraphEdge( id=f"{endpoint}->{local}", source=endpoint, target=local, kind="physical", )