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:

  1. The basic ReadSequence architecture

  2. Configuring a dummy readout sequence

  3. Compiling the Sequence to QUA code

  4. Parameter sweeps and measurements

  5. 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.

  1. The ‘parameters’ section acts identically to the configuration of a SubSequence that we discussed before. Parameters are created according to provided names, values, units etc.

  2. 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 its ReadoutPoints that are being used in the measurement.

  3. 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??