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.

Ramsey pulse sequence

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()
../_images/ramsey_fringes_light.svg ../_images/ramsey_fringes_dark.svg

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)