# Rabi amplitude Using {mod}`.pulsed` mode, we measure Rabi oscillations by keeping the length of the qubit-control pulse constant and sweeping its amplitude. The control pulse is at the frequency of the qubit and has a $\sin^2$ envelope, while the resonator-readout pulse is at the frequency of the readout resonator and has square envelope. We sample the response of the readout resonator to determine the state of the qubit. We optionally output $N$ consecutive control pulses in order to obtain a more accurate measurement of the Rabi period. ![Rabi pulse sequence](images/Rabi_pulse_sequence.svg){align=center} The full source code for this experiment is available at [presto-measure/rabi_amp.py][rabi_amp.py]. Here, we first run the Rabi experiment and analyze the data in order to establish the amplitude of the $\pi$ and $\pi/2$ pulses, 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 `RabiAmp` to match your experiment and change `presto_address` to match the IP address of your Presto: ```python from rabi_amp import RabiAmp import numpy as np experiment = RabiAmp( readout_freq=6.2e9, control_freq=4.2e9, readout_amp=0.1, control_amp_arr=np.linspace(0, 0.5, 101), readout_duration=2.5e-6, control_duration=20e-9, sample_duration=2.5e-6, readout_port=1, control_port=4, sample_port=1, wait_delay=100e-6, readout_sample_delay=0e-9, num_averages=100, num_pulses=10, ) presto_address = "192.168.88.65" # your Presto IP address save_filename = experiment.run(presto_address) ``` Or you can also load older data: ```python experiment = RabiAmp.load("data/rabi_amp_20220413_082434.h5") ``` In either case, we analyze the data to get a nice plot: ```python experiment.analyze() ``` ```{image} images/rabi_amp_light.svg :align: center :class: only-light ``` ```{image} images/rabi_amp_dark.svg :align: center :class: only-dark ``` In the example above, one control pulse is applied before the readout pulse. When the amplitude of the control pulse is zero the qubit remains in the ground state, so we can conclude that the high value of the readout quadrature I corresponds to the ground state. When the amplitude of the control pulse corresponds to the amplitude of the $\pi$ pulse, the qubit is in the excited state, and we observe the first minimum at about 5% of the full-scale range (0.05 FS). When the amplitude of the control pulse corresponds to the amplitude of the $2\pi$ pulse we observe the second maximum at about 10% of the full-scale range, the qubit is in the ground state, and so on. The data is fitted to a cosine function. ## Code explanation Here we look under the hood of the `RabiAmp` class and discuss the main parts of the code, you can find the full source at [presto-measure/rabi_amp.py][rabi_amp.py]. :::{note} As this might be your first experiment with the {mod}`.pulsed` mode, we will take our time and explain all the details. ::: We start by creating an instance of the {class}`.Pulsed` class: `pls`. At this point, we connect to the Presto unit at the IP address `presto_address` and set up the reference clock (`ext_ref_clk`). We also tell the API we want to use digital up- and down-conversion, by setting both {class}`.AdcMode` and {class}`.DacMode` to the `Mixed` variant. If we didn't want to use the digital mixers, we'd use the default `Direct` variant instead. See the documentation for the {class}`.Pulsed` class and [Advanced tile configuration](<#adv tile>) for more details. ```python with pulsed.Pulsed( address=presto_address, ext_ref_clk=ext_ref_clk, adc_mode=AdcMode.Mixed, dac_mode=DacMode.Mixed, ) as pls: ``` --- We then configure the frequency up- and down-conversion with the built-in digital IQ mixers using {meth}`.Hardware.configure_mixer`. We need one mixer on the qubit-control output port, and two mixers on the resonator-readout output and input ports. ```python pls.hardware.configure_mixer( freq=self.readout_freq, in_ports=self.sample_port, out_ports=self.readout_port, ) pls.hardware.configure_mixer( freq=control_nco, out_ports=self.control_port, ) ``` `freq` is the frequency of the numerically-controlled oscillator (NCO), the digital counterpart to a local oscillator (LO) in analog mixing. The NCOs are armed immediately after calling {meth}`~.Hardware.configure_mixer`, but only start when a synchronization event occurs. In our case, the synchronization event is simply provided by the call to {meth}`.Pulsed.run` when we start the experiment later on. In this experiment, we choose to not use an intermediate frequency (IF), or rather we use an IF of zero. That means that our qubit-control and resonator-readout pulses will be output directly at the respective NCO frequencies. In an external, *analog* frequency conversion scheme you typically don't want to use zero IF to avoid $1/f$ noise and isolate your signal from mixing artifacts like LO leakage and mixer imbalance. Presto, on the other hand, uses *digital* frequency conversion so these concerns are not applicable and using zero IF is a good way of simplifying the signal chain. --- Next, we program the amplitude of the pulses in two look-up tables (LUT) using {meth}`~.Pulsed.setup_scale_lut`. Before reaching the digital up-conversion, output pulses go through a scalar multiplier or variable-gain block. There are two multipliers on each output port, each identified by their `group` (0 or 1) and with an associated LUT with 512 programmable entries storing values from -1 to 1. See the [functional schematics of the pulsed output](#sch-pls-output) for more details. ```python pls.setup_scale_lut(self.readout_port, group=0, scales=self.readout_amp) pls.setup_scale_lut(self.control_port, group=0, scales=self.control_amp_arr) ``` For the resonator-readout pulse we program a single amplitude `readout_amp` into the scale LUT, while for the qubit-control pulse we upload an array `control_amp_arr` containing all the amplitudes we want to sweep. The scale LUT provides a very efficient way of performing an amplitude sweep: rather than uploading a new pulse with a new amplitude, we just step to the next entry in the LUT! There are also two IF generators on each output port to implement efficient frequency and phase sweeps, but we don't use them in this experiment. --- Next, we create the output pulses by providing templates. Each output group has 8 available templates that can store arbitrary data for up to 1022 ns each, i.e. 1022 complex-valued data points at 1 GS/s when using digital up-conversion (`dac_mode` is one of {class}`DacMode.Mixedxx `). Templates can be concatenated, superimposed and played in a loop to create longer and mode complex waveforms. The maximum number of data points in a template depends on the IF sampling rate {meth}`get_fs("dac") <.Pulsed.get_fs>` and can be obtained by calling {meth}`.get_max_template_len`. There are two main ways to create a new pulse. For the qubit-control pulse, we provide the raw data points that make up our desired pulse shape using {meth}`~.Pulsed.setup_template`: ```python # number of samples in template control_ns = int(round(self.control_duration * pls.get_fs("dac"))) control_envelope = sin2(control_ns) control_pulse = pls.setup_template( self.control_port, group=0, template=control_envelope + 1j * control_envelope, ) ``` We first create a NumPy array containing a $\sin^2$ envelope using the convenience function {func}`.utils.sin2`. We then set the same template on both I and Q ports by defining the complex array `template=control_envelope + 1j*control_envelope`. For the resonator-readout pulse, we use the second way to create a pulse: {meth}`~.Pulsed.setup_long_drive`, which is very convenient when we are interested in a constant-amplitude pulse with optionally smooth rise and fall segments. A {class}`.LongDrive` can also encode a very long pulse without using up all the template slots available, and we can resize its duration during an experiment. ```python readout_pulse = pls.setup_long_drive( self.readout_port, group=0, duration=self.readout_duration, amplitude=1.0 + 1j, envelope=False, ) ``` We set the amplitudes of both I and Q templates to maximum `1+1j`. By doing a bit of [trigonometry for mixers][mixer_math] we can calculate that in our case ($\phi_I = \phi_Q = 0$ and $A_I=A_Q=1$), the output of the mixer will be $\sqrt{2}\cos(2\pi$ `readout_freq` $t + \pi/4)$, for duration `readout_duration`. We set `envelope=False`, so this pulse will not be multiplied with the IF generator and will be output directly at the NCO frequency. --- To complete the setup, we configure data acquisition by specifying what input ports we want to store data from with {meth}`~.Pulsed.set_store_ports`, and for how long with {meth}`~.Pulsed.set_store_duration`. Once we do that, each sampling window we open will store a number of data points equal to `sample_duration` × {meth}`get_fs("adc") <.Pulsed.get_fs>`. ```python pls.set_store_ports(self.sample_port) pls.set_store_duration(self.sample_duration) ``` --- Now that we are done with the setup, we move on to describe the pulse sequence that makes up our experiment. Let's look at the code first, then explain it line by line. ```python T = 0.0 # s, start at time zero ... for _ in range(self.num_pulses): pls.output_pulse(T, control_pulse) # Control pulse T += self.control_duration pls.output_pulse(T, readout_pulse) # Readout pls.store(T + self.readout_sample_delay) T += self.readout_duration pls.next_scale(T, self.control_port) # Move to next Rabi amplitude T += self.wait_delay # Wait for decay ``` We start at time `T = 0`, and all pointers of the LUTs (frequency and scale) point to their first entry. We use {meth}`~.Pulsed.output_pulse` to schedule the qubit-control pulse `control_pulse` to be output at time `T`. We repeat this `num_pulses` times in a `for` loop to output many control pulses back-to-back with no delay. We then output the resonator-readout pulse `readout_pulse` just once, and start sampling data with {meth}`~.Pulsed.store`. We include a delay `readout_sample_delay` between the readout pulse and the data acquisition to account for latency in the experimental setup. Finally, we prepare for the next iteration: {meth}`~.Pulsed.next_scale` selects the next value in the scale LUT for the control port (the new amplitude for `control_pulse`), and we wait for the qubit to decay by simply incrementing the time variable `T`. --- We're done describing the experiment but, up until now, we didn't actually output any pulses. To run the experiment, we call {meth}`~.Pulsed.run`: Presto will start outputting pulses, acquiring data, stepping parameters and performing interleaved averaging. ```python nr_amps = len(self.control_amp_arr) pls.run(period=T, repeat_count=nr_amps, num_averages=self.num_averages) ``` `period` is the time between each individual run of the programmed sequence. Multiple runs are repeated back-to-back with exact timing due to `repeat_count` and `num_averages`. `repeat_count` indicates we want to repeat the experiment `nr_amps` times, each time with a different qubit-control amplitude and storing a separate measured trace. In general, for each repetition we increment all frequency pointers called by {meth}`~.Pulsed.next_frequency` and all scale pointers called by {meth}`~.Pulsed.next_scale`. At this point, we have a sequence that is `period` × `repeat_count` long, and each period contains a unique qubit-control amplitude. We now repeat this larger sequence `num_averages` times, thus performing interleaved averaging. In total, the sequence is repeated `repeat_count` × `num_averages` times, and will terminate after `period` × `repeat_count` × `num_averages` seconds. --- When the experiment is done, we retrieve the averaged acquired data with {meth}`~.Pulsed.get_store_data`: ```python self.t_arr, self.store_arr = pls.get_store_data() ``` `t_arr` contains the time axis in seconds during one acquisition, with {meth}`get_fs("adc") <.Pulsed.get_fs>` sampling rate. `store_arr` is a three-dimensional NumPy array containing the sampled data, scaled to ±1.0 being full-scale input. The shape of the array is `(num_stores * repeat_count, num_ports, smpls_per_store)`, where `num_stores` is the number of store events programmed in the sequence (the number of calls to {meth}`~.Pulsed.store`), `num_ports` is the number of inputs ports set with {meth}`~.Pulsed.set_store_ports`, and `smpls_per_store` is the number of samples in one acquisition set by {meth}`~.Pulsed.set_store_duration`. Because we are using digital down-conversion, the returned data is complex valued (`dtype=np.complex128`). [rabi_amp.py]: https://github.com/intermod-pro/presto-measure/blob/master/rabi_amp.py [mixer_math]: ../special/mixer.md