Ramsey fringes¶
Using pulsed
mode, we measure Ramsey fringes to accurately determine the qubit frequency.
We send two \(\pi/2\) pulses to the qubit, separated by a variable delay \(\delta\) and we sweep their
frequency. The \(\pi/2\) pulses have a \(\sin^2\) envelope, while the resonator-readout pulse has a
square envelope.
The class RamseyFringes
for performing the experiment is available at
presto-measure/ramsey_fringes.py. Here, we use the class to run the
experiment, and then have a 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
RamseyFriges
to match your experiment and change presto_address
to match the IP address of
your Presto:
from ramsey_fringes import RamseyFringes
import numpy as np
experiment = RamseyFringes(
readout_freq=6.2e9,
control_freq_center=4.2e9,
control_freq_span=1e6,
control_freq_nr=101,
readout_amp=0.1,
control_amp=0.25,
readout_duration=2.5e-6,
control_duration=100e-9,
sample_duration=2.5e-6,
delay_arr=np.linspace(0, 50e-6, 101),
readout_port=1,
control_port=4,
sample_port=1,
wait_delay=100e-6,
readout_sample_delay=0e-9,
num_averages=100,
)
presto_address = "192.168.88.65" # your Presto IP address
save_filename = experiment.run(presto_address)
Or you can also load older data:
experiment = RamseyFringes.load("data/ramsey_fringes_20220413_013628.h5")
In either case, we analyze the data to get a nice plot:
experiment.analyze()
The frequency of Ramsey fringes (left panel) depends on the detuning between the qubit frequency and the frequency of the \(\pi/2\) pulses (control frequency). This frequency is extracted for each slice of the data (right panel), and at the “true” qubit frequency is found at the crossing of the two linear fits.
Code explanation¶
Here we discuss the main bits of the code in presto-measure/ramsey_fringes.py. This experiment is an extension of the earlier Ramsey \(T_2^*\) chapter in this tutorial, in the sense that we now sweep one extra parameter: the frequency of the qubit-control pulses.
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.
The main novelty in this experiment is that we want to sweep the frequency of the qubit-control
pulse. Since this is the first time we sweep frequencies inside of a pulsed
sequence, we’ll
take things slowly and in detail.
We begin with configuring the digital up-conversion scheme. So far, we have used an intermediate frequency (IF) of zero, and programmed the numerically-controlled oscillator (NCO) directly at the desired output frequency (see the Rabi amplitude chapter for more details). We do the same here for the resonator readout, but we take a different strategy for the qubit control to enable efficient sweeping of the frequency. This is because we are able to change the IF frequency during the sweep every 2 ns, while changing the NCO frequency currently takes a few ms.
We choose to use single-sideband (SSB) modulation, specifically the upper sideband (USB): the final frequency of the qubit-control pulse is the sum of the NCO frequency and of the IF. We also choose to center the IF sweep around the middle of the USB for simplicity:
# intermediate frequency
control_if_center = pls.get_fs("dac") / 4 # 250 MHz, middle of USB
control_if_start = control_if_center - self.control_freq_span / 2
control_if_stop = control_if_center + self.control_freq_span / 2
control_if_arr = np.linspace(control_if_start, control_if_stop, self.control_freq_nr)
# up-conversion carrier
control_nco = self.control_freq_center - control_if_center
# final frequency array
self.control_freq_arr = control_nco + control_if_arr
We then program the up- and down-conversion mixers as usual:
pls.hardware.configure_mixer(
self.readout_freq, # <-- output frequency (zero IF)
in_ports=self.sample_port,
out_ports=self.readout_port,
)
pls.hardware.configure_mixer(
control_nco, # <-- up-conversion frequency (nonzero IF)
out_ports=self.control_port,
)
Previously in the Rabi amplitude chapter of the tutorial, we used the scale look-up table (LUT) to efficiently sweep the amplitude of the qubit-control pulse. Here we want to sweep the frequency of the pulse, so we use an IF generator and a frequency LUT.
The output of the IF generator is modulated by a pulse we specify and sent to the I and the Q ports
of the digital mixer. The LUT and IF generator provide an efficient way of performing parametric
sweeps: instead of uploading new data points in the pulse template, we just step to the next entry
in the LUT. Every output port has two programmable IF generators, identified by their group
(0 or
1). Each group has a LUT with 512 programmable entries that store values between 0 and 500 MHz. See
the functional schematics of the pulsed output for more details.
We program the LUT for the IF generator using setup_freq_lut()
:
pls.setup_freq_lut(
self.control_port, group=0,
frequencies=control_if_arr,
phases=np.full_like(control_if_arr, 0.0),
phases_q=np.full_like(control_if_arr, -np.pi / 2), # upper sideband
)
In addition to the array of IF frequencies control_if_arr
we created before, we also create two
arrays for the I and Q phases, with the same length as control_if_arr
. We set the phases to zero
in the I port (phases
) and to \(-\pi/2\) for the Q port (phases_q
). This performs
single-sideband modulation (SSB) and select the upper sideband (USB); for the lower sideband (LSB),
we would set phases_q
to \(+\pi/2\) instead. More about IQ-mixer math and conventions can be
found in Mixer math.
We create to qubit-control pulse using setup_template()
nearly as usual:
# number of samples in template
control_ns = int(round(self.control_duration * pls.get_fs("dac")))
control_envelope = self.control_amp * sin2(control_ns)
# increase amplitude by 3 dB
control_envelope *= np.sqrt(2)
control_pulse = pls.setup_template(
self.control_port, group=0,
template=control_envelope + 1j * control_envelope,
envelope=True, # <-- multiply by IF generator
)
We set envelope=True
to indicate that the qubit-control should modulate the IF generator on
control_port
and group
0. We also increase the pulse amplitude by a factor of \(\sqrt{2}\), to be
consistent with the previous experiments where we were outputting control pulses directly at the
NCO frequency. See Mixer math for more details on mixer math.
The definition of the pulse schedule is almost identical to that we did in Ramsey
\(T_2^*\) We just add a call to next_frequency()
right after the for
loop:
T = 0.0 # s, start at time zero ...
for delay in self.delay_arr:
# first π/2 pulse
pls.reset_phase(T, self.control_port)
pls.output_pulse(T, control_pulse)
T += self.control_duration
T += delay # variable delay
# second π/2 pulse
pls.output_pulse(T, control_pulse)
T += self.control_duration
# readout
pls.output_pulse(T, readout_pulse)
pls.store(T + self.readout_sample_delay)
T += self.readout_duration
T += self.wait_delay # wait for decay
pls.next_frequency(T, self.control_port)
T += self.wait_delay
We finally indicate how many frequencies we want to step through in the run()
command by setting repeat_count
:
pls.run(period=T, repeat_count=self.control_freq_nr, num_averages=self.num_averages)