Memory spectroscopy#

Using pulsed mode, we measure the frequency of the bosonic mode. We sweep the frequency of the displacement pulse sent to the memory. Following the displacement pulse, we send a long selective \(\pi\) pulse to the qubit and finally the readout pulse to the readout resonator. The memory displacement pulse has a \(\sin^2\) envelope. The qubit-control pulse is a selective \(\pi\) pulse with a \(\sin^2\) envelope whose frequency is the frequency of the qubit when the cavity is in the Fock state \(|0\rangle\). The readout pulse is at the frequency of the readout resonator and has a square envelope. By probing the state of the qubit, we directly get the probablility that the cavity is in state \(|0\rangle\).

Memory spectroscopy sequence

The class for performing the memory spectroscopy experiment is available at presto-measure/sweep_memory.py. Here, we run the experiment and observe the qubit population reducing when the frequency of the memory displacement drive equals the memory frequency. This is because the memory displacemet drive displaced the memory state into some coherent state and the probability to find the memory in state \(|0\rangle\) reduced. We then have a look at the main parts of the code.

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

from sweep_memory import Sweep_memory
import numpy as np

experiment = Sweep_memory(
    readout_freq=6.2e9,
    control_freq=4.2e9,
    memory_freq_center=3.5e9,
    memory_freq_span=100e6,
    memory_freq_nr=101,
    readout_amp=0.1,
    control_amp=0.5,
    memory_amp=0.2,
    readout_duration=2.5e-6,
    control_duration=2000e-9,
    memory_duration=50e-9,
    sample_duration=2.5e-6,
    readout_port=1,
    control_port=2,
    memory_port=5,
    sample_port=1,
    wait_delay=50e-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 = Sweep_memory.load("../data/sweep_memory_20240129_155938.h5")

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

experiment.analyze()
../_images/sweep_memory_light.svg../_images/sweep_memory_dark.svg

The qubit is in the excited state for all the frequencies of the memory drive that are different than the memory frequency. This is because the memory is in the state \(|0\rangle\) and the selective qubit \(\pi\) pulse can excite the qubit. When the memory drive frequency is equal to the memory frequency, the memory state becomes a coherent state \(|\alpha\rangle\) and the probablility of memory being in the ground state \(|0\rangle\) reduces. Hence, the selective qubit \(\pi\) pulse will only partially excite the qubit and we will observe a reduction in the qubit population. Data is fitted to extract the frequency of the memory. The complex-valued readout signal is rotated using utils.rotate_opt() so that all the information about the qubit state is in the I quadrature.

Code explanation#

Here we discuss the main part of the code of the Sweep_memory class: the definition of the experiment sequence. The full source is available at presto-measure/sweep_memory.py.

Since we want to sweep the frequency of the memory pulse we should setup the frequency lookup table. We first create an array of all the memory frequencies

self.memory_freq_arr = self.memory_freq_center + np.linspace(
	-self.memory_freq_span / 2, self.memory_freq_span / 2, self.memory_freq_nr
)

Now we should convert this array into an array of intermediate frequencies IF and phases that are sent to the I and Q ports of the digital mixer. We set self.memory_freq_center as the LO frequency, so IF frequencies are the difference between the self.memory_freq_arr and the LO frequency.

memory_if_arr = self.memory_freq_arr - self.memory_freq_center

If IF frequency is positive this means we should output the high sideband, meaning phase Q should be \(\pi/2\) smaller than phase I: ph_q = ph_i - np.pi / 2. However, if IF frequency is negative we should output the lower sideband, meaning phase Q should be \(\pi/2\) bigger than phase I: ph_q = ph_i + np.pi / 2. Frequency has to be a positive number so we will have to take the absolute value of memory_if_arr and assign appropriate phases to each IF frequency. All of this is compactly written in the following lines of code:

mask = np.ma.masked_less(memory_if_arr, 0).mask
memory_if_arr = np.abs(memory_if_arr)
ph_i = np.zeros_like(memory_if_arr)
ph_q = ph_i - np.pi / 2 + mask * np.pi  # +-np.pi/2 for low/high sideband

mask is an array where an element is equal to 1 if the same element of memory_if_arr is smaller than zero. All the other elements of mask are equal to 0.

We should setup the memory template and indicate that this template will be multiplied with the IF generator by setting the envelope=True:

memory_ns = int(round(self.memory_duration * pls.get_fs("dac")))
memory_envelope = sin2(memory_ns)
memory_pulse = pls.setup_template(
	self.memory_port,
	group=0,
	template=memory_envelope + 1j * memory_envelope,
	envelope=True,
)

Note

All other pulses and scale lookup tables are set in a similar way like in the qubit tutorial. See for example Rabi amplitude for more details.

We program the pulse sequence:

T = 0.0  # s, start at time zero ...
pls.reset_phase(T, self.memory_port, group=0)
pls.output_pulse(T, memory_pulse)  # displace memory
T += self.memory_duration
pls.output_pulse(T, control_pulse)  # pi pulse conditioned on memory in |0>
T += self.control_duration
pls.output_pulse(T, readout_pulse)  # Readout
pls.store(T + self.readout_sample_delay)
T += self.readout_duration
pls.next_frequency(T, self.memory_port, group=0)
T += self.wait_delay  # Wait for decay

We first output the memory displacement pulse, then we output qubit-control \(\pi\) pulse that maps the population of Fock state \(|0\rangle\) in the memory onto qubit population. Finally, we perform the readout. We call Pulsed.next_frequency() to sweep the frequency of the memory pulse.


run() executes the experiment sequence. We set repeat_count=self.memory_freq_nr since we want to sweep through self.memory_freq_nr frequencies.

pls.run(period=T, repeat_count=self.memory_freq_nr, num_averages=self.num_averages)