Sweeper Module for Spectroscopy

In this tutorial, we demonstrate how to run a frequency sweep using the Python API Sweeper module, obtain the data and plot it. Here, the Sweeper Module is configured to enable a continuous output signal that first probes the device under test and is then demodulated with the generated signal. As a result, we obtain the amplitude and phase response in transmission of our device under test.

Goals and Requirements

In this tutorial you learn how to:

  • prepare a frequency sweep using the SHFQA spectroscopy mode.

  • run the frequency sweep and obtain the measurement data

  • interpret and plot the measured data

This tutorial is applicable to all SHFQA Instruments and no additional instrumentation is needed.

Users can download all LabOne API Python example files introduced in this tutorial from GitHub, https://github.com/zhinst/labone-api-examples.

Preparation

The tutorial starts with the instrument in the default configuration (e.g., after a power cycle). For an optimal tutorial experience, please follow these preparation steps:

  • ensure that the version of ziPython, LabOne and the Firmware of the SHFQA device are updated and compatible,

  • make sure that the instrument is powered on and connected by Ethernet (or USB) to your local area network (LAN) where the host computer resides,

  • start LabOne and open the LabOne graphical user interface using the default web browser,

  • prepare an empty python script.

Tutorial

First we connect to the SHFQA using Python. For this, we open a daq-session to the dev-instrument using this code from the tutorial "Connecting to the instrument" and replace XXXXX with the id of our SHFQA instrument, e.g. 12024).

Next we import the Sweeper and its Data Classes from 'zhinst.utils' and instantiate the module.

# Load the Sweeper module and data classes
from zhinst.utils.shf_sweeper import (
    ShfSweeper,
    AvgConfig,
    RfConfig,
    SweepConfig,
)

# instantiate ShfSweeper
sweeper = ShfSweeper(daq, device_id)

The data classes 'SweepConfig', 'RFConfig' and 'AvgConfig' are useful as they bundle important configuration settings of the Sweeper module 'ShfSweeper' together.

We now configure our frequency sweep using the data classes and assign them to the Sweeper module.

# configure Sweeper
## configure the channel and RF settings
rf_config = RfConfig(channel=0, input_range=0, output_range=0, center_freq=6e9)

## configure in-band frequenc sweep parameters
sweep_config = SweepConfig(
    start_freq=-1000e6,
    stop_freq=1000e6,
    num_points=101,
    mapping="linear",
    oscillator_gain=1,
)

## configure settings for averaging and integration time
avg_config = AvgConfig(integration_time=100e-6, num_averages=2, mode="sequential")

## configure the Sweeper module using the data classes and switch on the channel
sweeper.configure(sweep_config, avg_config, rf_config)

The data class RfConfig includes the channel-specific settings, such as the center frequency in units of Hz, and the power ranges of the Input and Output channel.

The data class SweepConfig allows to configure the sweeps start and stop frequency as the in-band offset frequency. In addition, is contains the number of measured points in the sweep 'num_points', and the mapping of the points ('linear' or 'logarithmic'). The gain of the oscillator 'oscillator_gain' changes the output amplitude of the probe signal relative to the channels power ranges. Hence, the output power of the probe signal hence changes quadratically with this number.

The data class AvgConfig contains all settings related to averaging and integration. The 'integration_time' defines how long the signal is integrated in the qubit_measurement_unit, which can be up to ~16.7 ms. The mode 'sequential' or 'cyclic' define, whether each point is first averaged 'num_averages' times before the frequency is changed, or whether every sweep is averaged 'num_averages' times.

Now that the frequency sweep is fully configured, we can start the spectroscopy measurement after we have switched on the Output and the Input. This can be conveniently done either using the In/Out - tab of the GUI, or by writing the following node-commands in the Python script.

daq.setInt(f'/{device_id}/QACHANNELS/{rf_config.channel}/INPUT/ON', 1)
daq.setInt(f'/{device_id}/QACHANNELS/{rf_config.channel}/OUTPUT/ON', 1)

