Source code for qblox_instruments.qcodes_drivers.cluster

# --------------------------------------------------------------------------
# Description    : Cluster QCoDeS interface
# Git repository : https://gitlab.com/qblox/packages/software/qblox_instruments.git
# Copyright (C) Qblox BV (2020)
# --------------------------------------------------------------------------


# -- include -----------------------------------------------------------------

import warnings
from collections.abc import Sequence
from functools import partial
from typing import Any, Callable, Optional, Union

from qcodes import Instrument, InstrumentChannel, Parameter
from qcodes import validators as vals

from qblox_instruments import DeviceInfo, SystemStatusFlags
from qblox_instruments.native import Cluster as ClusterNative
from qblox_instruments.qcodes_drivers.module import Module, get_item
from qblox_instruments.qcodes_drivers.time import Time
from qblox_instruments.types import DebugLevel

# -- class -------------------------------------------------------------------


[docs] class Cluster(ClusterNative, Instrument): """ This class connects `QCoDeS <https://microsoft.github.io/Qcodes/>`_ to the Cluster native interface. """ # ------------------------------------------------------------------------
[docs] def __init__( self, name: str, identifier: Optional[str] = None, port: Optional[int] = None, debug: Optional[DebugLevel] = None, dummy_cfg: Optional[dict] = None, ) -> None: """ Creates Cluster QCoDeS class and adds all relevant instrument parameters. These instrument parameters call the associated methods provided by the native interface. Parameters ---------- name : str Instrument name. identifier : Optional[str] Instrument identifier. See :func:`~qblox_instruments.resolve()`. If None, the instrument is identified by name. port : Optional[int] Override for the TCP port through which we should connect. debug : Optional[DebugLevel] Debug level. See :class:`~qblox_instruments.types.DebugLevel` for more information. By default None, which means that for a connection to a dummy cluster, `DebugLevel.ERROR_CHECK` will be used, and for a real cluster, `DebugLevel.VERSION_CHECK`. dummy_cfg : Optional[dict] Configure as dummy using this configuration. For each slot that needs to be occupied by a module add the slot index as key and specify the type of module in the slot using the type :class:`~qblox_instruments.ClusterType`. """ # For backwards compatibility: a boolean used to work, where True was # interpreted as "no version check" and False as the default value. if isinstance(debug, bool): warnings.warn( "Passing a boolean argument for the `debug` parameter is deprecated and could " "cause unexpected behaviour in a future version.", FutureWarning, ) debug = DebugLevel.ERROR_CHECK if debug else None # Initialize parent classes. if identifier is None: identifier = name super().__init__(identifier, port, debug, dummy_cfg) Instrument.__init__(self, name) # Check for any errors that occurred during initialization status = self.get_system_status() if status.status == "ERROR": for slot_idx, slot_err in enumerate(status.slot_flags): if slot_err and any( err == SystemStatusFlags.MODULE_FIRM_OR_HARDWARE_INCOMPATIBLE for err in slot_err ): if self._debug in (DebugLevel.ERROR_CHECK, DebugLevel.NO_CHECK): warnings.warn( f"Received a module incompatibility error in slot " f"{slot_idx + 1}: \n -> {status}" ) else: raise ConnectionError( f"Received the following Error Status in slot " f"{slot_idx + 1}: \n -> {status}" ) # Set number of slots self._num_slots = 20 # Add QCoDeS parameters self.add_parameter( "led_brightness", label="LED brightness", docstring="Sets/gets frontpanel LED brightness.", unit="", vals=vals.Strings(), val_mapping={ "off": "OFF", "low": "LOW", "medium": "MEDIUM", "high": "HIGH", }, set_parser=str, get_parser=str, set_cmd=self._set_led_brightness, get_cmd=self._get_led_brightness, ) self.add_parameter( "reference_source", label="Reference source.", docstring="Sets/gets reference source ('internal' = internal " "10 MHz, 'external' = external 10 MHz).", unit="", vals=vals.Bool(), val_mapping={"internal": True, "external": False}, set_parser=bool, get_parser=bool, set_cmd=self._set_reference_source, get_cmd=self._get_reference_source, ) self.add_parameter( "ext_trigger_input_delay", label="Trigger input delay.", docstring="Sets/gets the delay of the external input trigger in picoseconds.", unit="ps", vals=vals.Multiples(39, min_value=0, max_value=31 * 39), set_parser=int, get_parser=int, set_cmd=self.set_trg_in_delay, get_cmd=self.get_trg_in_delay, ) self.add_parameter( "ext_trigger_input_trigger_en", label="Trigger input enable.", docstring="Enable/disable the external input trigger.", unit="", vals=vals.Bool(), set_parser=bool, get_parser=bool, set_cmd=self.set_trg_in_map_en, get_cmd=self.get_trg_in_map_en, ) self.add_parameter( "ext_trigger_input_trigger_address", label="Trigger address.", docstring="Sets/gets the external input trigger address to which " "the input trigger is mapped to the trigger network (T1 to " "T15).", unit="", vals=vals.Numbers(1, 15), set_parser=int, get_parser=int, set_cmd=self.set_trg_in_map_addr, get_cmd=self.get_trg_in_map_addr, ) for x in range(1, 16): self.add_parameter( f"trigger{x}_monitor_count", label=f"Trigger monitor count for trigger address T{x}.", docstring=f"Gets the trigger monitor count from trigger address T{x}.", unit="", get_cmd=partial(self.get_trigger_monitor_count, int(x)), ) self.add_parameter( "trigger_monitor_latest", label="Latest monitor trigger for trigger address.", docstring="Gets the trigger address which was triggered last (T1 to T15).", unit="", get_cmd=self.get_trigger_monitor_latest, ) self.__add_modules()
# ------------------------------------------------------------------------ def __reinitialize_modules(self) -> None: """ Reinitialize modules based on the physical state of the slots. """ slot_info = self.get_json_description().get("modules", {}) # Iterate over slot information to update or add modules for slot_str, info in slot_info.items(): slot_id = int(slot_str) # Skip if serial matches if ( slot_id in self._mod_handles and DeviceInfo.from_dict(info).serial == self._mod_handles[slot_id]["serial"] ): continue # Recreate module handle and add module self._create_mod_handles(slot_id) self.__add_modules(slot_id) # Remove entries if their slot_id is not in slot_info for slot_id in list(self._mod_handles.keys()): if str(slot_id) not in slot_info: del self._mod_handles[slot_id] # Remove the module and add it again self.__add_modules(slot_id) # ------------------------------------------------------------------------ def __add_modules(self, slot: Optional[int] = None) -> None: """ Create and add modules. Parameters ---------- slot : int, optional Specific slot number to add the module for. If None, adds modules for all slots. """ if slot is not None: # Add the specific module for the provided slot del self.submodules[f"module{slot}"] module = Module(self, f"module{slot}", slot) self.add_submodule(f"module{slot}", module) else: self.submodules.clear() # Add modules for all slots for slot_idx in range(1, self._num_slots + 1): module = Module(self, f"module{slot_idx}", slot_idx) self.add_submodule(f"module{slot_idx}", module) # Add time-keeping functionality if "time" not in self.submodules: time = Time(self) self.add_submodule("time", time) # ------------------------------------------------------------------------ @property def modules(self) -> list: """ Get list of modules. Returns ---------- list List of modules. """ modules_list = [ submodule for submodule in self.submodules.values() if "module" in str(submodule) ] return modules_list # ------------------------------------------------------------------------ @property def times(self) -> list: """ Get list of time blocks. Returns ---------- list List of digital time modules. There is only one, but we still need to be able to iterate through it as if it was a list to not break pytest. """ time_list = [ submodule for submodule in self.submodules.values() if "time" in str(submodule) ] return time_list # -------------------------------------------------------------------------
[docs] def get_connected_modules( self, filter_fn: Optional[Callable[[Module], bool]] = None ) -> dict[int, Module]: """ Get the currently connected modules for each occupied slot in the Cluster. A selection of modules can be made by passing a filter function. For example: .. code-block:: python cluster.get_connected_modules( filter_fn = lambda mod: mod.is_qrm_type and not mod.is_rf_type ) Parameters ---------- filter_fn Optional filter function that must return True for the modules that should be included in the return value, and False otherwise. Returns ------- dict Dictionary with key-value pairs consisting of slot numbers and corresponding :class:`~.Module` objects. Only contains entries for modules that are present and match the `filter_fn`. """ def checked_filter_fn(mod) -> bool: if filter_fn is not None: return filter_fn(mod) return True return { mod.slot_idx: mod for mod in self.modules if mod.present() and mod.connected() and checked_filter_fn(mod) }
# ------------------------------------------------------------------------
[docs] def toggle_all_lo(self, enable: bool) -> None: """ Turn ON or OFF all local oscillators present on all modules . Parameters ---------- enable : bool Turn ON if True, OFF if False. """ rf_modules = self.get_connected_modules( filter_fn=lambda mod: (mod.is_qcm_type or mod.is_qrm_type) and mod.is_rf_type ) for module in rf_modules.values(): module.toggle_all_lo(enable)
# ------------------------------------------------------------------------
[docs] def reset(self) -> None: """ Resets device, invalidates QCoDeS parameter cache and clears all status and event registers (see `SCPI <https://www.ivifoundation.org/downloads/SCPI/scpi-99.pdf>`_). """ self._reset() # Reinitialize modules self.__reinitialize_modules() # Invalidate the QCoDeS cache self._invalidate_qcodes_parameter_cache()
# ------------------------------------------------------------------------
[docs] def disconnect_outputs(self, slot: int) -> None: """ Disconnects all outputs from the waveform generator paths of the sequencers. Parameters ---------- slot: int Slot index """ self._disconnect_outputs(slot) self._invalidate_qcodes_parameter_cache(slot)
# ------------------------------------------------------------------------
[docs] def disconnect_inputs(self, slot: int) -> None: """ Disconnects all inputs from the acquisition paths of the sequencers. Parameters ---------- slot: int Slot index """ self._disconnect_inputs(slot) self._invalidate_qcodes_parameter_cache(slot)
# ------------------------------------------------------------------------
[docs] def connect_sequencer(self, slot: int, sequencer: int, *connections: str) -> None: """ Makes new connections between the indexed sequencer and some inputs and/or outputs. This will fail if a requested connection already existed, or if the connection could not be made due to a conflict with an existing connection (hardware constraints). In such a case, the channel map will not be affected. Parameters ---------- slot: int Slot index sequencer : int Sequencer index *connections : str Zero or more connections to make, each specified using a string. The string should have the format `<direction><channel>` or `<direction><I-channel>_<Q-channel>`. `<direction>` must be `in` to make a connection between an input and the acquisition path, `out` to make a connection from the waveform generator to an output, or `io` to do both. The channels must be integer channel indices. If only one channel is specified, the sequencer operates in real mode; if two channels are specified, it operates in complex mode. Raises ---------- RuntimeError If the connection command could not be completed due to a conflict. ValueError If parsing of a connection fails. """ self._sequencer_connect(slot, sequencer, *connections) self._invalidate_qcodes_parameter_cache(slot, sequencer)
[docs] def snapshot_base( self, update: Optional[bool] = False, params_to_skip_update: Optional[Sequence[str]] = None, ) -> dict[Any, Any]: """ Override the cluster snapshot method to prevent it from sending unnecessary commands to each module when polling its `connected` and `present` statuses. We do this by first fetching all the slot numbers with a module connected/present, then manually setting their QCoDeS parameter cache so their respective getter function (which sends a SCPI command) will not be invoked. See :meth:`InstrumentBase.snapshot_base` for the function signature. """ slots_with_module_connected = self._get_slots_with_module_connected() slots_with_module_present = self._get_slots_with_module_present() for module in self.modules: module.parameters["connected"].cache.set(module.slot_idx in slots_with_module_connected) module.parameters["present"].cache.set(module.slot_idx in slots_with_module_present) params_to_skip_update = set(params_to_skip_update or []) | {"connected", "present"} return super().snapshot_base(update, params_to_skip_update) # type: ignore
# ------------------------------------------------------------------------ def _invalidate_qcodes_parameter_cache( self, slot: Optional[int] = None, sequencer: Optional[int] = None ) -> None: """ Marks the cache of all QCoDeS parameters in the module, including in any sequencers the module might have, as invalid. Optionally, a slot and a sequencer can be specified. This will invalidate the cache of that slot or sequencer in that specific slot only instead of all parameters. Parameters ---------- slot : Optional[int] Slot index of slot for which to invalidate the QCoDeS parameters. sequencer : Optional[int] Sequencer index of sequencer for which to invalidate the QCoDeS parameters. """ # Invalidate instrument parameters if slot is None: for param in self.parameters.values(): param.cache.invalidate() module_list = self.modules else: module_list = [self.modules[slot - 1]] # Invalidate module parameters for module in module_list: module._invalidate_qcodes_parameter_cache(sequencer) # Invalidate time-keeping parameters for tim in self.times: tim._invalidate_qcodes_parameter_cache() # ------------------------------------------------------------------------ def __getitem__(self, key: str) -> Union[InstrumentChannel, Parameter, Callable[..., Any]]: """ Get module or parameter using string based lookup. Parameters ---------- key : str Module, parameter or function to retrieve. Returns ---------- Union[InstrumentChannel, Parameter, Callable[..., Any]] Module, parameter or function. Raises ---------- KeyError Module, parameter or function does not exist. """ return get_item(self, key) # ------------------------------------------------------------------------ def __repr__(self) -> str: """ Returns simplified representation of class giving just the class, name and connection. Returns ---------- str String representation of class. """ loc_str = "" if hasattr(self._transport, "_socket"): address, port = self._transport._socket.getpeername() loc_str = f" at {address}:{port}" return f"<{type(self).__name__}: {self.name}" + loc_str + ">"