Source code for semi_cr.core.station.instruments.qcodes_proxy
import logging
from abc import ABC
from collections.abc import Mapping
from dataclasses import dataclass, field
from io import StringIO
from typing import Any
import qcodes
from qcodes import Instrument
from ruamel.yaml import YAML
from semi_cr.core.station.instruments import LabInstrument
type QCoDeSInstrumentSpec = tuple[str, Mapping[str, Any]]
[docs]
class QCoDeSStationSingleton:
"""We need a Station temporarily in order to construct the QCoDeS instruments from config. Maybe we could benefit
from having an actual persistent QCoDeS station on NQCPStation and using that. I'm concerned about separation of
concerns, so I'm opting for the smallest thing for now."""
_station: qcodes.Station | None = None
[docs]
@classmethod
def get_instance(cls) -> qcodes.Station:
if cls._station is None:
cls._station = qcodes.Station()
return cls._station
[docs]
@dataclass
class QCoDeSInstrumentProxy(LabInstrument, ABC):
"""Base class for static instruments backed by a live QCoDeS instrument. QCoDeS represents a connection to an
instrument as the _presence_ of a driver object. I.e. if the object exists, we may expect there to be a
connection (more or less)... We wrap that behavior by maintaining a copy of the specification from the config
files, and only inflating the actual QCoDeS instrument when it's actually needed."""
qcodes_config: QCoDeSInstrumentSpec = field(repr=False)
_qcodes_instrument: Instrument | None = field(default=None, init=False, repr=False)
[docs]
def connect(self) -> None:
"""In QCoDeS, connection means instantiating the driver with the configured instrument."""
if self._qcodes_instrument is not None:
return
name, config = self.qcodes_config
stream = StringIO()
YAML().dump(
{
"instruments": {
name: config,
}
},
stream,
)
station = QCoDeSStationSingleton.get_instance()
station.load_config(stream.getvalue())
try:
self._qcodes_instrument = station.load_instrument(
name,
update_snapshot=False,
)
except Exception:
# Some drivers register their QCoDeS instance before their
# constructor finishes. Remove that partial instance so Retry can
# use the same configured name.
try:
components = getattr(station, "components", {})
partial = components.get(name) if isinstance(components, Mapping) else None
if partial is not None:
station.remove_component(name)
partial.close()
elif Instrument.exist(name):
Instrument.find_instrument(name).close()
except Exception:
logging.exception("Could not clean up partially attached instrument %s", name)
raise
[docs]
def is_connected(self):
return self._qcodes_instrument is not None
[docs]
def disconnect(self):
if self._qcodes_instrument is not None:
instrument = self._qcodes_instrument
try:
instrument.close()
finally:
self._qcodes_instrument = None
station = QCoDeSStationSingleton.get_instance()
components = getattr(station, "components", {})
if isinstance(components, Mapping) and components.get(self.name) is instrument:
station.remove_component(self.name)