We do not automatically switch on or off the Input and Output of the device using the Sweeper module in order to protect the device under test. This needs to be a conscious action by the user.

To start the frequency sweep and obtain access to the measurement data, we can now use the 'sweeper.run()' function and either assign this call to a result vector, or we use 'sweeper.get_result()'. Hence, add the following code snippet to your python program and choose your preferred alternative to obtain the measurement results.

# start a sweep

##alternative 1
result = sweeper.run()

##alternative 2
sweeper.run()
result = sweeper.get_result()

# print additional information
num_points_result = len(result["vector"])
print(f"Measured at {num_points_result} frequency points.")

We are now ready to print the results. For a quick view of the data, execute the built-in plotter function 'sweeper.plot()'. In the image below, we see the full in-band transmission of the SHFQA. The loss in signal in the analog up/downconversion of the Microwave Input/Output lies well outside of the specified band of 1 GHz around the center frequency.

tutorial spectroscopy preview

To preview the data, the results of the Sweeper do not need to be assigned to a separate variable (see 'result' above). This is only necessary, if the data should be reused for further processing.

Plotting and Converting the Measurement Results

Alternatively, we can also plot and analyze the spectroscopy data using custom code. In this subsection we investigate the structure of the result-vector and use it to recreate a similar plot than the 'sweeper.plot()'-command.

First, lets have a look at the data structure of the returned results:

print("Keys in the ShfSweeper result dictionary: ")
print(result.keys())

### results
# dict_keys(['timestamp', 'flags', 'vector', 'properties'])

The results are returned as part of a dictionary with a 'timestamp' that provides when the measurement was taken, 'flags' that indicate errors or notable communications from the instrument, the results in form of a 'vector', and meta information about the sweep in 'properties'.

The data in 'vector' is complex-valued and provided in units of Volt, which corresponds to the signal that is measured by the SHFQA. To convert the measured data 'S' into a power in units of dBm, we need to take the decadic logarithm of the absolute value, i.e.

\[\begin{equation} P = 10\log_{10}\left(\frac{\lvert S \rvert^2}{R} \frac{[W]}{[mW]}\right) \end{equation}\]

where \(R=50~\Omega\), and we convert units of W to mW. To obtain the phase, we use

\[\begin{equation} \phi=\text{arctan2}\left[\text{Im}\left(S\right),\text{Re}\left(S \right)\right] \end{equation}\]

In the code below we first convert the data according to the above equations and also unwrap the phase. We then generate the frequency vector for the x-axis using the metadata from 'properties', before plotting the results using matplotlib.

import matplotlib.pyplot as plt
import numpy as np

# convert data into power and phase
power=10*np.log10(np.abs(result['vector'])**2/50/0.001)
phase=np.unwrap(np.angle(result['vector']))

# generate x-axis using metadata information
head=result['properties']
frequency=(np.linspace(head['startfreq'], head['stopfreq'], head['numpoints'])+head['centerfreq'])/10**9

# plot the data
plt.figure(figsize=(10, 6))
plt.subplot(211)
plt.plot(frequency,power)
plt.tick_params(labelbottom = False)
plt.ylabel('power [dBm]')
plt.title('Spectroscopy')
plt.grid(color='k', linestyle='-', linewidth=0.1)

plt.subplot(212)
plt.plot(frequency,phase)
plt.xlabel('frequency [GHz]')
plt.ylabel('phase [rad]')
plt.savefig('plots.png')
plt.grid(color='k', linestyle='-', linewidth=0.1)

plt.show()

Finally, to save and reload the data, it is possible to use the 'pickle' module, e.g. using the following code snippet.

import pickle

# save dictionary
a_file = open('data.pkl', 'wb')
pickle.dump(result, a_file)
a_file.close()

# load dictionary
a_file = open('data.pkl', 'rb')
output = pickle.load(a_file)
a_file.close()