Tutorial 2) Readout sequences
0. Introduction
The first tutorial demonstrated sequence writing and parameterisation with configuration files was demonstrated. This enables user to apply and quantify any waveform on the instrument outputs.
Playing arbitrary waveforms is one part of qubit experiments, reading them out is equally crucial.
In this tutorial we explain the concept of ReadSequences and how to use them.
The tutorial is structured in the following way:
The basic
ReadSequencearchitectureConfiguring a dummy readout sequence
Compiling the Sequence to QUA code
Parameter sweeps and measurements
Scaling readout sequences to bigger systems
Chapter 1-4 demonstrates how a ReadSequence is built, how to configure and how to compile it. Part 4 connects this with sweeps of arbitrary parameters and part 5 demonstrates how to scale up the examined systems without altering the ReadSequence class.
1. The basic ReadSequence architecture
ReadSequence
The ReadSeqeunce is a child class of the SubSequence which we explored in Tutorial 1. Arbitrary waveforms can still be played, parameterised and swept.
The ReadSequence is meant to handle measurement sequences and describe them in a device agnostic way. For this the classes in the rest of this list are introduced. They all relate to a single given ReadSequence.
Signal
Signals represent one or more measurement results from a single physical entity (e.g qubit, SET, quantum dot etc.) A ReadSequence can have an arbitrary number of Signal instances.
Observable
Each observable handles a single result that is being accquired during the execution of a ReadSequence. Observables store the qua variables that store measurement results temporarily and the streams (to be discussed later) they are saved to. Each result is assigned to a single signal instance.
ReadoutPoint
A readout point describes a direct readout of a given quantum element at a different ‘point’ (e.g in voltage or frequency space).
Qua commands describing this measurement are given in the qua_measure method.
Per Readout point, multiple observables can be introduced whose FPGA variables are automatically declared, assigned and saved to the correct stream (discussed later) by this class.
AbstractReadout
The AbstractReadout works similarly to the ReadoutPoint but gives you all the freedom. Qua commands can be executed, arbitrary arguments like observables from other AbstractReadouts or even ReadoutPoints can be passed as arguments to process them further. in comparison to the ReadoutPoint, the results (observables) of an AbstractReadout can be assigned to ANY given signal.
This might seem a bit abstract on the first glace but will be a lot more clear after looking how this works in action.
This scheme can be further visualised by comparing this with the configuration we will use in this tutorial.
2. Configuring a dummy readout sequence
2.1 The three parts of the configuration
The following example may seem overwhelming at first, but can easily be sliced into logical blocks.
The given dictionary has three keys.
The ‘parameters’ section acts identically to the configuration of a
SubSequencethat we discussed before. Parameters are created according to provided names, values, units etc.The ‘signals’ section defines the physical entities we want to measure that are grouped into
Signals. To define a signal, the involved readout elements of the quantum machine need to be provided as well as itsReadoutPointsthat are being used in the measurement.The last key is the ‘readout_groups’ within which multiple logical groups are defined that again contain configurations for
AbstractReadouts. Those groups are convenient if you want to execute many readout operations at the same time. An abstract readout is simply configured by giving the desired ‘method’ (1), a name under which the result should be stored on the signal (2) and the arguments that are required for the given ‘method’ (3).
from arbok_driver.parameter_types import Voltage, Time
readout_sequence_config = {
'parameters': {
't_between_measurements': {
'value': 50,
'type': Time,
}
},
'signals':{
'qubit1':{
'elements': {
'sensor1': 'readout_element',
},
'readout_points': {
'reference': {
'method': 'average',
'desc':'reference point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
},
'read': {
'method': 'average',
'desc': 'readout point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
}
}
},
},
'readout_groups': {
'difference': {
'qubit1__diff': {
'method': 'difference',
'name': 'diff',
'args': {
'signal': 'qubit1',
'minuend': 'qubit1.reference.sensor1_IQ',
'subtrahend': 'qubit1.read.sensor1_IQ',
},
},
}
},
}
2.2 Writing custom Readsequences
In the next step we will have a look at a custom ReadSequence that takes a ‘reference’ and a ‘read’ measurement for each signal that has those ReadPoints provided.
After that the difference between those acquired results is calculated (in real time on the FPGA) and saved in the respective buffer.
from example_configs.dummy_sample import dummy_sample
from example_sequences.dummy_readout import DummyReadout
Let us have a look at the source code for DummyReadout. Writing a custom class can be quite simple as only two methods need to be provided:
__init__:
This constructor is called when the class is instantiated. We need to provide a name, the used Sample and a configuration as given above.
Within DummyReadout’s constructor, the parent’s (ReadSequence.__init__) constructor is being called using super(). All arguments are forwarded to the parent and the available types of ReadoutPoints and AbstractReadouts are being passed as well. All keys provided in those dictionaries can be used in the ‘method’ section of the sequence configuration discussed above.
qua_sequence:
This method contains qua commands to be executed on the hardware defining our measurement. In this example not a lot of explicit qua code can be seen, but rather method calls to qua_measure_and_save. Those helper classes store the respective qua code in their own methods.
It can be seen that explicit FPGA variable declaration and stream/memory management is not required. This is all handled automatically by the given observables. Remember, an Observable is always responsible for a single measurement result and stores its qua variables and data streams and puts them in the correct place in the qua program.
DummyReadout??
For the sake of readability jupyter inline tools from rich are imported.
%load_ext rich
As in tutorial 1, an ArbokDriver and an empty Sequence is created to which we add our dummy_readout.
from arbok_driver import ArbokDriver, Sample, Measurement
qm_driver = ArbokDriver('qm_driver', dummy_sample)
dummy_sequence = Sequence(qm_driver, 'dummy_squence', dummy_sample)
dummy_readout = DummyReadout(dummy_sequence, 'dummy_readout', dummy_sample, readout_sequence_config)
With the discussed configuration, a sample object and the dummy readout class above, we instantiated a DummyReadout.
2.3 Attributes and helpers of ReadSequence
In this section we will explore the attributes and helper of a ReadSeqeunce that a user can interact with after instantiation with a configuration file. The given examples use the previously discussed DummyReadout and the example configuration from the section before that.
The easiest way to quickly inspect any qcodes instrument is to call its print_readable_snapshot method.
2.3.1 Parameters
First of all, a standard SequenceParameter can be found. This is the one we defined in the ‘parameters’ section of the configuration above as ‘t_between_measurements’ and behaves exactly as a parameter in a simple SubSequence.
Then many parameters with rather lengthy names can be found whose value is given as ‘Not available’. Those are the ‘GettableParameters’ we will be able to measure once the program is run on the hardware. Their names are always unique since sequence and signal names have to be unique as well. Therefore those names are used as well to define streams within qua.
dummy_readout.print_readable_snapshot()
2.3.2 Observables
From the parameter name you can easily find the respective observable. Double underscores in the name indicate that the following name is an attribute of the previous class. Lets give it a try:
dummy_readout.qubit1.diff
dummy_readout.qubit1.reference.sensor1_IQ
Calling an observable returns you its GettableParameter:
dummy_readout.qubit1.diff()
2.3.3 Signals
All present signals can be accquired by calling the signals attribute of the ReadSequence.
dummy_readout.signals
2.3.4 Readout points and abstract readouts
All ReadoutPoints of a sequence can be found but also the ones that are linked to a specific Signal:
dummy_readout.readout_points
dummy_readout.qubit1.readout_points
The same is true for AbstractReadout. However those are not necessarily signal specific thus they are not bound to a Signal.
dummy_readout.abstract_readouts
ReadoutPoints and AbstractReadouts can introduce multiple Observabless (single results). The ‘average’ method being used in the given example introduces 3 observables:
dummy_readout.qubit1.reference.observables
3. Compiling a ReadSequence to QUA code
After instantiating the ReadSequence it can be compiled to QUA code as shown below:
qua_program = qm_driver.get_qua_program()
qm_driver.print_qua_program_to_file('qua_programs/tut2_readout.py', qua_program)
from qua_programs import tut2_readout
tut2_readout??
As seen in the output, arbok takes care of all repetitive parts of the QUA program like declaring variables or streams. It only allocated memory for variables that are actually used within the measurement.
The user can therefore focus on writing the pulse sequences instead of managing FPGA resources.
4. Parameter sweeps and measurements
Now we know how to set up sequences with measurements. Let us combine this with adding parameter sweeps as shown in tutorial 1. Just call the sequence and set_sweeps by given one dict per sweep axis with the parameter to sweep as key and the sweep array as value.
import numpy as np
dummy_sequence.set_sweeps(
{
dummy_readout.t_between_measurements: np.arange(10,100,10, dtype = int)
},
)
qua_program = qm_driver.get_qua_program()
qm_driver.print_qua_program_to_file(
'qua_programs/tut2_readout_with_sweep.py', qua_program)
from qua_programs import tut2_readout_with_sweep
tut2_readout_with_sweep??
5. Scaling readout sequences to bigger systems
To use this type of readout on a bigger system, you only need to touch the configuration. All sequences are always meant to be designed to be sample agnostic. In the case below we add a second signal ‘qubit2’ with the same ReadPoints and the respective AbstractReadout to take the difference between those results. Scroll down to see the QUA program of this sequence.
readout_sequence_config2 = {
'parameters': {
't_between_measurements': {
'value': 50,
'type': Time,
}
},
'signals':{
'qubit1':{
'elements': {
'sensor1': 'readout_element',
},
'readout_points': {
'reference': {
'method': 'average',
'desc':'reference point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
},
'read': {
'method': 'average',
'desc': 'redout point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
}
}
},
'qubit2':{
'elements': {
'sensor1': 'readout_element',
},
'readout_points': {
'reference': {
'method': 'average',
'desc':'reference point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
},
'read': {
'method': 'average',
'desc': 'redout point',
'observables': ['I', 'Q', 'IQ'],
'save_values': True
}
}
},
},
'readout_groups': {
'difference': {
'qubit1__diff': {
'method': 'difference',
'name': 'diff',
'args': {
'signal': 'qubit1',
'minuend': 'qubit1.reference.sensor1_IQ',
'subtrahend': 'qubit1.read.sensor1_IQ',
},
},
'qubit2__diff': {
'method': 'difference',
'name': 'diff',
'args': {
'signal': 'qubit2',
'minuend': 'qubit2.reference.sensor1_IQ',
'subtrahend': 'qubit2.read.sensor1_IQ',
},
},
}
},
}
Creating the driver and sequences:
qm_driver2 = ArbokDriver('qm_driver2', dummy_sample)
dummy_sequence2 = Sequence(qm_driver2, 'dummy_squence2', dummy_sample)
dummy_readout2 = DummyReadout(dummy_sequence2, 'dummy_readout2', dummy_sample, readout_sequence_config2)
The philosophy of arbok is to:
write devices and setup agnostic control/read sequences
quantify those sequences with configuration files In this manner, the dummy_readout2 driver’s corresponding parameters (shown below) were created without even touching the dummy_sequence2.
dummy_readout2.print_readable_snapshot()
QUA variables and streams are automatically put in place and the sequence is executed as expect for the system with double the size.
qua_program = qm_driver2.get_qua_program()
qm_driver.print_qua_program_to_file(
'qua_programs/tut2_double_readout.py', qua_program)
from qua_programs import tut2_double_readout
tut2_double_readout??