Overview#
quantify-scheduler
is a python module for writing quantum programs featuring a hybrid gate-pulse control model with explicit timing control.
It extends the circuit model from quantum information processing by adding a pulse-level representation to operations defined at the gate-level, and the ability to specify timing constraints between operations.
Thus, a user is able to mix gate- and pulse-level operations in a quantum circuit.
In quantify-scheduler
, both a quantum circuit consisting of gates and measurements and a timed sequence of control pulses are described as a Schedule
.
The Schedule
contains information on when operations should be performed.
When adding operations to a schedule, one does not need to specify how to represent this Operation
on all (both gate and pulse) abstraction levels.
Instead, this information can be added later during Compilation.
This allows the user to effortlessly mix the gate- and pulse-level descriptions as is required for many experiments.
We support similar flexibility in the timing constraints, one can either explicitly specify the timing using ScheduleBase.schedulables
, or rely on the compilation which will use the duration of operations to schedule them back-to-back.
Creating a schedule#
The most convenient way to interact with a Schedule
is through the quantify_scheduler
API.
In the following example, we will create a function to generate a Schedule
for a Bell experiment and visualize one instance of such a circuit.
# import the Schedule class and some basic operations.
from quantify_scheduler import Schedule
from quantify_scheduler.operations.gate_library import Reset, Measure, CZ, Rxy, X90
def bell_schedule(angles, q0:str, q1:str, repetitions: int):
for acq_index, angle in enumerate(angles):
sched = Schedule(f"Bell experiment on {q0}-{q1}")
sched.add(Reset(q0, q1)) # initialize the qubits
sched.add(X90(qubit=q0))
# Here we use a timing constraint to explicitly schedule the second gate to start
# simultaneously with the first gate.
sched.add(X90(qubit=q1), ref_pt="start", rel_time=0)
sched.add(CZ(qC=q0, qT=q1))
sched.add(Rxy(theta=angle, phi=0, qubit=q0) )
sched.add(Measure(q0, acq_index=acq_index)) # denote where to store the data
sched.add(Measure(q1, acq_index=acq_index), ref_pt="start")
return sched
sched = bell_schedule(
angles=[45.0],
q0="q0",
q1="q1",
repetitions=1024)
# visualize the circuit
f, ax = sched.plot_circuit_diagram()
Tip
Creating schedule generating functions is a convenient design pattern when creating measurement code. See the section on execution for an example of how this is used in practice.
Concepts and terminology#
quantify-scheduler
can be understood by understanding the following concepts.
Schedule
s describe when an operation needs to be applied.Operation
s describe what needs to be done.Resource
s describe where an operation should be applied.Compilation: between different abstraction layers for execution on physical hardware.
The following table shows an overview of the different concepts and how these are represented at the quantum-circuit layer and quantum-device layer.
Concept |
Quantum-circuit layer |
Quantum-device layer |
|
When |
– |
– |
|
What |
|||
Where |
Quantum-circuit layer#
The Quantum-circuit description is an idealized mathematical description of a schedule.
Gates and measurements#
In this description operations are
quantum gates that act on idealized qubits as part of a quantum circuit.
Operations can be represented by (idealized) unitaries acting on qubits.
The gate_library
contains common operations (including the measurement operation) described at the quantum-circuit level.
The Measure
is a special operation that represents a measurement on a qubit.
In addition to the qubit it acts on, one also needs to specify where to store the data.
Qubits#
At the gate-level description, operations are applied to qubits.
Qubits are represented by strings corresponding to the name of a qubit (e.g., q0
, q1
, A1
, QL
, qubit_1
, etc.).
Valid qubits are strings that appear in the device configuration file used when compiling the schedule.
Visualization#
A Schedule
containing operations can be visualized as a circuit diagram by calling its method plot_circuit_diagram()
.
Alternatively, one can plot the waveforms in schedules using plot_pulse_diagram()
, which requires that the absolute timing of the schedule has been determined (see Tutorial: Schedules and Pulses for an example).
from quantify_scheduler.operations.gate_library import X90, Measure
schedule = Schedule("X90 schedule")
schedule.add(X90("q0"))
schedule.add(Measure("q0"))
_ = schedule.plot_circuit_diagram()
Summary#
Gates are described by unitaries.
Gates are applied to qubits.
Measurements are applied to qubits.
Qubits are represented by strings.
Quantum-device layer#
The quantum-device layer describes waveforms and acquisition protocols applied to a device. These waveforms can be used to implement the idealized operations expressed on the quantum-circuit layer, or can be used without specifying a corresponding representation at the quantum-circuit layer:
from quantify_scheduler.operations.pulse_library import SquarePulse, RampPulse
schedule = Schedule("waveforms")
schedule.add(SquarePulse(amp=0.2, duration=4e-6, port="P"))
schedule.add(RampPulse(amp=-0.1, offset=.2, duration=6e-6, port="P"))
schedule.add(SquarePulse(amp=0.1, duration=4e-6, port="Q"), ref_pt='start')
{
'name': '23c4ef3c-5280-4d33-a275-c4f37db5d533',
'operation_id': '3458333132545197846',
'timing_constraints': [
{'rel_time': 0, 'ref_schedulable': None, 'ref_pt_new': None, 'ref_pt': 'start'}
],
'label': '23c4ef3c-5280-4d33-a275-c4f37db5d533'
}
Pulses and acquisition operations#
The pulse-level description typically contains parameterization information,
such as amplitudes, durations and so forth required to synthesize the waveform
on control hardware.
pulse_library
module contains
a collection of commonly used pulses.
Measurements are decomposed into pulses and acquisition operations.
Similarly to pulse operations, acquisition operations contain their timing information
and correspondent acquisition protocols.
acquisition_library
module contains
a collection of commonly used acquisition operations.
Ports and clocks#
To specify where an operation is applied, the quantum-device layer description needs to specify both the location in physical space as well as in frequency space.
For many systems, it is possible to associate a qubit with an element or location on a device that a signal can be applied to.
We call such a location on a device a port.
Like qubits, ports are represented as strings (e.g., P0
, feedline_in
, q0:mw_drive
, etc.).
In the last example, a port is associated with a qubit by including the qubit name at the beginning of the port name (separated by a colon :
).
Associating a qubit can be useful when visualizing a schedule and or keeping configuration files readable. It is, however, not required to associate a port with a single qubit. This keeps matters simple when ports are associated with multiple qubits or with non-qubit elements such as tunable couplers.
Besides the physical location on a device, a pulse is typically applied at a certain frequency and with a phase.
These two parameters are stored in a ClockResource
.
Each ClockResource
also has a name
to be easily identified.
The name
should identify the purpose of the clock resource, not the value of the frequency.
By storing the frequency and phase in a clock, we can adjust the frequency of a transition, but refer to it with the same name.
Similar to ports, clocks can be associated with qubits by including the qubit name in the clock name (again, this is not required). If the frequency of a clock is set to 0 (zero), the pulse is applied at baseband and is assumed to be real-valued.
Fig. 1 shows how the resources (qubit, port and clock) map to a physical device.
Acquisition protocols#
When we define an acquisition, we must specify how to acquire data and how to process it to provide a meaningful result. For example, typical readout of a superconducting qubit state will consist of the following steps:
Sending a spectroscopy pulse to a port of a device, that is connected to a readout resonator of the qubit.
Acquiring a raw voltage trace of the reflected or transmitted signal using microwave digitizer equipment.
Taking a weighted sum of the returned signal to obtain a single complex number.
Assign most likely readout outcome (0 or 1) based on a pre-calibrated threshold.
Description of these processing steps is called an acquisition protocol. To define an acquisition protocol, the developer must define two things: an algorithm to process the data and the schema of a data array returned by an instrument coordinator component (ICC). ICC’s job is to retrieve data from the instrument, process it and return it in a required format. If the readout equipment supports hardware acceleration for part of the processing steps, its ICC can and should utilize it.
The Acquisition data format chapter briefly describes the implication of
acquisition protocols and binning mode on a format of the data returned by ICC.
For a detailed description of all acquisition protocols defined in quantify-scheduler
and supported by at least some of the backends consult
the acquisition protocols reference section.
Acquisition channel and acquisition index#
quantify-scheduler
identifies each acquisition in a resulting dataset with
two integer numbers: acquisition channel and acquisition index.
Acquisition channel is a stream of acquisition data that corresponds to a single device element measured with the same acquisition protocol. Each acquisition within the same acquisition channel must return data of a uniform size and structure, that is described by the definition of an acquisition protocol. The order number of an acquisition within the acquisition channel during a schedule run is called an acquisition index.
On a quantum circuit layer they roughly correspond to a qubit being measured and
a number of the measurement of a given qubit in a schedule.
However, if a qubit (or, more precisely, a device element) can be measured using more
than one acquisition protocol, that will require defining several
acquisition channels for it.
When you are specifying a schedule on circuit level, acquisition channel is supposed
to be configured within a submodule of a device element, that corresponds to
a given measurement (i.e.
measure()
submodule of a
BasicTransmonElement
}).
Acquisition index should be specified at the instantiation of a measurement operation.
Summary#
Pulses are described as parameterized waveforms.
Pulses are applied to ports at a frequency specified by a clock.
Ports and clocks are represented by strings.
Acquisition protocols describe the processing steps to perform on an acquired signal in order to interpret it.
Acquisition channel and acquisition index describe how to find an acquisition result in a dataset.
Compilation#
Different compilation steps are required to go from a high-level description of a schedule to something that can be executed on hardware.
The scheduler supports multiple compilation steps, the most important ones are the step from the quantum-circuit layer (gates) to the quantum-device layer (pulses), and the one from the quantum-device layer into instructions suitable for execution on physical hardware.
The compilation is performed by a QuantifyCompiler
, which is configured through the CompilationConfig
:
- class CompilationConfig(/, **data: Any)[source]
Base class for a compilation config.
Subclassing is generally required to create useful compilation configs, here extra fields can be defined.
- backend: Type[QuantifyCompiler][source]
A reference string to the
QuantifyCompiler
class used in the compilation.
- device_compilation_config: DeviceCompilationConfig | None = None[source]
The
DeviceCompilationConfig
used in the compilation from the quantum-circuit layer to the quantum-device layer.
- hardware_compilation_config: quantify_scheduler.backends.types.common.HardwareCompilationConfig | None = None[source]
The
HardwareCompilationConfig
used in the compilation from the quantum-device layer to the control-hardware layer.
The compilers are described in detail in Compilers. This is schematically shown in Fig. 2.
In the first compilation step, pulse information is added to all operations that are not valid pulses (see Operation.valid_pulse
) based on the information specified in the Device compilation configuration.
A second compilation step takes the schedule at the pulse level and translates this for use on a hardware backend. This compilation step is performed using a hardware dependent compiler and uses the information specified in the Hardware compilation configuration.
Note
We use the term “device” to refer to the physical object(s) on the receiving end of the control pulses, e.g. a thin-film chip inside a dilution refrigerator.
And we employ the term “hardware” to refer to the instruments (electronics) that are involved in the pulse generations / signal digitization.
Device compilation configuration#
The device compilation configuration is used to compile from the quantum-circuit layer to the quantum-device layer.
This datastructure is auto-generated by the QuantumDevice
using the parameters stored in the DeviceElement
s.
- class DeviceCompilationConfig(/, **data: Any)[source]
Information required to compile a schedule to the quantum-device layer.
From a point of view of Compilation this information is needed to convert a schedule defined on a quantum-circuit layer to a schedule defined on a quantum-device layer.
Examples
The DeviceCompilationConfig is structured such that it should allow the specification of the circuit-to-device compilation for many different qubit platforms. Here we show a basic configuration for a two-transmon quantum device. In this example, the DeviceCompilationConfig is created by parsing a dictionary containing the relevant information.
Important
Although it is possible to manually create a configuration using dictionaries, this is not recommended. The
QuantumDevice
is responsible for managing and generating configuration files.import pprint from quantify_scheduler.backends.graph_compilation import ( DeviceCompilationConfig ) from quantify_scheduler.schemas.examples.device_example_cfgs import ( example_transmon_cfg ) pprint.pprint(example_transmon_cfg)
{'clocks': {'q0.01': 6020000000.0, 'q0.ro': 7040000000.0, 'q1.01': 5020000000.0, 'q1.ro': 6900000000.0}, 'compilation_passes': [{'compilation_func': 'quantify_scheduler.backends.circuit_to_device.compile_circuit_to_device_with_config_validation', 'name': 'circuit_to_device'}], 'edges': {'q0_q1': {'CZ': {'factory_func': 'quantify_scheduler.operations.pulse_factories.composite_square_pulse', 'factory_kwargs': {'square_amp': 0.5, 'square_clock': 'cl0.baseband', 'square_duration': 2e-08, 'square_port': 'q0:fl', 'virt_z_child_qubit_clock': 'q1.01', 'virt_z_child_qubit_phase': 63, 'virt_z_parent_qubit_clock': 'q0.01', 'virt_z_parent_qubit_phase': 44}}}}, 'elements': {'q0': {'H': {'factory_func': 'quantify_scheduler.operations.composite_factories.hadamard_as_y90z', 'factory_kwargs': {'qubit': 'q0'}}, 'Rxy': {'factory_func': 'quantify_scheduler.operations.pulse_factories.rxy_drag_pulse', 'factory_kwargs': {'amp180': 0.32, 'clock': 'q0.01', 'duration': 2e-08, 'motzoi': 0.45, 'port': 'q0:mw'}, 'gate_info_factory_kwargs': ['theta', 'phi']}, 'Rz': {'factory_func': 'quantify_scheduler.operations.pulse_factories.phase_shift', 'factory_kwargs': {'clock': 'q0.01'}, 'gate_info_factory_kwargs': ['theta']}, 'measure': {'factory_func': 'quantify_scheduler.operations.measurement_factories.dispersive_measurement_transmon', 'factory_kwargs': {'acq_channel': 0, 'acq_delay': 1.2e-07, 'acq_duration': 3e-07, 'acq_rotation': 0, 'acq_threshold': 0, 'clock': 'q0.ro', 'freq': None, 'port': 'q0:res', 'pulse_amp': 0.25, 'pulse_duration': 1.6e-07, 'pulse_type': 'SquarePulse'}, 'gate_info_factory_kwargs': ['acq_channel_override', 'acq_index', 'bin_mode', 'acq_protocol']}, 'reset': {'factory_func': 'quantify_scheduler.operations.pulse_library.IdlePulse', 'factory_kwargs': {'duration': 0.0002}}}, 'q1': {'Rxy': {'factory_func': 'quantify_scheduler.operations.pulse_factories.rxy_drag_pulse', 'factory_kwargs': {'amp180': 0.4, 'clock': 'q1.01', 'duration': 2e-08, 'motzoi': 0.25, 'port': 'q1:mw'}, 'gate_info_factory_kwargs': ['theta', 'phi']}, 'Rz': {'factory_func': 'quantify_scheduler.operations.pulse_factories.phase_shift', 'factory_kwargs': {'clock': 'q1.01'}, 'gate_info_factory_kwargs': ['theta']}, 'measure': {'factory_func': 'quantify_scheduler.operations.measurement_factories.dispersive_measurement_transmon', 'factory_kwargs': {'acq_channel': 1, 'acq_delay': 1.2e-07, 'acq_duration': 3e-07, 'acq_rotation': 0, 'acq_threshold': 0, 'clock': 'q1.ro', 'freq': None, 'port': 'q1:res', 'pulse_amp': 0.21, 'pulse_duration': 1.6e-07, 'pulse_type': 'SquarePulse'}, 'gate_info_factory_kwargs': ['acq_channel_override', 'acq_index', 'bin_mode', 'acq_protocol']}, 'reset': {'factory_func': 'quantify_scheduler.operations.pulse_library.IdlePulse', 'factory_kwargs': {'duration': 0.0002}}}}}
The dictionary can be parsed using the
model_validate
method.device_cfg = DeviceCompilationConfig.model_validate(example_transmon_cfg) device_cfg
DeviceCompilationConfig(clocks={'q0.01': 6020000000.0, 'q0.ro': 7040000000.0, 'q1.01': 5020000000.0, 'q1.ro': 6900000000.0}, elements={'q0': {'reset': OperationCompilationConfig(factory_func=<class 'quantify_scheduler.operations.pulse_library.IdlePulse'>, factory_kwargs={'duration': 0.0002}, gate_info_factory_kwargs=None), 'Rxy': OperationCompilationConfig(factory_func=<function rxy_drag_pulse at 0x7c46e5d83a60>, factory_kwargs={'amp180': 0.32, 'motzoi': 0.45, 'port': 'q0:mw', 'clock': 'q0.01', 'duration': 2e-08}, gate_info_factory_kwargs=['theta', 'phi']), 'Rz': OperationCompilationConfig(factory_func=<function phase_shift at 0x7c46e5d83f70>, factory_kwargs={'clock': 'q0.01'}, gate_info_factory_kwargs=['theta']), 'H': OperationCompilationConfig(factory_func=<function hadamard_as_y90z at 0x7c46e5b79ee0>, factory_kwargs={'qubit': 'q0'}, gate_info_factory_kwargs=None), 'measure': OperationCompilationConfig(factory_func=<function dispersive_measurement_transmon at 0x7c46e5b7b280>, factory_kwargs={'port': 'q0:res', 'clock': 'q0.ro', 'pulse_type': 'SquarePulse', 'pulse_amp': 0.25, 'pulse_duration': 1.6e-07, 'acq_delay': 1.2e-07, 'acq_duration': 3e-07, 'acq_channel': 0, 'acq_rotation': 0, 'acq_threshold': 0, 'freq': None}, gate_info_factory_kwargs=['acq_channel_override', 'acq_index', 'bin_mode', 'acq_protocol'])}, 'q1': {'reset': OperationCompilationConfig(factory_func=<class 'quantify_scheduler.operations.pulse_library.IdlePulse'>, factory_kwargs={'duration': 0.0002}, gate_info_factory_kwargs=None), 'Rxy': OperationCompilationConfig(factory_func=<function rxy_drag_pulse at 0x7c46e5d83a60>, factory_kwargs={'amp180': 0.4, 'motzoi': 0.25, 'port': 'q1:mw', 'clock': 'q1.01', 'duration': 2e-08}, gate_info_factory_kwargs=['theta', 'phi']), 'Rz': OperationCompilationConfig(factory_func=<function phase_shift at 0x7c46e5d83f70>, factory_kwargs={'clock': 'q1.01'}, gate_info_factory_kwargs=['theta']), 'measure': OperationCompilationConfig(factory_func=<function dispersive_measurement_transmon at 0x7c46e5b7b280>, factory_kwargs={'port': 'q1:res', 'clock': 'q1.ro', 'pulse_type': 'SquarePulse', 'pulse_amp': 0.21, 'pulse_duration': 1.6e-07, 'acq_delay': 1.2e-07, 'acq_duration': 3e-07, 'acq_channel': 1, 'acq_rotation': 0, 'acq_threshold': 0, 'freq': None}, gate_info_factory_kwargs=['acq_channel_override', 'acq_index', 'bin_mode', 'acq_protocol'])}}, edges={'q0_q1': {'CZ': OperationCompilationConfig(factory_func=<function composite_square_pulse at 0x7c46e5b799d0>, factory_kwargs={'square_port': 'q0:fl', 'square_clock': 'cl0.baseband', 'square_amp': 0.5, 'square_duration': 2e-08, 'virt_z_parent_qubit_phase': 44, 'virt_z_parent_qubit_clock': 'q0.01', 'virt_z_child_qubit_phase': 63, 'virt_z_child_qubit_clock': 'q1.01'}, gate_info_factory_kwargs=None)}}, scheduling_strategy='asap', compilation_passes=[SimpleNodeConfig(name='circuit_to_device', compilation_func=<function compile_circuit_to_device_with_config_validation at 0x7c46e5b7b700>)])
Hardware compilation configuration#
The hardware compilation configuration is used to compile from the quantum-device layer to the control-hardware layer.
Currently, this datastructure is parsed from a user-defined dict that should be passed to the quantum_device.hardware_config
parameter.
- class HardwareCompilationConfig(/, **data: Any)[source]
Information required to compile a schedule to the control-hardware layer.
From a point of view of 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.,QbloxHardwareCompilationConfig
,ZIHardwareCompilationConfig
.- _check_connectivity_graph_nodes_format()[source]
- _connectivity_old_style_hw_cfg_empty_hw_options_and_descriptions()[source]
- _distortions_in_hardware_config(connectivity)[source]
- _import_config_type_if_str(config_type: Type[HardwareCompilationConfig]) Callable[[quantify_scheduler.schedules.schedule.Schedule, Any], quantify_scheduler.schedules.schedule.Schedule] [source]
- _latencies_in_hardware_config(connectivity)[source]
- _serialize_backend_func(v)[source]
- compilation_passes: List[quantify_scheduler.backends.graph_compilation.SimpleNodeConfig] = [][source]
The list of compilation nodes that should be called in succession to compile a schedule to instructions for the control hardware.
- config_type: Type[HardwareCompilationConfig][source]
A reference to the
HardwareCompilationConfig
DataStructure for the backend that is used.
- connectivity: Connectivity | Dict[source]
Datastructure representing how ports on the quantum device are connected to ports on the control hardware.
- hardware_description: Dict[str, HardwareDescription][source]
Datastructure describing the control hardware instruments in the setup and their high-level settings.
- hardware_options: HardwareOptions[source]
The
HardwareOptions
used in the compilation from the quantum-device layer to the control-hardware layer.
Example Qblox hardware compilation configuration file
{
"config_type": "quantify_scheduler.backends.qblox_backend.QbloxHardwareCompilationConfig",
"hardware_description": {
"cluster0": {
"instrument_type": "Cluster",
"ref": "internal",
"modules": {
"1": {
"instrument_type": "QCM",
"complex_output_0": {
"marker_debug_mode_enable": true
}
},
"2": {
"instrument_type": "QCM_RF"
},
"3": {
"instrument_type": "QRM"
},
"4": {
"instrument_type": "QRM_RF"
},
"7": {
"instrument_type": "QCM"
},
"10": {
"instrument_type": "QCM"
},
"12": {
"instrument_type": "QCM"
}
}
},
"lo0": {
"instrument_type": "LocalOscillator",
"power": 1
},
"lo1": {
"instrument_type": "LocalOscillator",
"power": 1
},
"lo_real": {
"instrument_type": "LocalOscillator",
"power": 1
},
"iq_mixer_lo0": {
"instrument_type": "IQMixer"
},
"iq_mixer_lo1": {
"instrument_type": "IQMixer"
},
"iq_mixer_lo_real": {
"instrument_type": "IQMixer"
}
},
"hardware_options": {
"latency_corrections": {
"q4:mw-q4.01": 8e-9,
"q5:mw-q5.01": 4e-9
},
"distortion_corrections": {
"q0:fl-cl0.baseband": {
"filter_func": "scipy.signal.lfilter",
"input_var_name": "x",
"kwargs": {
"b": [
0,
0.25,
0.5
],
"a": [
1
]
},
"clipping_values": [
-2.5,
2.5
]
}
},
"modulation_frequencies": {
"q0:res-q0.ro": {
"interm_freq": null,
"lo_freq": 7.8e9
},
"q0:mw-q0.01": {
"interm_freq": 50e6,
"lo_freq": null
},
"q4:mw-q4.01": {
"interm_freq": 200e6,
"lo_freq": null
},
"q4:res-q4.ro": {
"interm_freq": null,
"lo_freq": 7.2e9
},
"q5:mw-q5.01": {
"interm_freq": 50e6,
"lo_freq": null
},
"q5:res-q5.ro": {
"interm_freq": 50e6
},
"q6:mw-q6.01": {
"lo_freq": 5e9
},
"q7:mw-q7.01": {
"lo_freq": 5e9
}
},
"mixer_corrections": {
"q4:mw-q4.01": {
"amp_ratio": 0.9999,
"phase_error": -4.2
},
"q4:res-q4.ro": {
"dc_offset_i": -0.054,
"dc_offset_q": -0.034,
"amp_ratio": 0.9997,
"phase_error": -4.0
}
},
"output_att": {
"q0:mw-q0.01": 4,
"q0:res-q0.ro": 12,
"q5:mw-q5.01": 4,
"q6:mw-q6.01": 6
},
"input_gain": {
"q4:res-q4.ro": {
"gain_I": 2,
"gain_Q": 3
}
},
"input_att": {
"q0:res-q0.ro": 4,
"q5:res-q5.ro": 10
},
"sequencer_options": {
"qe0:optical_readout-qe0.ge0": {
"ttl_acq_threshold": 0.5
}
}
},
"connectivity": {
"graph": [
[
"cluster0.module1.complex_output_0",
"iq_mixer_lo0.if"
],
[
"lo0.output",
"iq_mixer_lo0.lo"
],
[
"iq_mixer_lo0.rf",
"q4:mw"
],
[
"cluster0.module2.complex_output_0",
[
"q0:mw",
"q5:mw"
]
],
[
"cluster0.module2.complex_output_1",
"q6:mw"
],
[
"cluster0.module2.digital_output_0",
"q0:switch"
],
[
"cluster0.module3.complex_output_0",
"iq_mixer_lo1.if"
],
[
"lo1.output",
"iq_mixer_lo1.lo"
],
[
"iq_mixer_lo1.rf",
"q4:res"
],
[
"cluster0.module3.complex_input_0",
"qe0:optical_readout"
],
[
"cluster0.module4.complex_input_0",
"q5:res"
],
[
"cluster0.module4.complex_output_0",
"q0:res"
],
[
"cluster0.module7.real_output_0",
"iq_mixer_lo_real.if"
],
[
"lo_real.output",
"iq_mixer_lo_real.lo"
],
[
"iq_mixer_lo_real.rf",
"q7:mw"
],
[
"cluster0.module10.real_output_0",
"q0:fl"
],
[
"cluster0.module10.real_output_1",
"q1:fl"
],
[
"cluster0.module10.real_output_2",
"q2:fl"
],
[
"cluster0.module10.real_output_3",
"q3:fl"
],
[
"cluster0.module12.real_output_0",
"q4:fl"
]
]
}
}
Example Zurich Instruments hardware compilation configuration file
{
"config_type": "quantify_scheduler.backends.zhinst_backend.ZIHardwareCompilationConfig",
"hardware_description": {
"ic_hdawg0": {
"instrument_type": "HDAWG8",
"ref": "int",
"clock_select": 0,
"channelgrouping": 0,
"channel_0": {
"mode": "complex",
"markers": [
"AWG_MARKER1",
"AWG_MARKER2"
]
},
"channel_1": {
"mode": "complex",
"markers": [
"AWG_MARKER1",
"AWG_MARKER2"
]
},
"channel_2": {
"mode": "complex",
"markers": [
"AWG_MARKER1",
"AWG_MARKER2"
]
},
"channel_3": {
"mode": "complex",
"markers": [
"AWG_MARKER1",
"AWG_MARKER2"
]
}
},
"ic_uhfqa0": {
"instrument_type": "UHFQA",
"ref": "ext",
"channel_0": {
"mode": "real",
"trigger": 2
}
},
"lo0_ch1": {
"instrument_type": "LocalOscillator",
"instrument_name": "lo0",
"frequency_param": "ch1.frequency",
"power": 13
},
"lo0_ch2": {
"instrument_type": "LocalOscillator",
"instrument_name": "lo0",
"frequency_param": "ch2.frequency",
"power_param": "ch2.power",
"power": 10
},
"lo1": {
"instrument_type": "LocalOscillator",
"instrument_name": "lo1",
"frequency_param": "frequency",
"power": 16
},
"iqm_ic_hdawg0_ch0": {
"instrument_type": "IQMixer"
},
"iqm_ic_hdawg0_ch1": {
"instrument_type": "IQMixer"
},
"iqm_ic_hdawg0_ch2": {
"instrument_type": "IQMixer"
},
"iqm_ic_hdawg0_ch3": {
"instrument_type": "IQMixer"
},
"iqm_ic_uhfqa0_ch0": {
"instrument_type": "IQMixer"
}
},
"hardware_options": {
"latency_corrections": {
"q0:mw-q0.01": 95e-9,
"q1:mw-q1.01": 95e-9,
"q0:res-q0.ro": -95e-9,
"q1:res-q1.ro": -95e-9
},
"distortion_corrections": {
"q0:fl-cl0.baseband": {
"filter_func": "scipy.signal.lfilter",
"input_var_name": "x",
"kwargs": {
"b": [
0,
0.25,
0.5
],
"a": [
1
]
},
"clipping_values": [
-2.5,
2.5
]
}
},
"modulation_frequencies": {
"q0:mw-q0.01": {
"interm_freq": -1e8,
"lo_freq": null
},
"q0:res-q0.ro": {
"interm_freq": 2e8,
"lo_freq": null
},
"q1:mw-q1.01": {
"interm_freq": -1e8,
"lo_freq": null
},
"q2:mw-q2.01": {
"interm_freq": -1e8,
"lo_freq": null
},
"q3:mw-q3.01": {
"interm_freq": -1e8,
"lo_freq": null
}
},
"mixer_corrections": {
"q0:mw-q0.01": {
"amp_ratio": 0.95,
"phase_error": 0.07,
"dc_offset_i": -0.0542,
"dc_offset_q": -0.0328
},
"q1:mw-q1.01": {
"amp_ratio": 0.95,
"phase_error": 0.07,
"dc_offset_i": 0.042,
"dc_offset_q": 0.028
},
"q2:mw-q2.01": {
"amp_ratio": 0.95,
"phase_error": 0.07,
"dc_offset_i": 0.042,
"dc_offset_q": 0.028
},
"q3:mw-q3.01": {
"amp_ratio": 0.95,
"phase_error": 0.07,
"dc_offset_i": 0.042,
"dc_offset_q": 0.028
}
},
"output_gain": {
"q0:mw-q0.01": {
"gain_I": 1,
"gain_Q": 1
},
"q1:mw-q1.01": {
"gain_I": 1,
"gain_Q": 1
},
"q2:mw-q2.01": {
"gain_I": 1,
"gain_Q": 1
},
"q3:mw-q3.01": {
"gain_I": 1,
"gain_Q": 1
}
}
},
"connectivity": {
"graph": [
[
"ic_hdawg0.channel_0",
"iqm_ic_hdawg0_ch0.if"
],
[
"lo0_ch1.output",
"iqm_ic_hdawg0_ch0.lo"
],
[
"iqm_ic_hdawg0_ch0.rf",
"q0:mw"
],
[
"ic_hdawg0.channel_1",
"iqm_ic_hdawg0_ch1.if"
],
[
"lo0_ch2.output",
"iqm_ic_hdawg0_ch1.lo"
],
[
"iqm_ic_hdawg0_ch1.rf",
"q1:mw"
],
[
"ic_hdawg0.channel_2",
"iqm_ic_hdawg0_ch2.if"
],
[
"lo0_ch2.output",
"iqm_ic_hdawg0_ch2.lo"
],
[
"iqm_ic_hdawg0_ch2.rf",
"q2:mw"
],
[
"ic_hdawg0.channel_3",
"iqm_ic_hdawg0_ch3.if"
],
[
"lo0_ch2.output",
"iqm_ic_hdawg0_ch3.lo"
],
[
"iqm_ic_hdawg0_ch3.rf",
"q3:mw"
],
[
"ic_uhfqa0.channel_0",
"iqm_ic_uhfqa0_ch0.if"
],
[
"lo1.output",
"iqm_ic_uhfqa0_ch0.lo"
],
[
"iqm_ic_uhfqa0_ch0.rf",
"q0:res"
]
]
}
}
Hardware Description#
The HardwareDescription
datastructure specifies a control hardware instrument in the setup, along with its instrument-specific settings.
There is a specific HardwareDescription
datastructure for each of the currently supported instrument types, which are discriminated through the instrument_type
field.
- class HardwareDescription(/, **data: Any)[source]
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
ClusterDescription
,ZIHDAWG4Description
,LocalOscillatorDescription
.This datastructure is used to specify the control-hardware ports that are included in the
Connectivity
graph.
Hardware Options#
The HardwareOptions
datastructure contains the settings used in compiling from the quantum-device layer to a set of instructions for the control hardware. Most hardware options are structured as Dict[str, HardwareOption]
, where the keys are the port-clock combinations on which these settings should be applied.
- class HardwareOptions(/, **data: Any)[source]
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.,
QbloxHardwareOptions
,ZIHardwareOptions
.- distortion_corrections: Dict[str, SoftwareDistortionCorrection] | None = None[source]
Dictionary containing the distortion corrections (values) that should be applied to waveforms on a certain port-clock combination (keys).
- latency_corrections: Dict[str, LatencyCorrection] | None = None[source]
Dictionary containing the latency corrections (values) that should be applied to operations on a certain port-clock combination (keys).
- mixer_corrections: Dict[str, MixerCorrections] | None = None[source]
Dictionary containing the mixer corrections (values) that should be used for signals on a certain port-clock combination (keys).
- modulation_frequencies: Dict[str, ModulationFrequencies] | None = None[source]
Dictionary containing the modulation frequencies (values) that should be used for signals on a certain port-clock combination (keys).
Connectivity#
The Connectivity
datastructure describes how ports on the quantum device are connected to the control hardware. The connectivity is represented as an undirected graph, where the nodes are the ports on the quantum device and the control hardware inputs/outputs, and the edges are the connections between them.
- class Connectivity(/, **data: Any)[source]
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 Connectivity examples).
The connectivity graph can be drawn using
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).Example
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()
<Axes: >
- graph: quantify_scheduler.structure.types.Graph[source]
The connectivity graph consisting of i/o ports (nodes) on the quantum device and on the control hardware, and their connections (edges).
One-to-many, many-to-one, many-to-many
See Connectivity examples for different ways of specifying connectivity graphs.
This information is used in the compilation backends to assign the pulses and acquisitions in the schedule to ports on the control hardware. To achieve this, the nodes in the connectivity graph should be consistent with the inputs/outputs in the Hardware Description and the schedule (see the note below). Each backend has its own requirements on the connectivity graph, which are described in the documentation of the backends themselves (see the sections on Connectivity for Qblox and Zurich Instruments). For example, a backend can support adding one or more components (such as attenuators or IQ mixers) between a control-hardware output and a quantum-device port.
Important
Nodes that correspond to an input/output channel of an instrument should be named
"instrument_name.channel_name"
, where the instrument_name
should correspond
to a HardwareDescription
in
the HardwareCompilationConfig
.
Nodes that correspond to a port on the quantum device should be identical
to a port that is used in the Schedule
.
If you use gate-level operations, you should use:
"device_element_name:mw"
forRxy
operation (and its derived operations),"device_element_name:res"
for any measure operation,"device_element_name:fl"
for the flux port.
Execution#
Different kinds of instruments#
In order to execute a schedule, one needs both physical instruments to execute the compiled instructions as well as a way to manage the calibration parameters used to compile the schedule.
Although one could use manually written configuration files and send the compiled files directly to the hardware, the Quantify framework provides different kinds of Instrument
s to control the experiments and the management of the configuration files (Fig. 3).
Physical instruments#
QCoDeS instrument drivers are used to represent the physical hardware. For the purpose of quantify-scheduler, these instruments are treated as stateless, the desired configurations for an experiment being described by the compiled instructions. Because the instruments correspond to physical hardware, there is a significant overhead in querying and configuring these parameters. As such, the state of the instruments in the software is intended to track the state of the physical hardware to facilitate lazy configuration and logging purposes.
Hardware abstraction layer#
Because different physical instruments have different interfaces, a hardware abstraction layer serves to provide a uniform interface.
This hardware abstraction layer is implemented as the InstrumentCoordinator
to which individual InstrumentCoordinatorComponent
s are added that provide the uniform interface to the individual instruments.
The quantum device and the device elements#
The knowledge of the system is described by the QuantumDevice
and DeviceElement
s.
The QuantumDevice
directly represents the device under test (DUT) and 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 DeviceElement
s, representations of elements on a device (e.g, a transmon qubit) containing the (calibrated) control-pulse parameters.
Because the QuantumDevice
and the DeviceElement
s are an Instrument
, the parameters used to generate the configuration files can be easily managed and are stored in the snapshot containing the experiment’s metadata.
Experiment flow#
To use schedules in an experimental setting, in which the parameters used for compilation as well as the schedules themselves routinely change, we provide a framework for performing experiments making use of the concepts of quantify-core
.
Central in this framework are the schedule quantify_scheduler.gettables
that can be used by the quantify_core.measurement.control.MeasurementControl
and are responsible for the experiment flow.
This flow is schematically shown in Fig. 4.
Let us consider the example of an experiment used to measure the coherence time \(T_1\). In this experiment, a \(\pi\) pulse is used to excite the qubit, which is left to idle for a time \(\tau\) before it is measured. This experiment is then repeated for different \(\tau\) and averaged.
In terms of settables and gettables to use with the quantify_core.measurement.control.MeasurementControl
, the settable in this experiment is the delay time \(\tau\), and the gettable is the execution of the schedule.
We represent the settable as a qcodes.instrument.parameter.ManualParameter
:
from qcodes.instrument.parameter import ManualParameter
tau = ManualParameter("tau", label=r"Delay time", initial_value=0, unit="s")
To execute the schedule with the right parameters, the ScheduleGettable
needs to have a reference to a template function that generates the schedule, the appropriate keyword arguments for that function, and a reference to the QuantumDevice
to generate the required configuration files.
For the \(T_1\) experiment, quantify-scheduler provides a schedule generating function as part of the quantify_scheduler.schedules.timedomain_schedules
: the quantify_scheduler.schedules.timedomain_schedules.t1_sched()
.
from quantify_scheduler.schedules.timedomain_schedules import t1_sched
schedule_function = t1_sched
Inspecting the quantify_scheduler.schedules.timedomain_schedules.t1_sched()
, we find that we need to provide the times \(\tau\), the name of the qubit, and the number of times we want to repeat the schedule.
Rather than specifying the values of the delay times, we pass the parameter tau
.
qubit_name = "q0"
sched_kwargs = {
"times": tau,
"qubit": qubit_name,
"repetitions": 1024 # could also be a parameter
}
The ScheduleGettable
is set up to evaluate the value of these parameter on every call of ScheduleGettable.get
.
This flexibility allows the user to create template schedules that can then be measured by varying any of it’s input parameters using the quantify_core.measurement.control.MeasurementControl
.
Similar to how the schedule keyword arguments are evaluated for every call to ScheduleGettable.get
, the device config and hardware config files are re-generated from the QuantumDevice
for every iteration.
This ensures that if a calibration parameter is changed on the QuantumDevice
, the compilation will be affected as expected.
from quantify_scheduler.device_under_test.quantum_device import QuantumDevice
device = QuantumDevice(name="quantum_sample")
These ingredients can then be combined to perform the experiment:
from quantify_core.measurement import MeasurementControl
meas_ctrl = MeasurementControl("meas_ctrl")
t1_gettable = ScheduleGettable(
device=device,
schedule_function=schedule_function,
schedule_kwargs=sched_kwargs
)
meas_ctrl.settables(tau)
meas_ctrl.setpoints(times)
meas_ctrl.gettables(t1_gettable)
label = f"T1 experiment {qubit_name}"
dataset = meas_ctrl.run(label)
and the resulting dataset can be analyzed using
# from quantify_core.analysis.t1_analysis import T1Analysis
# analysis = T1Analysis(label=label).run()
Acquisition data format#
quantify-scheduler
has two primary interfaces for retrieving acquisition results: using
InstrumentCoordinator
and using quantify_scheduler.gettables.ScheduleGettable
.
Each acquisition and measurement operation in the schedule has associated
acquisition channel and acquisition index.
If you specifiy a schedule using raw acquisition operations (for example, using
SSBIntegrationComplex
),
use acq_channel
and acq_index
arguments of the operation to specify them.
Optionally you may also specify the requested binning mode in bin_mode
(it is AVERAGE
by default):
schedule.add(
SSBIntegrationComplex(
t0=0,
duration=100e-9,
port="q0:res",
clock="q0.ro",
acq_channel=3,
acq_index=1,
bin_mode=BinMode.AVERAGE
)
)
For a selected acquisition channel acquisition protocol and binning mode must be the same, otherwise compilation will fail.
When circuit-to-device compilation machinery is used, acq_channel
should be specified
in the
DeviceElement
being measured, for example in the
DispersiveMeasurement
submodule of
BasicTransmonElement
.
acq_index
and bin_mode
are still specified as input parameters to
Measure
(or another specialised
measurement operation supported by the device element).
Retrieve acquisitions through InstrumentCoordinator
#
InstrumentCoordinator.retrieve_acquisition()
method returns an xarray.Dataset
:
Each
xarray.DataArray
in the dataset corresponds to oneacq_channel
.Exact structure of a data array is defined by an acquisition protocol and
bin_mode
, that are associated with this acquisition channel.
For example, if a schedule contains two qubits (each one has its own acquisition channel,
say, 0
and 1
), the first of which has been measured three times and second twice using
SSBIntegrationComplex
acquisition protocol in BinMode.APPEND
,
the resulting dataset will have the form:
<xarray.Dataset> Size: 400B Dimensions: (repetition: 5, acq_index_0: 3, acq_index_1: 2) Dimensions without coordinates: repetition, acq_index_0, acq_index_1 Data variables: 0 (repetition, acq_index_0) complex128 240B (0.349+0.583j) ... (0.... 1 (repetition, acq_index_1) complex128 160B (0.347+0.938j) ... (0....
Definitions of acquisition protocols and correspondent data format can be found in the acquisition protocols reference section. Note that acquisition protocols define the meaning of each dimension of a data array, but do not guarantee a consistent naming of the dimensions in a dataset. Instead, the exact names of dimensions should be retrieved dynamically during the processing of the dataset.
Retrieve acquisition through ScheduleGettable
#
ScheduleGettable
proxies the instrument-coordinator format to a format that can be used with
MeasurementControl
.
Effectively it flattens the data arrays retrieved from the instrument coordinator,
splits complex numbers into either real and imaginary parts (if real_imag
is set to
True
) or absolute value and phase (if real_imag
is False
) and pads the data
with nan
s to fit it into a single array.
Data that corresponds to acquisition channel number \(n\) will end up in
items number \(2n\) and \(2n+1\) of that array.
For example, if real_imag
is set to True
in the ScheduleGettable
,
the dataset above will be converted to:
(
array([0.349, 0.039, 0.601, 0.732, 0.892, 0.673, 0.484, 0.836, 0.515,
0.42 , 0.291, 0.551, 0.017, 0.603, 0.349]),
array([0.583, 0.823, 0.608, 0.552, 0.218, 0.583, 0.177, 0.958, 0.622,
0.386, 0.548, 0.927, 0.023, 0.936, 0.004]),
array([0.347, 0.816, 0.607, 0.158, 0.709, 0.665, 0.775, 0.934, 0.895,
0.274]),
array([0.938, 0.482, 0.779, 0.532, 0.109, 0.274, 0.438, 0.805, 0.579,
0.997])
)
Footnotes