Pulsed Resonator Spectroscopy

In this tutorial, the SHFQA is used to perform pulsed spectroscopy measurement with a customized pulse envelope using the Python API ShfSweeper class. The measurement starts with configuration of sweep parameters and verification of the readout pulse envelope, is followed by integration delay calibration and spectroscopy measurement, and is finished with power and phase plots calculated from integration results.

The LabOne Python API example used in this tutorial can be downloaded from GitHub, https://github.com/zhinst/labone-api-examples.

Goals and Requirements

This tutorial will show users how to

  • configure the input and output parameters, pulse envelope, sweep and average parameters, and trigger,

  • verify the spectroscopy pulse using the SHFQA Scope,

  • calibrate the delay between readout pulse generation and integration,

  • get the integrated results and plot the power and phase of the input signal.

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


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,

  • connect the SHFQA channel 1 output (input) to the readout input (output) line, see SHFQA connection.

shfqa tutorial loopback
Figure 1. SHFQA connection.


Users can use the Python code below to perform pulsed spectroscopy measurement, and each step is explained as the following.

  1. Connect the SHFQA to a host computer

    Open a daq-session to the dev-Instrument (see tutorial Connecting to the Instrument) and replace devXXXXX with the ID of the SHFQA Instrument, e.g. dev12024.

    device_id = 'devXXXXX'
    apilevel_example = 6
    server_host = "localhost"
    server_port = 8004
    (daq, dev, _) = zhinst.utils.create_api_session(
        device_id, apilevel_example, server_host=server_host, server_port=server_port
  2. Import required packages

    Import utility classes, helper packages, and general calculation and plotting packages to assist in this tutorial.

    import numpy as np
    import matplotlib.pyplot as plt
    import zhinst.utils
    from zhinst.utils.shf_sweeper import (
    import helper_resonator
    import helper_commons
    import zhinst.deviceutils.shfqa as shfqa_utils
  3. Configure measurement parameters

    The ShfSweeper class is issued by the daq-session and the device ID dev. The configuration of this class includes sweep parameters set by SweepConfig, averaging and integration set by AvgConfig, RF input and output parameters set by RFConfig, trigger source set by TrigConfig, and the envelope uploading set by EnvelopeConfig. All parameters are uploaded to the Instrument by sweeper.set_to_device().

    The readout pulse envelope is defined by a flap-top Gaussian function with pulse duration of 1 μs, rise and fall time of 0.05 μs, modulation frequency of 0 Hz, and scaling factor of 1. The envelope_delay, i.e., the delay between the trigger and propagation of the output signal, is set to 0.

    The input and output parameters are defined such that the readout signal is sent out from channel 1 with a center frequency of 4 GHz, and output and input power range of 0 dBm.

    The sweep parameters are defined such that the offset frequency is linearly swept over 51 points from -200 MHz to 300 MHz with an oscillator gain of 0.8. Users can also sweep the frequency logarithmically with "log" mapping. The integration length is the same as the pulse duration, and the results are averaged after the measurement is repeated twice at each frequency step, i.e., using sequential averaging mode. With another averaging mode called "cyclic", the results are averaged cyclically. Please note that use_sequencer not shown in the parameter list is True by default. In this mode, a SeqC program is automatically generated, uploaded and compiled in the SHFQA Sequencer, and the frequency of the digital oscillator is controlled by the Sequencer. This mode allows a fast resonator spectroscopy with estimated running time of \((t_{\mathrm{int}} + 200 ns)n_{\mathrm{averages}}n_{\mathrm{points}}\) with trigger source is "None", where \(n_{\mathrm{averages}}\) is the number of averages, \(n_{\mathrm{points}}\) is the number of sweep points. If set_sequencer is False, the Sequencer is not used, and the frequency sweep is slower.

    The default and recommended trigger source is "None". This setting provides the fastest measurement speed. If an external trigger is desired, the trigger source option can be found in LabOne Generator tab → Trigger → Digital Triggers → Signal or in the Device Node Tree under node /DEV…​./QACHANNELS/n/GENERATOR/AUXTRIGGERS/n/CHANNEL. If a self testing or debugging is desired, a loopback configuration (Marker output A (B) and Trig A (B) input of the same channel are internally connected set by the helper function) can be used. As shown in this tutorial, the trigger is sent from channel 1 Marker A to channel 1 trigger A. The trigger level is set to 0 V.

    # instantiate ShfSweeper
    sweeper = ShfSweeper(daq, dev)
    # generate the complex pulse envelope with a zero modulation frequency
    envelope_duration = 1.0e-6
    envelope_rise_fall_time = 0.05e-6
    envelope_frequencies = [0]
    flat_top_gaussians = helper_commons.generate_flat_top_gaussian(
    flat_top_gaussians_key = 0
    pulse_envelope = flat_top_gaussians[flat_top_gaussians_key]
    envelope_config = EnvelopeConfig(waveform=pulse_envelope, delay=envelope_delay)
    rf_config = RfConfig(channel=0, input_range=0, output_range=0, center_freq=4e9)
    sweep_config = SweepConfig(
    avg_config = AvgConfig(
    # use the marker output via loopback to trigger the measurement
    # remove this code when using a real external trigger (e.g. an HDAWG)
    trigger_source = "channel0_trigger_input0"
    # enable the loopback trigger
    helper_resonator.set_trigger_loopback(daq, dev)
    trig_config = TriggerConfig(source=trigger_source, level=0)
    sweeper.configure(sweep_config, avg_config, rf_config, trig_config, envelope_config)
    # set to device, can also be ignored but is needed to verify envelope before sweep
  4. Validate the spectroscopy pulse with the Monitor Scope

    The Monitor Scope is used to record the spectroscopy pulse with the offset frequency of 0 Hz. For this, turn on both the Signal Input and Output of the channel such that the instrument is ready to sending out pulses and acquiring data. In the following code snippet, the Monitor Scope is configured such that the input of the active channel is monitored, it is triggered by the same trigger used for the sweeper, and the acquired trace length is the same as the envelope duration. With the helper function, the acquired data is plotted, as shown in Figure 2.

    # turn on the input / output channel
    daq.setInt(f"/{dev}/qachannels/{rf_config.channel}/input/on", 1)
    daq.setInt(f"/{dev}/qachannels/{rf_config.channel}/output/on", 1)
    daq.setDouble(f"/{device_id}/qachannels/{rf_config.channel}/oscs/0/freq", 0)
    scope_trace = helper.measure_resonator_pulse_with_scope(
  5. Calibrate the spectroscopy delay

    To achieve the best SNR, the readout pulse coming from the device under test should fully overlap with the demodulation pulse. Therefore, the delay, i.e., how much time it takes for the readout pulse to propagate through the Instrument and all the cables to the integration unit, has to be calibrated. This can be done by comparing the data recorded on the scope with the original data. The calibrated delay is 220 ns and this parameter will be used for the pulsed spectroscopy measurement.

    # simple filter window size for filtering the scope data
    window_s = 4
    # apply the filter to both the intial pulse and the one observed with the scope
    # to introduce the same amount of 'delay'
    pulse_smooth = np.convolve(
        np.abs(pulse_envelope), np.ones(window_s) / window_s, mode="same"
    pulse_diff = np.diff(np.abs(pulse_smooth))
    sync_tick = np.argmax(pulse_diff)
    scope_smooth = np.diff(np.abs(scope_trace))
    scope_diff = np.convolve(
        scope_smooth, np.ones(window_s) / window_s, mode="same"
    sync_tack = np.argmax(scope_diff)
    delay_in_ns = (
       1.0e9 * (sync_tack - sync_tick) / shf_utils.Shfqa.SAMPLING_FREQUENCY
    delay_in_ns = 2 * ((delay_in_ns + 1) // 2)  # round up to the 2ns resolution
    print(f"delay between generator and monitor: {delay_in_ns} ns")
    print(f"Envelope delay: {envelope_delay * 1e9:.0f} ns")
    if spectroscopy_delay * 1e9 == delay_in_ns + (envelope_delay * 1e9):
        print("Spectroscopy delay and envelope perfectly timed!")
            f"Consider setting the spectroscopy delay to [{(envelope_delay + (delay_in_ns * 1e-9))}] "
        print("to exactly integrate the envelope.")
  6. Run the measurement and plot the results

    The measurement starts after running sweeper.run, and the results is acquired and plotted, as shown in Figure 3. The calculation from voltage to the power and phase can be found in the tutorial of Continuous Resonator Spectroscopy.

    # start a sweep
    result = sweeper.run()
    print("Keys in the ShfSweeper result dictionary: ")
    # alternatively, get result after sweep
    result = sweeper.get_result()
    num_points_result = len(result["vector"])
    print(f"Measured at {num_points_result} frequency points.")
    # simple plot over frequency
    helper_resonator.clear_trigger_loopback(daq, dev)
    pulsed spectroscopy
    Figure 2. Readout pulse envelope in spectroscopy mode
pulsed spec power phase
Figure 3. Power and phase of readout results versus offset frequency.