Source code for quantify_scheduler.device_under_test.quantum_device

# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Module containing the QuantumDevice object."""
from __future__ import annotations

import json
from typing import Any

from qcodes.instrument.base import Instrument
from qcodes.instrument.parameter import InstrumentRefParameter, ManualParameter
from qcodes.utils import validators

from quantify_scheduler.backends.graph_compilation import (
    DeviceCompilationConfig,
    SerialCompilationConfig,
    SimpleNodeConfig,
)
from quantify_scheduler.backends.qblox_backend import QbloxHardwareCompilationConfig
from quantify_scheduler.backends.types.common import (
    HardwareCompilationConfig,
    HardwareOptions,
)
from quantify_scheduler.device_under_test.device_element import DeviceElement
from quantify_scheduler.device_under_test.edge import Edge
from quantify_scheduler.device_under_test.hardware_config import HardwareConfig
from quantify_scheduler.helpers.importers import (
    export_python_object_to_path_string,
    import_python_object_from_string,
)
from quantify_scheduler.json_utils import (
    JSONSerializableMixin,
    SchedulerJSONDecoder,
    SchedulerJSONEncoder,
)


[docs] class QuantumDevice(JSONSerializableMixin, Instrument): """ The QuantumDevice directly represents the device under test (DUT). This contains a description of the connectivity to the control hardware as well as parameters specifying quantities like cross talk, attenuation and calibrated cable-delays. The QuantumDevice also contains references to individual DeviceElements, representations of elements on a device (e.g, a transmon qubit) containing the (calibrated) control-pulse parameters. This object can be used to generate configuration files for the compilation step from the gate-level to the pulse level description. These configuration files should be compatible with the :meth:`~quantify_scheduler.backends.graph_compilation.QuantifyCompiler.compile` function. """ def __init__(self, name: str) -> None: super().__init__(name=name)
[docs] self.elements = ManualParameter( "elements", initial_value=list(), vals=validators.Lists(validators.Strings()), docstring="A list containing the names of all elements that" " are located on this QuantumDevice.", instrument=self, )
[docs] self.edges = ManualParameter( "edges", initial_value=list(), vals=validators.Lists(validators.Strings()), docstring="A list containing the names of all the edges which connect the" " DeviceElements within this QuantumDevice", instrument=self, )
[docs] self.instr_measurement_control = InstrumentRefParameter( "instr_measurement_control", docstring="A reference to the measurement control instrument.", vals=validators.MultiType(validators.Strings(), validators.Enum(None)), instrument=self, )
[docs] self.instr_instrument_coordinator = InstrumentRefParameter( "instr_instrument_coordinator", docstring="A reference to the instrument_coordinator instrument.", vals=validators.MultiType(validators.Strings(), validators.Enum(None)), instrument=self, )
[docs] self.cfg_sched_repetitions = ManualParameter( "cfg_sched_repetitions", initial_value=1024, docstring=( "The number of times execution of the schedule gets repeated when " "performing experiments, i.e. used to set the repetitions attribute of " "the Schedule objects generated." ), vals=validators.Ints(min_value=1), instrument=self, )
[docs] self.keep_original_schedule = ManualParameter( "keep_original_schedule", initial_value=True, docstring=( "If `True`, the compiler will not modify the schedule argument. " "If `False`, the compilation modifies the schedule, thereby " "making the original schedule unusable for further usage; this " "improves compilation time. Warning: if `False`, the returned schedule " "references objects from the original schedule, please refrain from modifying " "the original schedule after compilation in this case!" ), vals=validators.Bool(), instrument=self, )
[docs] self.hardware_config: HardwareConfig = HardwareConfig(instrument=self)
""" The input dictionary used to generate a valid HardwareCompilationConfig using :meth:`~.generate_hardware_compilation_config`. This configures the compilation from the quantum-device layer to the control-hardware layer. Useful methods to write and reload the configuration from a json file are :meth:`~.HardwareConfig.load_from_json_file` and :meth:`~.HardwareConfig.write_to_json_file`. """
[docs] self.scheduling_strategy = ManualParameter( "scheduling_strategy", docstring="Scheduling strategy used to calculate absolute timing.", vals=validators.Enum("asap", "alap"), initial_value="asap", )
# Store refs to prevent them from being garbage collected.
[docs] self._instrument_references = {}
def __getstate__(self) -> dict[str, Any]: # type: ignore """ Serializes :class:`~QuantumDevice` into a dict containing serialized :class:`~DeviceElement` and :class:`~Edge` objects plus ``cfg_sched_repetitions``. """ data: dict[str, Any] = {"name": self.name} data["elements"] = { element_name: json.dumps(self.get_element(element_name), cls=SchedulerJSONEncoder) for element_name in self.elements() } data["edges"] = { edge_name: json.dumps(self.get_edge(edge_name), cls=SchedulerJSONEncoder) for edge_name in self.edges() } data["cfg_sched_repetitions"] = str(self.cfg_sched_repetitions()) state = { "deserialization_type": export_python_object_to_path_string(self.__class__), "data": data, } return state def __setstate__(self, state: dict[str, Any]) -> None: """ Deserializes a dict of serialized :class:`~DeviceElement` and :class:`~Edge` objects into a `QuantumDevice`. """ self.__init__(state["data"]["name"]) for element_name, serialized_element in state["data"]["elements"].items(): self._instrument_references[element_name] = json.loads( serialized_element, cls=SchedulerJSONDecoder ) self.add_element(self._instrument_references[element_name]) for edge_name, serialized_edge in state["data"]["edges"].items(): self._instrument_references[edge_name] = json.loads( serialized_edge, cls=SchedulerJSONDecoder ) self.add_edge(self._instrument_references[edge_name]) self.cfg_sched_repetitions(int(state["data"]["cfg_sched_repetitions"]))
[docs] def to_json(self) -> str: """ Convert the :class:`~QuantumDevice` data structure to a JSON string. Overrides the base mixin method to perform additional checks. Returns ------- : The json string containing the serialized `QuantumDevice`. """ # Check whether there are closed instruments that prevent serialization. device_instruments = [] if hasattr(self, "elements"): device_instruments += self.elements() if hasattr(self, "edges"): device_instruments += self.edges() if not device_instruments: raise RuntimeError( f"Cannot serialize '{self.name}'. All attached instruments have been " f"closed and their information cannot be retrieved any longer." ) closed_instruments = [] for device_name in device_instruments: try: Instrument.find_instrument(device_name) except KeyError: closed_instruments.append(device_name) if closed_instruments: raise RuntimeError( f"Cannot serialize '{self.name}'. Instruments '{closed_instruments}' have " f"been closed and their information cannot be retrieved any longer. " f"If you do not wish to include these in the " f"serialization, please remove using `QuantumDevice.remove_element` or " f"`QuantumDevice.remove_edge`." ) # Let the JSON mixin handle serialization. return super().to_json()
[docs] def generate_compilation_config(self) -> SerialCompilationConfig: """Generate a config for use with a :class:`~.graph_compilation.QuantifyCompiler`.""" return SerialCompilationConfig( name="QuantumDevice-generated SerialCompilationConfig", keep_original_schedule=self.keep_original_schedule(), device_compilation_config=self.generate_device_config(), hardware_compilation_config=self.generate_hardware_compilation_config(), )
[docs] def generate_hardware_config(self) -> dict[str, Any]: """ Generate a valid hardware configuration describing the quantum device. Returns ------- The hardware configuration file used for compiling from the quantum-device layer to a hardware backend. .. warning: The config currently has to be specified by the user using the :code:`hardware_config` parameter. """ return self.hardware_config()
[docs] def generate_device_config(self) -> DeviceCompilationConfig: """ Generate a device config. This config is used to compile from the quantum-circuit to the quantum-device layer. """ clocks = {} elements_cfg = {} edges_cfg = {} # iterate over the elements on the device for element_name in self.elements(): element = self.get_element(element_name) element_cfg = element.generate_device_config() clocks.update(element_cfg.clocks) elements_cfg.update(element_cfg.elements) # iterate over the edges on the device for edge_name in self.edges(): edge = self.get_edge(edge_name) edge_cfg = edge.generate_edge_config() edges_cfg.update(edge_cfg) device_config = DeviceCompilationConfig( elements=elements_cfg, clocks=clocks, edges=edges_cfg, scheduling_strategy=self.scheduling_strategy(), ) return device_config
[docs] def generate_hardware_compilation_config(self) -> HardwareCompilationConfig | None: """ Generate a hardware compilation config. The compilation config is used to compile from the quantum-device to the control-hardware layer. """ hardware_config = self.hardware_config() if hardware_config is None: return None elif isinstance(hardware_config, HardwareCompilationConfig): # Hardware config is already a valid HardwareCompilationConfig DataStructure return hardware_config elif not any( [ key in hardware_config for key in [ "config_type", "hardware_description", "hardware_options", "connectivity", ] ] ): # Legacy support for the old hardware config dict: if ( hardware_config["backend"] == "quantify_scheduler.backends.qblox_backend.hardware_compile" ): hardware_compilation_config = QbloxHardwareCompilationConfig.model_validate( hardware_config ) elif ( hardware_config["backend"] == "quantify_scheduler.backends.zhinst_backend.compile_backend" ): compilation_passes = [ SimpleNodeConfig( name="zhinst_compile_backend", compilation_func=hardware_config["backend"], ), ] hardware_compilation_config = HardwareCompilationConfig( hardware_description={}, hardware_options=HardwareOptions(), connectivity=hardware_config, compilation_passes=compilation_passes, ) else: compilation_passes = [ SimpleNodeConfig( name="custom_hardware_backend", compilation_func=hardware_config["backend"], ), ] hardware_compilation_config = HardwareCompilationConfig( hardware_description={}, hardware_options=HardwareOptions(), connectivity=hardware_config, compilation_passes=compilation_passes, ) else: # Parse a (backend-specific) HardwareCompilationConfig if "backend" in hardware_config: raise ValueError( f"`{HardwareCompilationConfig.__name__}` no longer takes a" f" 'backend' field; instead, specify the 'config_type', which should" " contain a string reference to the backend-specific datastructure" " that should be parsed." ) hardware_compilation_config_model = import_python_object_from_string( hardware_config["config_type"] ) hardware_compilation_config = hardware_compilation_config_model.model_validate( hardware_config ) return hardware_compilation_config
[docs] def get_element(self, name: str) -> DeviceElement: """ Return a :class:`~quantify_scheduler.device_under_test.device_element.DeviceElement` by name. Parameters ---------- name The element name. Returns ------- : The element. Raises ------ KeyError If key ``name`` is not present in `self.elements`. """ if name in self.elements(): return self.find_instrument(name) # type: ignore raise KeyError(f"'{name}' is not an element of {self.name}.")
[docs] def add_element( self, element: DeviceElement, ) -> None: """ Add an element to the elements collection. Parameters ---------- element The element to add. Raises ------ ValueError If a element with a duplicated name is added to the collection. TypeError If :code:`element` is not an instance of the base element. """ if element.name in self.elements(): raise ValueError(f"'{element.name}' has already been added.") if not isinstance(element, DeviceElement): raise TypeError(f"{repr(element)} is not a DeviceElement.") self.elements().append(element.name) # list gets updated in place self._instrument_references[element.name] = element
[docs] def remove_element(self, name: str) -> None: """ Removes an element by name. Parameters ---------- name The element name. """ self.elements().remove(name) # list gets updated in place
[docs] def get_edge(self, name: str) -> Instrument: """ Returns an edge by name. Parameters ---------- name The edge name. Returns ------- : The edge. Raises ------ KeyError If key ``name`` is not present in ``self.edges``. """ if name in self.edges(): return self.find_instrument(name) raise KeyError(f"'{name}' is not an edge of {self.name}.")
[docs] def add_edge(self, edge: Edge) -> None: """ Add the edges. Parameters ---------- edge The edge name connecting the elements. Has to follow the convention 'element_0'-'element_1' """ if edge.name in self.edges(): raise ValueError(f"'{edge.name}' has already been added.") if not isinstance(edge, Edge): raise TypeError(f"{repr(edge)} is not an Edge.") self.edges().append(edge.name) self._instrument_references[edge.name] = edge
[docs] def remove_edge(self, edge_name: str) -> None: """ Remove an edge by name. Parameters ---------- edge_name The edge name. """ self.edges().remove(edge_name) # list gets updated in place