Pulsed Resonator Spectroscopy

This tutorial is applicable to all SHFQA Instruments.

Goals and Requirements

The goal of this tutorial is to demonstrate how to use SHFQA to perform pulsed spectroscopy measurement with a customized pulse envelope using Zurich Instruments Toolkit API.

Users can download all zhinst-toolkit example files introduced in this tutorial from GitHub, https://github.com/zhinst/zhinst-toolkit/tree/main/examples.


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 the LabOne Python API, LabOne and the Firmware of the SHFQA device, zhinst-toolkit (pip install zhinst-toolkit) and Python (3.7 or newer) are updated and compatible,

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

  • start LabOne and open the LabOne Graphical User Interface using the default web browser,

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

shfqa tutorial loopback
Figure 1. SHFQA connection.


The tutorial starts with configuring sweep parameters is followed by generating and uploading pulse envelope and run the measurement, is finished with verification of the spectroscopy pulse. `` Users can use the Python code below to perform pulsed spectroscopy measurement, and each step is explained as the following.

  1. Connect the instrument

    Create a toolkit session to the data server and connect the device with the device ID, e.g. 'DEV12001', see Connecting to the Instrument.

    # Load the LabOne API and other necessary packages
    from zhinst.toolkit import Session
    from scipy.signal import gaussian
    import numpy as np
    SERVER_HOST = 'localhost'
    session = Session(SERVER_HOST)              ## connect to data server
    device = session.connect_device(DEVICE_ID)  ## connect to device
  2. Create a Sweeper and configure it

    Create a Sweeper and configure sweep parameters, see tutorial Continuous Resonator Spectroscopy.

    sweeper = session.modules.shfqa_sweeper
    CHANNEL_INDEX = 0 # physical Channel 1
    sweeper.sweep.start_freq(-1000e6) # in units of Hz
    sweeper.sweep.stop_freq(1000e6) # in units of Hz
    sweeper.sweep.oscillator_gain(0.8) # amplitude scaling factor, 0 to 1
    sweeper.sweep.use_sequencer = True # True (recommended): sequencer-based sweep; False: host-driven sweep
    sweeper.average.integration_time(1e-6) # in units of second
    sweeper.average.mode("sequential") # "sequential" or "cyclic"
    sweeper.rf.center_freq(7e9) # in units of Hz
    sweeper.rf.input_range(0) # in units of dBm
    sweeper.rf.output_range(0) # in units of dBm
    with device.set_transaction():
  3. Generate and upload pulse envelope

    Create a complex flat-top Gaussian envelope with 1 μs duration and 50 ns rise and fall time. Enable the pulsed mode by sweeper.envelope.enable(True) and upload the envelope to the waveform memory. This envelope can be displayed on the Waveform Viewer of the Readout Pulse Generator.

    SAMPLING_FREQUENCY = 2e9 # in units of Hz
    ENVELOPE_DURATION = 1.0e-6 # in units of second
    ENVELOPE_RISE_FALL_TIME = 0.05e-6 # in units of second
    std_dev = rise_fall_len // 10
    gauss = gaussian(2 * rise_fall_len, std_dev)
    complex_amplitude = (1 - 1j)/np.sqrt(2)
    flat_top_gaussian = np.ones(int(ENVELOPE_DURATION * SAMPLING_FREQUENCY)) * complex_amplitude
    flat_top_gaussian[0:rise_fall_len] = gauss[0:rise_fall_len] * complex_amplitude
    flat_top_gaussian[-rise_fall_len:] = gauss[-rise_fall_len:] * complex_amplitude
    sweeper.average.integration_delay(220e-9) # in units of second
    sweeper.envelope.enable(True) # True: Pulsed mode; False: Continuous mode
    sweeper.envelope.waveform(flat_top_gaussian) # upload envelope waveform
  4. Run the measurement and plot the data

    After executing sweeper.run(), all above parameters are updated, a SeqC program is automatically generated, uploaded and compiled based on the sweep parameters, see Continuous Resonator Spectroscopy, and the result is downloaded after the measurement is done. The power and phase are calculated (see Continuous Resonator Spectroscopy.) and plotted, see in Figure 3.

    result = sweeper.run()
    num_points_result = len(result["vector"])
    print(f"Measured at {num_points_result} frequency points.")
  5. Verify the spectroscopy pulse with the Scope

    To achieve the best SNR, integration should start when the pulse reaches the integration units. Here, the scope is used to verify the spectroscopy pulse and measure the integration delay.

    The Scope is configured in a single mode triggered by the Sequencer 1 Trigger Output without any trigger delay, the recorded channel is physical Input Channel 1, and the recording duration is 1.5 μs. The trigger generated from the Sequencer 1 is also used to trigger Spectroscopy measurement to send the spectroscopy pulse. The pulse recorded by the Scope is shown in Figure 2. The measured phase of the input signal depends on the delay between signal generation at the Readout Pulse Generator and signal arrival at the Integration Units. The integration delay can be simply obtained by checking the delay of the pulse from the Scope. In the loopback mode the delay is about 220 ns.

    RECORD_DURATION = 1.5e-6
    with device.set_transaction():
        device.scopes[0].trigger.channel(32 + CHANNEL_INDEX) # Sequencer 1 Trigger Output
        device.scopes[0].trigger.delay(0) # in units of second
        device.scopes[0].length(int(RECORD_DURATION * SAMPLING_FREQUENCY/16)*16)
        device.scopes[0].channels[SCOPE_CHANNEL].inputselect(CHANNEL_INDEX) # physical Channel 1
        device.qachannels[CHANNEL_INDEX].oscs[0].freq(0) # set oscillator frequency to 0 Hz
        device.qachannels[CHANNEL_INDEX].spectroscopy.trigger.channel(32 + CHANNEL_INDEX) # Sequencer 1 Trigger Output
        seqc_program = """\
            repeat(1) {
                resetOscPhase(); // reset the digital oscillator phase of Channel 1
                setTrigger(1); setTrigger(0); // send out Sequencer 1 Trigger
        device.scopes[0].run(single = True) # run the scope
        device.qachannels[CHANNEL_INDEX].generator.load_sequencer_program(seqc_program) # load SeqC program to the Sequencer
        device.qachannels[CHANNEL_INDEX].generator.single(1) # using single mode
        device.qachannels[CHANNEL_INDEX].generator.enable(1) # run the Sequencer
    scope_data, *_ = device.scopes[0].read() # readout the data from the Scope
    tutorial pulsed spectroscopy scope
    Figure 2. Pulse envelope recorded by the Scope
    tutorial pulsed spectroscopy
    Figure 3. Pulsed spectroscopy measurement results in loopback configuration.