# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Common python dataclasses for multiple backends."""
from __future__ import annotations
import warnings
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Literal,
Optional,
Tuple,
Type,
Union,
)
import matplotlib.pyplot as plt
import networkx as nx
from matplotlib.axes import Axes
from matplotlib.patches import Rectangle
from pydantic import Field, field_serializer, field_validator, model_validator
from quantify_scheduler.backends.qblox import constants
from quantify_scheduler.helpers.importers import export_python_object_to_path_string
from quantify_scheduler.schedules.schedule import Schedule
from quantify_scheduler.structure.model import (
DataStructure,
deserialize_function,
)
from quantify_scheduler.structure.types import Graph, NDArray
if TYPE_CHECKING:
from quantify_scheduler.backends.graph_compilation import SimpleNodeConfig
[docs]
LatencyCorrection = float
"""
Latency correction in seconds for a port-clock combination.
Positive values delay the operations on the corresponding port-clock combination,
while negative values shift the operation backwards in time with respect to other
operations in the schedule.
.. note::
If the port-clock combination of a signal is not specified in the corrections,
it is set to zero in compilation. The minimum correction over all port-clock
combinations is then subtracted to allow for negative latency corrections and to
ensure minimal wait time (see
:meth:`~quantify_scheduler.backends.corrections.determine_relative_latency_corrections`).
.. admonition:: Example
:class: dropdown
Let's say we have specified two latency corrections in the CompilationConfig:
.. code-block:: python
compilation_config.hardware_options.latency_corrections = {
"q0:res-q0.ro": LatencyCorrection(-20e-9),
"q0:mw-q0.01": LatencyCorrection(120e9),
}
In this case, all operations on port ``"q0:mw"`` and clock ``"q0.01"`` will
be delayed by 140 ns with respect to operations on port ``"q0:res"`` and
clock ``"q0.ro"``.
"""
[docs]
class SoftwareDistortionCorrection(DataStructure):
"""
Software distortion correction information for a port-clock combination.
.. admonition:: Example
:class: dropdown
.. jupyter-execute::
from quantify_scheduler.backends.types.common import (
SoftwareDistortionCorrection
)
distortion_corrections = {
"q0:fl-cl0.baseband": SoftwareDistortionCorrection(
filter_func="scipy.signal.lfilter",
input_var_name="x",
kwargs={
"b": [0, 0.25, 0.5],
"a": [1]
},
clipping_values=[-2.5, 2.5]
)
}
"""
"""The function applied to the waveforms."""
"""The argument to which the waveforms will be passed in the filter_func."""
[docs]
kwargs: Dict[str, Union[List, NDArray]]
"""The keyword arguments that are passed to the filter_func."""
[docs]
clipping_values: Optional[List] = None
"""
The optional boundaries to which the corrected pulses will be clipped,
upon exceeding."""
[docs]
sampling_rate: float = 1e9
"""The sample rate of the corrected pulse, in Hz."""
@field_validator("clipping_values")
[docs]
def _only_two_clipping_values(cls, clipping_values):
if clipping_values and len(clipping_values) != 2:
raise KeyError(
f"Clipping values should contain only two values, min and max.\n"
f"clipping_values: {clipping_values}"
)
return clipping_values
# @deprecated does not work with pydantic models.
[docs]
class DistortionCorrection(DataStructure):
"""
Software distortion correction information for a port-clock combination.
Note: deprecated and to be removed after 0.23.0. Please use
``SoftwareDistortionCorrection`` instead.
.. admonition:: Example
:class: dropdown
.. jupyter-execute::
from quantify_scheduler.backends.types.common import (
SoftwareDistortionCorrection
)
distortion_corrections = {
"q0:fl-cl0.baseband": SoftwareDistortionCorrection(
filter_func="scipy.signal.lfilter",
input_var_name="x",
kwargs={
"b": [0, 0.25, 0.5],
"a": [1]
},
clipping_values=[-2.5, 2.5]
)
}
"""
def __init__(self, **data) -> None:
super().__init__(**data)
warnings.warn(
"DistortionCorrection is deprecated and will be removed from "
"quantify-scheduler after 0.23.0. "
"Please use SoftwareDistortionCorrection instead.",
FutureWarning,
)
"""The function applied to the waveforms."""
"""The argument to which the waveforms will be passed in the filter_func."""
[docs]
kwargs: Dict[str, Union[List, NDArray]]
"""The keyword arguments that are passed to the filter_func."""
[docs]
clipping_values: Optional[List] = None
"""
The optional boundaries to which the corrected pulses will be clipped,
upon exceeding."""
[docs]
sampling_rate: float = 1e9
[docs]
class HardwareDistortionCorrection(DataStructure):
"""Parent class for hardware distortion correction."""
[docs]
class ModulationFrequencies(DataStructure):
"""
Modulation frequencies for a port-clock combination.
.. admonition:: Example
:class: dropdown
.. jupyter-execute::
from quantify_scheduler.backends.types.common import (
ModulationFrequencies
)
modulation_frequencies = {
"q0:res-q0.ro": ModulationFrequencies(
interm_freq=None,
lo_freq=6e9,
)
}
"""
[docs]
interm_freq: Optional[float] = None
"""The intermodulation frequency (IF) used for this port-clock combination."""
[docs]
lo_freq: Optional[float] = None
"""The local oscillator frequency (LO) used for this port-clock combination."""
[docs]
class MixerCorrections(DataStructure):
"""
Mixer corrections for a port-clock combination.
.. admonition:: Example
:class: dropdown
.. jupyter-execute::
from quantify_scheduler.backends.types.common import (
MixerCorrections
)
mixer_corrections = {
"q0:mw-q0.01": MixerCorrections(
dc_offset_i = -0.0542,
dc_offset_q = -0.0328,
amp_ratio = 0.95,
phase_error= 0.07,
)
}
"""
[docs]
dc_offset_i: float = 0.0
"""The DC offset on the I channel used for this port-clock combination."""
[docs]
dc_offset_q: float = 0.0
"""The DC offset on the Q channel used for this port-clock combination."""
"""The mixer gain ratio used for this port-clock combination."""
[docs]
phase_error: float = 0.0
"""The mixer phase error used for this port-clock combination."""
[docs]
class HardwareOptions(DataStructure):
"""
Datastructure containing the hardware options for each port-clock combination.
This datastructure contains the HardwareOptions that are currently shared among
the existing backends. Subclassing is required to add backend-specific options,
see e.g.,
:class:`~quantify_scheduler.backends.types.qblox.QbloxHardwareOptions`,
:class:`~quantify_scheduler.backends.types.zhinst.ZIHardwareOptions`.
"""
[docs]
latency_corrections: Optional[Dict[str, LatencyCorrection]] = None
"""
Dictionary containing the latency corrections (values) that should be applied
to operations on a certain port-clock combination (keys).
"""
[docs]
distortion_corrections: Optional[Dict[str, SoftwareDistortionCorrection]] = None
"""
Dictionary containing the distortion corrections (values) that should be applied
to waveforms on a certain port-clock combination (keys).
"""
[docs]
modulation_frequencies: Optional[Dict[str, ModulationFrequencies]] = None
"""
Dictionary containing the modulation frequencies (values) that should be used
for signals on a certain port-clock combination (keys).
"""
[docs]
mixer_corrections: Optional[Dict[str, MixerCorrections]] = None
"""
Dictionary containing the mixer corrections (values) that should be used
for signals on a certain port-clock combination (keys).
"""
[docs]
class LocalOscillatorDescription(DataStructure):
"""Information needed to specify a Local Oscillator in the :class:`~.CompilationConfig`."""
[docs]
instrument_type: Literal["LocalOscillator"]
"""The field discriminator for this HardwareDescription datastructure."""
[docs]
instrument_name: Optional[str] = None
"""The QCoDeS instrument name corresponding to this Local Oscillator."""
[docs]
generic_icc_name: Optional[str] = None
"""The name of the :class:`~.GenericInstrumentCoordinatorComponent` corresponding to this Local Oscillator."""
[docs]
frequency_param: str = "frequency"
"""The QCoDeS parameter that is used to set the LO frequency."""
[docs]
power_param: str = "power"
"""The QCoDeS parameter that is used to set the LO power."""
[docs]
power: Optional[int] = None
"""The power setting for this Local Oscillator."""
@field_validator("generic_icc_name")
[docs]
def _only_default_generic_icc_name(cls, generic_icc_name):
if (
generic_icc_name is not None
and generic_icc_name != constants.GENERIC_IC_COMPONENT_NAME
):
raise NotImplementedError(
f"Specified name '{generic_icc_name}' as a generic instrument "
f"coordinator component, but the Qblox backend currently only "
f"supports using the default name "
f"'{constants.GENERIC_IC_COMPONENT_NAME}'"
)
return generic_icc_name
[docs]
class IQMixerDescription(DataStructure):
"""Information needed to specify an IQ Mixer in the :class:`~.CompilationConfig`."""
[docs]
instrument_type: Literal["IQMixer"]
"""The field discriminator for this HardwareDescription datastructure."""
[docs]
class OpticalModulatorDescription(DataStructure):
"""Information needed to specify an optical modulator in the :class:`~.CompilationConfig`."""
[docs]
instrument_type: Literal["OpticalModulator"]
"""The field discriminator for this HardwareDescription datastructure."""
[docs]
class HardwareDescription(DataStructure):
"""
Specifies a piece of hardware and its instrument-specific settings.
Each supported instrument type should have its own datastructure that inherits from
this class. For examples, see :class:`~quantify_scheduler.backends.types.qblox.ClusterDescription`,
:class:`~quantify_scheduler.backends.types.zhinst.ZIHDAWG4Description`,
:class:`~.LocalOscillatorDescription`.
This datastructure is used to specify the control-hardware ports that
are included in the :class:`~.Connectivity` graph.
"""
"""The instrument type."""
[docs]
class Connectivity(DataStructure):
"""
Connectivity between ports on the quantum device and control hardware inputs/outputs.
The connectivity graph can be parsed from a list of edges, which are given by a
set of two strings that each correspond to an input/output on an instrument or a port
on the quantum device.
.. note::
To specify connections between more than one pair of ports at once, one can
also specify a list of ports within the edge input (see example below, and also
see :ref:`sec-connectivity-examples`).
The connectivity graph can be drawn using :meth:`~.draw`, which groups the nodes
according to the instrument name (specified by the string before the first ``"."``
in the node name; the name is omitted for the quantum device).
.. admonition:: Example
:class: dropdown
.. jupyter-execute::
from quantify_scheduler.backends.types.common import (
Connectivity
)
connectivity_dict = {
"graph": [
("awg0.channel_0", "q0:mw"),
("awg0.channel_1", "q1:mw"),
("rom0.channel_0", ["q0:res", "q1:res"]),
]
}
connectivity = Connectivity.model_validate(connectivity_dict)
connectivity.draw()
"""
"""
The connectivity graph consisting of i/o ports (nodes) on the quantum device and on
the control hardware, and their connections (edges).
"""
@field_validator("graph", mode="before")
[docs]
def draw(
self,
ax: Optional[Axes] = None,
figsize: Tuple[float, float] = (20, 10),
**options,
) -> Axes:
"""
Draw the connectivity graph using matplotlib.
The nodes are positioned using a multipartite layout, where the nodes
are grouped by instrument (identified by the first part of the node name).
Parameters
----------
ax
Matplotlib axis to plot the figure on.
figsize
Optional figure size, defaults to something slightly larger that fits the
size of the nodes.
options
optional keyword arguments that are passed to
:code:`networkx.draw_networkx`.
Returns
-------
:
Matplotlib axis on which the figure is plotted.
"""
if ax is None:
_, ax = plt.subplots(figsize=figsize)
options_dict = {
"font_size": 10,
"node_size": 1000,
"node_color": "white",
"edgecolors": "C0",
}
options_dict.update(options)
# Group nodes by instrument:
node_labels = {}
grouped_nodes = {}
for node in self.graph:
if "." in node:
node_instrument, node_port = node.split(sep=".", maxsplit=1)
self.graph.nodes[node]["subset"] = node_instrument
else:
node_instrument = ""
node_port = node
self.graph.nodes[node]["subset"] = "quantum_device"
if node_instrument not in grouped_nodes:
grouped_nodes[node_instrument] = []
grouped_nodes[node_instrument].append(node)
node_labels[node] = node_port
pos = nx.drawing.multipartite_layout(self.graph)
# Draw boxes around instrument ports:
for instrument, nodes in grouped_nodes.items():
min_node_pos_x = min(pos[node][0] for node in nodes)
max_node_pos_x = max(pos[node][0] for node in nodes)
min_node_pos_y = min(pos[node][1] for node in nodes)
max_node_pos_y = max(pos[node][1] for node in nodes)
instrument_anchor = (min_node_pos_x - 0.05, min_node_pos_y - 0.05) # type: ignore
instrument_width = max_node_pos_x - min_node_pos_x + 0.1 # type: ignore
instrument_height = max_node_pos_y - min_node_pos_y + 0.1 # type: ignore
ax.add_patch( # type: ignore
Rectangle(
xy=instrument_anchor,
width=instrument_width,
height=instrument_height,
fill=False,
color="b",
)
)
ax.text(x=min_node_pos_x, y=max_node_pos_y + 0.1, s=instrument, color="b") # type: ignore
nx.draw_networkx(self.graph, pos=pos, ax=ax, labels=node_labels, **options_dict)
ax.set_axis_off() # type: ignore
return ax # type: ignore
[docs]
class HardwareCompilationConfig(DataStructure):
"""
Information required to compile a schedule to the control-hardware layer.
From a point of view of :ref:`sec-compilation` this information is needed
to convert a schedule defined on a quantum-device layer to compiled instructions
that can be executed on the control hardware.
This datastructure defines the overall structure of a ``HardwareCompilationConfig``.
Specific hardware backends should customize fields within this structure by inheriting
from this class and specifying their own `"config_type"`, see e.g.,
:class:`~quantify_scheduler.backends.qblox_backend.QbloxHardwareCompilationConfig`,
:class:`~quantify_scheduler.backends.zhinst_backend.ZIHardwareCompilationConfig`.
"""
[docs]
config_type: Type[HardwareCompilationConfig] = Field(
default="quantify_scheduler.backends.types.common.HardwareCompilationConfig",
validate_default=True,
)
"""
A reference to the ``HardwareCompilationConfig`` DataStructure for the backend
that is used.
"""
[docs]
hardware_description: Dict[str, HardwareDescription]
"""
Datastructure describing the control hardware instruments in the setup and their
high-level settings.
"""
[docs]
hardware_options: HardwareOptions
"""
The :class:`~HardwareOptions` used in the compilation from the quantum-device layer to
the control-hardware layer.
"""
[docs]
connectivity: Union[
Connectivity, Dict
] # Dict for legacy support for the old hardware config
"""
Datastructure representing how ports on the quantum device are connected to ports
on the control hardware.
"""
[docs]
compilation_passes: List["SimpleNodeConfig"] = []
"""
The list of compilation nodes that should be called in succession to compile a
schedule to instructions for the control hardware.
"""
@field_serializer("config_type")
[docs]
def _serialize_backend_func(self, v):
return export_python_object_to_path_string(v)
@field_validator("config_type", mode="before")
[docs]
def _import_config_type_if_str(
cls, config_type: Type[HardwareCompilationConfig] # noqa: N805
) -> Callable[[Schedule, Any], Schedule]:
if isinstance(config_type, str):
return deserialize_function(config_type)
return config_type # type: ignore
@field_validator("connectivity")
[docs]
def _latencies_in_hardware_config(cls, connectivity): # noqa: N805
# if connectivity contains a hardware config with latency corrections
if isinstance(connectivity, Dict) and "latency_corrections" in connectivity:
warnings.warn(
"Latency corrections should be specified in the "
"`backends.types.common.HardwareOptions` instead of "
"the hardware configuration as of quantify-scheduler >= 0.19.0",
FutureWarning,
)
return connectivity
@field_validator("connectivity")
[docs]
def _distortions_in_hardware_config(cls, connectivity): # noqa: N805
# if connectivity contains a hardware config with distortion corrections
if isinstance(connectivity, Dict) and "distortion_corrections" in connectivity:
warnings.warn(
"Distortion corrections should be specified in the "
"`backends.types.common.HardwareOptions` instead of "
"the hardware configuration as of quantify-scheduler >= 0.19.0",
FutureWarning,
)
return connectivity
@model_validator(mode="after")
@model_validator(mode="after")
[docs]
def _connectivity_old_style_hw_cfg_empty_hw_options_and_descriptions(self):
if isinstance(self.connectivity, dict):
if self.hardware_description != {}:
raise ValueError(
"Hardware description must be empty when using old-style hardware config dictionary."
)
for _, hw_option in self.hardware_options:
if hw_option is not None:
raise ValueError(
"Hardware options must be empty when using old-style hardware config dictionary."
)
return self