Resonator spectroscopy

Using lockin mode, we do a simple single-frequency sweep.

The example script for performing the resonator spectroscopy experiment is in the file presto-measure/sweep.py. Here, we first run the experiment and fit the resonator response, then we have a deeper look at the code used to perform the measurement.

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

from sweep import Sweep

experiment = Sweep(
    freq_center=6e9,
    freq_span=5e6,
    df=100e3,
    num_averages=100,
    amp=0.1,
    output_port=1,
    input_port=1,
)

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.load("data/sweep_20220331_163206.h5")

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

experiment.analyze()
../_images/sweep_light.svg ../_images/sweep_dark.svg

When the experiment is done, click and drag on the plot panel to select a region for the fit. This will fit the resonance with the circle-fit method and print the relevant fitted parameters. Note that the Python package resonator_tools should be installed for this to work.

----------------
fr = 6028146404.689569
Qi = 221104.1461204515
Qc = 12105.122298585347
Ql = 11476.785411046161
kappa = 497983.1063246707
f_min = 6028320000.0
----------------

Code explanation

Here we look under the hood of the Sweep class and discuss the main parts of the code. See the full source at presto-measure/sweep.py.

We start by creating the lck object, an instance of the Lockin class: this will 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 AdcMode and 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 Lockin class and Advanced tile configuration for more details.

with lockin.Lockin(
    address=presto_address,
    ext_ref_clk=ext_ref_clk,
    adc_mode=AdcMode.Mixed,
    dac_mode=DacMode.Mixed,
) as lck:

We tune the integration bandwidth df: this ensures that the chosen df is commensurate with the sampling frequency and if not, it will slightly change it. You can read more in the documentation for tune().

_, self.df = lck.tune(0.0, self.df)
lck.set_df(self.df)

We set up the digital IQ mixer for up- and down-conversion using Hardware.configure_mixer(). We start with the first frequency here, and we’ll update the frequency in a loop later on to perform the sweep.

freq = self.freq_arr[0]
lck.hardware.configure_mixer(freq, in_ports=self.input_port, out_ports=self.output_port)

Once the digital up- and down-conversion is set up, we proceed with the intermediate frequency (IF) configuration. We create an OutputGroup and an InputGroup, each with a single frequency. These groups map a number of the available output and input tones to output and input ports. We opt for using zero IF, so the spectroscopy tone will coincide with the local-oscillator frequency set in the mixer.

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.

og = lck.add_output_group(self.output_port, nr_freq=1)
og.set_frequencies(0.0)
og.set_amplitudes(self.amp)
og.set_phases(0.0, 0.0)

ig = lck.add_input_group(self.input_port, nr_freq=1)
ig.set_frequencies(0.0)

We complete the initial configuration by applying the settings to the hardware with apply_settings(). At this point, Presto will start outputting the spectroscopy tone. At very low output power, set_dither() can help to reduce nonlinearity by adding pseudorandom noise to the output.

lck.set_dither(self.dither, self.output_port)
lck.apply_settings()

To perform the actual frequency sweep, we loop through the frequency array and update the mixer settings. After making sure the settings are applied, we obtain the measured data with get_pixels().

for ii, freq in enumerate(self.freq_arr):
    lck.hardware.configure_mixer(freq, in_ports=self.input_port, out_ports=self.output_port)
    lck.apply_settings()

    _d = lck.get_pixels(self.num_skip + self.num_averages, quiet=True)
    data_i = _d[self.input_port][1][:, 0]  # in-phase data
    data_q = _d[self.input_port][2][:, 0]  # quadrature data
    data = data_i.real + 1j * data_q.real  # using zero IF

    self.resp_arr[ii] = np.mean(data[-self.num_averages :])

Finally, after the sweep is complete, we turn off the spectroscopy tone by setting the amplitude to zero.

og.set_amplitudes(0.0)
lck.apply_settings()