Hardware backends#
The compiler for a hardware backend for Quantify takes a Schedule
defined on the Quantum-device layer and a HardwareCompilationConfig
. The InstrumentCoordinatorComponent
for the hardware runs the operations within the schedule and can return the acquired data via the InstrumentCoordinator
method retrieve_acquisition()
. To that end, it generally consists of the following components:
CompilationNode
s that generate the compiledSchedule
from the device-levelSchedule
and aHardwareCompilationConfig
. This compiledSchedule
generally consists of (1) the hardware level instructions that should be executed and (2) instrument settings that should be applied on the hardware.A backend-specific
HardwareCompilationConfig
that contains the information that is used to compile from the quantum-device layer to the control-hardware layer (see Compilation). This generally consists of:A
HardwareDescription
(e.g., the available channels, IP addresses, etc.).The
Connectivity
between the quantum-device ports and the control-hardware ports.The
HardwareOptions
that includes the specific settings that are supported by the backend (e.g., the gain on an output port).A list of
SimpleNodeConfig
s, which specifies theCompilationNode
s that should be executed to compile theSchedule
to the hardware-specific instructions.
InstrumentCoordinatorComponent
s that send compiled instructions to the instruments, retrieve data, and convert the acquired data into a standardized, backend independent dataset (see Acquisition protocols).
Architecture overview#
The interfaces between Quantify and a hardware backend are illustrated in the following diagram:
Experiment flow#
See also
This diagram is similar to the the one in the Experiment flow section in the User Guide, but provides more details on the interfaces between the objects.
In the above diagram several methods are called. The get()
method of the ScheduleGettable
executes the sequence and returns the data from the acquisitions in the Schedule
. The generate_compilation_config()
method of the QuantumDevice
generates a CompilationConfig
that is used to compile the Schedule
within the ScheduleGettable
. The compile()
method of the QuantifyCompiler
compiles the Schedule
to the hardware-specific instructions. The prepare()
, start()
, and retrieve_acquisition()
methods of the InstrumentCoordinator
are used to prepare the hardware, start the acquisition, and retrieve the acquired data, respectively.
Developing a new backend#
To develop a new backend, the following approach is advised:
Implement a custom
Gettable
that takes a set of instructions in the form that the hardware can directly execute. The compiled schedule that the backend should return once developed should therefore be the same as the instructions that thisGettable
accepts. This enables testing of theInstrumentCoordinatorComponent
s without having to worry about the compilation of theSchedule
to the hardware.Implement
InstrumentCoordinatorComponent
s for each of the instruments (starting with the instrument with the acquisition channel to enable testing).Implement
CompilationNode
s to generate hardware instructions from aSchedule
and aCompilationConfig
.Integrate the (previously developed + tested)
QuantifyCompiler
andInstrumentCoordinatorComponent
s with theScheduleGettable
.Optionally test the
ScheduleGettable
with theMeasurementControl
.
Mock device#
To illustrate how to do this, let’s consider a basic example of an interface with a mock hardware device for which we would like to create a corresponding Quantify hardware backend. Our mock device resembles a readout module that can play and simultaneously acquire waveforms through the “TRACE” instruction. It is described by the following class:
- class MockReadoutModule(name: str, sampling_rate: float = 1000000000.0, gain: float = 1.0)[source]
Mock readout module that just supports “TRACE” instruction.
The device has three methods, execute
, upload_waveforms
, and upload_instructions
, and a property sampling_rate
.
In our endeavor to create a backend between Quantify and the mock device we start by creating a mock readout module so we can show its functionality:
import numpy as np
from quantify_scheduler.backends.mock.mock_rom import MockReadoutModule
rom = MockReadoutModule(name="mock_rom")
We first upload two waveforms (defined on a 1 ns grid) to the readout module:
intermodulation_freq = 1e8 # 100 MHz
amplitude = 0.5 # 0.5 V
duration = 1e-7 # 100 ns
time_grid = np.arange(0, duration, 1e-9)
complex_trace = np.exp(2j * np.pi * intermodulation_freq * time_grid)
wfs = {
"I": complex_trace.real,
"Q": complex_trace.imag
}
rom.upload_waveforms(wfs)
The mock readout module samples the uploaded waveforms and applies a certain gain to the acquired data. The sampling rate and gain can be set as follows:
rom.sampling_rate = 1.5e9 # 1.5 GSa/s
rom.gain = 2.0
The mock readout module takes a list of strings as instructions input:
rom.upload_instructions(["TRACE"])
We can now execute the instructions on the readout module:
rom.execute()
The data that is returned by our mock readout module is a dictionary containing the acquired I and Q traces:
import matplotlib.pyplot as plt
data = rom.get_results()
plt.plot(data[0])
plt.plot(data[1])
plt.show()
The goal is now to implement a backend that can compile and execute a Schedule
that consists of a single trace acquisition and returns a Quantify dataset.
1. Implement a custom Gettable
#
A good first step to integrating this mock readout module with Quantify is to implement a custom Gettable
that takes a set of instructions and waveforms that can be readily executed on the hardware. This Gettable
can then be used to retrieve the executed waveforms from the hardware. This enables testing of the InstrumentCoordinatorComponent
s without having to worry about the compilation of the Schedule
to the hardware.
- class MockROMGettable(mock_rom: MockReadoutModule, waveforms: Dict[str, quantify_scheduler.structure.types.NDArray], instructions: list[str], sampling_rate: float = 1000000000.0, gain: float = 1.0)[source]
Mock readout module gettable.
We check that this works as expected:
from quantify_scheduler.backends.mock.mock_rom import MockROMGettable
mock_rom_gettable = MockROMGettable(mock_rom=rom, waveforms=wfs, instructions=["TRACE"], sampling_rate=1.5e9, gain=2.0)
data = mock_rom_gettable.get()
plt.plot(data[0])
plt.plot(data[1])
plt.show()
From the plot, we observe that the waveforms are the same as what was sent into the MockReadoutModule
.
2. Implement InstrumentCoordinatorComponent
(s)#
Within Quantify, the InstrumentCoordinatorComponent
s are responsible for sending compiled instructions to the instruments, retrieving data, and converting the acquired data into a quantify-compatible Dataset (see Acquisition protocols). The InstrumentCoordinatorComponent
s are instrument-specific and should be based on the InstrumentCoordinatorComponentBase
class.
It is convenient to wrap all settings that are required to prepare the instrument in a single DataStructure
, in our example we take care of this in the MockROMAcquisitionConfig
and the settings for the mock readout module can be set via the MockROMSettings
class using the prepare
method of the MockROMInstrumentCoordinatorComponent
. The start
method is used to start the acquisition and the retrieve_acquisition
method is used to retrieve the acquired data:
- class MockROMAcquisitionConfig(/, **data: Any)[source]
Acquisition configuration for the mock readout module.
This information is used in the instrument coordinator component to convert the acquired data to an xarray dataset.
- acq_protocols[source]
- bin_mode[source]
- n_acquisitions[source]
- class MockROMSettings(/, **data: Any)[source]
Settings that can be uploaded to the mock readout module.
- acq_config[source]
- gain = 1.0[source]
- instructions[source]
- sampling_rate = 1000000000.0[source]
- waveforms[source]
We can now implement the InstrumentCoordinatorComponent
for the mock readout module:
- class MockROMInstrumentCoordinatorComponent(mock_rom: MockReadoutModule)[source]
Mock readout module instrument coordinator component.
- get_hardware_log(compiled_schedule: quantify_scheduler.schedules.schedule.CompiledSchedule)[source]
Return the hardware log.
- prepare(options: MockROMSettings)[source]
Upload the settings to the ROM.
- retrieve_acquisition()[source]
Get the acquired data and return it as an xarray dataset.
- start()[source]
Execute the sequence.
- stop()[source]
Stop the execution.
- property is_running[source]
Returns if the InstrumentCoordinator component is running.
The property
is_running
is evaluated each time it is accessed. Example:while my_instrument_coordinator_component.is_running: print('running')
- Returns:
The components’ running state.
Now we can control the mock readout module through the InstrumentCoordinatorComponent
:
from quantify_scheduler.backends.mock.mock_rom import MockROMInstrumentCoordinatorComponent, MockROMSettings, MockROMAcquisitionConfig
rom_icc = MockROMInstrumentCoordinatorComponent(mock_rom=rom)
settings = MockROMSettings(
waveforms=wfs,
instructions=["TRACE"],
sampling_rate=1.5e9,
gain=2.0,
acq_config = MockROMAcquisitionConfig(
n_acquisitions=1,
acq_protocols= {0: "Trace"},
bin_mode="average",
)
)
rom_icc.prepare(settings)
rom_icc.start()
dataset = rom_icc.retrieve_acquisition()
The acquired data is:
dataset
<xarray.Dataset> Size: 2kB Dimensions: (acq_index_0: 1, trace_index_0: 100) Dimensions without coordinates: acq_index_0, trace_index_0 Data variables: 0 (acq_index_0, trace_index_0) complex128 2kB (2+0j) ... (1.618033...
3. Implement CompilationNode
s#
The next step is to implement a QuantifyCompiler
that generates the hardware instructions from a Schedule
and a CompilationConfig
. The QuantumDevice
class already includes the generate_compilation_config()
method that generates a CompilationConfig
that can be used to perform the compilation from the quantum-circuit layer to the quantum-device layer. For the backend-specific compiler, we need to add a CompilationNode
and an associated HardwareCompilationConfig
that contains the information that is used to compile from the quantum-device layer to the control-hardware layer (see Compilation).
First, we define a DataStructure
that contains the information that is required to compile to the mock readout module:
- class MockROMHardwareCompilationConfig(/, **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
.- compilation_passes[source]
- config_type[source]
A reference to the
HardwareCompilationConfig
DataStructure for the Mock ROM backend.
- hardware_description[source]
- hardware_options[source]
We then implement a CompilationNode
that uses a Schedule
and a MockROMHardwareCompilationConfig
and generates the hardware-specific instructions:
- hardware_compile(schedule: quantify_scheduler.schedules.schedule.Schedule, config: quantify_scheduler.backends.graph_compilation.CompilationConfig)[source]
Compile the schedule to the mock ROM.
To test the implemented CompilationNode
, we first create a raw trace Schedule
:
from quantify_scheduler.schedules.trace_schedules import trace_schedule
sched = trace_schedule(
pulse_amp=0.1,
pulse_duration=1e-7,
pulse_delay=0,
frequency=3e9,
acquisition_delay=0,
integration_time=2e-7,
port="q0:res",
clock="q0.ro"
)
sched.plot_circuit_diagram()
(<Figure size 1000x100 with 1 Axes>,
<Axes: title={'center': 'Raw trace acquisition'}>)
Currently, the QuantumDevice
is responsible for generating the full CompilationConfig
. We therefore create an empty QuantumDevice
:
from quantify_scheduler.device_under_test.quantum_device import QuantumDevice
quantum_device = QuantumDevice("quantum_device")
We supply the hardware compilation config input to the QuantumDevice
, which will be used to generate the full CompilationConfig
:
from quantify_scheduler.backends.mock.mock_rom import hardware_compilation_config as hw_cfg
quantum_device.hardware_config(hw_cfg)
The QuantumDevice
can now generate the full CompilationConfig
:
from rich import print
print(quantum_device.generate_compilation_config())
SerialCompilationConfig( name='QuantumDevice-generated SerialCompilationConfig', version='v0.6', keep_original_schedule=True, backend=<class 'quantify_scheduler.backends.graph_compilation.SerialCompiler'>, device_compilation_config=DeviceCompilationConfig( clocks={}, elements={}, edges={}, scheduling_strategy='asap', compilation_passes=[ SimpleNodeConfig( name='circuit_to_device', compilation_func=<function _compile_circuit_to_device at 0x7fdcf03d8700> ), SimpleNodeConfig( name='set_pulse_and_acquisition_clock', compilation_func=<function set_pulse_and_acquisition_clock at 0x7fdcf03d8820> ), SimpleNodeConfig( name='resolve_control_flow', compilation_func=<function resolve_control_flow at 0x7fdcf0d071f0> ), SimpleNodeConfig( name='determine_absolute_timing', compilation_func=<function _determine_absolute_timing at 0x7fdcf0d00ee0> ), SimpleNodeConfig(name='flatten', compilation_func=<function flatten_schedule at 0x7fdcf0d07280>) ] ), hardware_compilation_config=MockROMHardwareCompilationConfig( config_type=<class 'quantify_scheduler.backends.mock.mock_rom.MockROMHardwareCompilationConfig'>, hardware_description={ 'mock_rom': MockROMDescription(instrument_type='Mock readout module', sampling_rate=1500000000.0) }, hardware_options=MockROMHardwareOptions( latency_corrections=None, distortion_corrections=None, modulation_frequencies={'q0:res-q0.ro': ModulationFrequencies(interm_freq=100000000.0, lo_freq=None)}, mixer_corrections=None, gain={'q0:res-q0.ro': 2.0} ), connectivity=Connectivity(graph=<quantify_scheduler.structure.types.Graph object at 0x7fdcf03d6280>), compilation_passes=[ SimpleNodeConfig( name='mock_rom_hardware_compile', compilation_func=<function hardware_compile at 0x7fdcfe5bb700> ) ] ), debug_mode=False )
We can now compile the Schedule
to settings for the mock readout module:
from quantify_scheduler.backends.graph_compilation import SerialCompiler
compiler = SerialCompiler(name="compiler", quantum_device=quantum_device)
compiled_schedule = compiler.compile(schedule=sched)
We can now check that the compiled settings are correct:
print(compiled_schedule.compiled_instructions)
{ 'mock_rom': MockROMSettings( waveforms={ '-5637678768848434263_I': NDArray([ 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455, 0.1 , 0.09135455, 0.06691306, 0.0309017 , -0.01045285, -0.05 , -0.0809017 , -0.09781476, -0.09781476, -0.0809017 , -0.05 , -0.01045285, 0.0309017 , 0.06691306, 0.09135455]), '-5637678768848434263_Q': NDArray([ 0.00000000e+00, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -2.44929360e-17, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -4.89858720e-17, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -4.28750176e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -9.79717439e-17, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -4.77736048e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -8.57500352e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -8.81993288e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -1.95943488e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02, -9.30979160e-16, 4.06736643e-02, 7.43144825e-02, 9.51056516e-02, 9.94521895e-02, 8.66025404e-02, 5.87785252e-02, 2.07911691e-02, -2.07911691e-02, -5.87785252e-02, -8.66025404e-02, -9.94521895e-02, -9.51056516e-02, -7.43144825e-02, -4.06736643e-02]) }, instructions=['TRACE_input0'], sampling_rate=1500000000.0, gain=2.0, acq_config=MockROMAcquisitionConfig( n_acquisitions=1, acq_protocols={0: 'Trace'}, bin_mode=<BinMode.AVERAGE: 'average'> ) ) }
4. Integration with the ScheduleGettable
#
The ScheduleGettable
integrates the InstrumentCoordinatorComponent
s with the QuantifyCompiler
to provide a straightforward interface for the user to execute a Schedule
on the hardware and retrieve the acquired data. The ScheduleGettable
takes a QuantumDevice
and a Schedule
as input and returns the data from the acquisitions in the Schedule
.
Note
This is also mainly a validation step. If we did everything correctly, no new development should be needed in this step.
We first instantiate the InstrumentCoordinator
, add the InstrumentCoordinatorComponent
for the mock readout module, and add a reference to the InstrumentCoordinator
to the QuantumDevice
:
from quantify_scheduler.instrument_coordinator.instrument_coordinator import InstrumentCoordinator
ic = InstrumentCoordinator("IC")
ic.add_component(rom_icc)
quantum_device.instr_instrument_coordinator(ic.name)
We then create a ScheduleGettable
:
from quantify_scheduler.gettables import ScheduleGettable
from quantify_scheduler.schedules.trace_schedules import trace_schedule
schedule_gettable = ScheduleGettable(
quantum_device=quantum_device,
schedule_function=trace_schedule,
schedule_kwargs=dict(
pulse_amp=0.1,
pulse_duration=1e-7,
pulse_delay=0,
frequency=3e9,
acquisition_delay=0,
integration_time=2e-7,
port="q0:res",
clock="q0.ro"
),
batched=True
)
I, Q = schedule_gettable.get()
We can now plot the acquired data:
plt.plot(I)
plt.plot(Q)
plt.show()
5. Integration with the MeasurementControl
#
Note
This is mainly a validation step. If we did everything correctly, no new development should be needed in this step.
Example of a MeasurementControl
that sweeps the pulse amplitude in the trace ScheduleGettable
:
from quantify_core.measurement.control import MeasurementControl
from quantify_core.data.handling import set_datadir, to_gridded_dataset
set_datadir()
meas_ctrl = MeasurementControl(name="meas_ctrl")
from qcodes.parameters import ManualParameter
amp_par = ManualParameter(name="trace_amplitude")
amp_par.batched = False
sample_par = ManualParameter("sample", label="Sample time", unit="s")
sample_par.batched = True
amps = [0.1,0.2,0.3]
integration_time = 2e-7
sampling_rate = quantum_device.generate_hardware_compilation_config().hardware_description["mock_rom"].sampling_rate
schedule_gettable = ScheduleGettable(
quantum_device=quantum_device,
schedule_function=trace_schedule,
schedule_kwargs=dict(
pulse_amp=amp_par,
pulse_duration=1e-7,
pulse_delay=0,
frequency=3e9,
acquisition_delay=0,
integration_time=integration_time,
port="q0:res",
clock="q0.ro"
),
batched=True
)
meas_ctrl.settables([amp_par, sample_par])
# problem: we don't necessarily know the size of the returned traces (solved by using xarray?)
# workaround: use sample_par ManualParameter that predicts this using the sampling rate
meas_ctrl.setpoints_grid([np.array(amps), np.arange(0, integration_time, 1 / sampling_rate)])
meas_ctrl.gettables(schedule_gettable)
data = meas_ctrl.run()
gridded_data = to_gridded_dataset(data)
Data will be saved in:
/root/quantify-data
Starting batched measurement...
Iterative settable(s) [outer loop(s)]:
trace_amplitude
Batched settable(s):
sample
Batch size limit: 900
Data processing and plotting:
import xarray as xr
magnitude_data = abs(gridded_data.y0 + 1j * gridded_data.y1)
phase_data = xr.DataArray(np.angle(gridded_data.y0 + 1j * gridded_data.y1))
magnitude_data.plot()
<matplotlib.collections.QuadMesh at 0x7fdcf01d2940>
phase_data.plot()
<matplotlib.collections.QuadMesh at 0x7fdceac38430>