Single shot readout

Using pulsed mode and template matching we can demodulate and integrate the resonator response based on the qubit state without averaging. This means we get a single point in the IQ-plane for each measurement perfomed. We interleave preparing qubit in the ground state (no \(\pi\) pulse) and preparing qubit in the excited state (\(\sin^2\) envelope \(\pi\) pulse) followed by a readout pulse (square pulse) and we perfom this measurement nr_averages times. We get two blobs each representing qubit in either ground or excited state.

Single shot readout pulse sequence

The full source code for this experiment is available at presto-measure/single_shot_readout.py. Here, we first run the single shot readout experiment and analyze the data, and then we have a more detailed look at the most important parts of the code.

You can create a new experiment and run it on your Presto. Be sure to change the parameters of SingleShotReadout to match your experiment and change presto_address to match the IP address of your Presto:

from single_shot_readout import SingleShotReadout
import numpy as np

experiment = SingleShotReadout(
    readout_freq=7.81438e9,
    control_freq=5.5229793e9,
    readout_amp=0.08,
    control_amp=0.176,
    readout_duration=1000e-9,
    control_duration=50e-9,
    readout_sample_delay=0e-9,
    sample_duration=2500e-9,
    readout_port=1,
    control_port=4,
    sample_port=1,
    wait_delay=400e-6,
    template_match_start=268e-9,
    template_match_duration=800e-9,
    template_match_phase=0.0,
    num_averages=5000,
)

presto_address = "192.168.88.65"  # your Presto IP address
save_filename = experiment.run(presto_address)

Or you can also load older data:

experiment = SingleShotReadout.load("../data/single_shot_readout_20230622_145840.h5")

In either case, we analyze the data to get a nice plot:

experiment.analyze(rotate_optimally=False)
../_images/single_shot_readout_not_rotated_light.svg ../_images/single_shot_readout_not_rotated_dark.svg

Each point represents one demodulated and integrated measurement (blue/orange markers for the qubit in the ground/excited state).


Measured points can be rotated in post-processing so that all the information is contained in only I-quadrature. The angle of rotation can be used to set the template_match_phase so that the templates are rotated in the IQ-plane such that all the information is contained in one quardature.

experiment.analyze(rotate_optimally=True)
../_images/single_shot_readout_light.svg ../_images/single_shot_readout_dark.svg

Code explanation

Here we discuss the main parts of the code in presto-measure/single_shot_readout.py.

Note

If this is your first measurement in pulsed mode, you might want to first have a look at the Rabi amplitude chapter in this tutorial. There we describe the code more pedagogically and in more detail.

We program two amplitudes in the scale_lut. Zero amplitude to keep the qubit in the ground state and self.control_amp to apply a \(\pi\) pulse.

pls.setup_scale_lut(self.control_port, group=0, scales=[0, self.control_amp])

We then set the matching templates similarly to setting any other templates. They will be multiplied element vise with the demodulated sampled data and the result will be summed. In this example, we choose the simplest template, the square of template_match_duration duration. We set the matching templates in pairs. So we use template1 to integrate the I-quadrature and template2 to integrate the Q-quadrature.

shape = np.ones(int(round(self.template_match_duration * pls.get_fs("dac"))))
match_events = pls.setup_template_matching_pair(
	input_port=self.sample_port,
	template1=shape * np.exp(self.template_match_phase),
	template2=1j * shape * np.exp(self.template_match_phase),
)

The template_match_phase determines the angle of the IQ coordinate system allowing us to rotate the readout data.


The core of the measurement is the definition of the experimental sequence:

for i in range(2):
	pls.select_scale(T, i, self.control_port, group=0)
	pls.output_pulse(T, [control_pulse])
	T += self.control_duration

	pls.output_pulse(T, [readout_pulse])
	pls.store(T + self.readout_sample_delay)
	pls.match(T + self.template_match_start, [match_events])
	T += self.readout_duration + self.wait_delay

We use a for loop to either select zero amplitude or the amplitude of the \(\pi\) pulse and output the control_pulse. As usual, the resonator-readout pulse and the data acquisition are scheduled right after the control pulse. The matching has to be scheduled within a store window, and finally we wait for the qubit to decay back to the ground state.


To finally execute the measurement, we call run():

pls.run(period=T, repeat_count=1, num_averages=self.num_averages)
self.t_arr, self.store_arr = pls.get_store_data()
self.match_arr = pls.get_template_matching_data([match_events])

Appart from the usual averaged store_arr result, we obtain the match_arr by calling get_template_matching_data(). len(match_arr) is the same as the number of matching templates. This is two in our example, as we are setup one template_matching_pair. match_arr[0] contains the outcomes of matching with template1 (I-quadrature) in chronological order. Similarly, match_arr[1] contains the outcomes of matching with template2 (Q-quadrature). We performed 5000 averages, meaning 10000 measurements in total (5000 ground and 5000 excited qubit state measurements). These measurements are interleaved and so are the template matching results.

complex_match_data = self.match_arr[0] + 1j * self.match_arr[1]
ground_data = complex_match_data[::2]
excited_data = complex_match_data[1::2]

So we simply pick out every other element of the array to separate the ground_data from the excited_data.