Subsampling Techniques for Achieving Waveform Precision in Picoseconds
An arbitrary waveform generator (AWG) can generate pulses with a timing resolution much finer than its sampling period. Users are often unaware of this capability, yet it enables highly precise timing control in application use cases such as:
- Pulse delay control in dynamical decoupling for NV centers
- Width control for superconducting qubit flux pulses
- Deskew of dual-channel gate voltage pulses for spin qubits
- IQ mixer calibration
In this blog post, we show how to realize timing precision at a fraction of a sample, better than 30 ps, with the Zurich Instruments HDAWG. The method is also applicable to the SHFSG and SHFQC. Here, we build on a previous blog post by Andrea Corna, demonstrating how to break the waveform granularity limit to reach sample-precise pulse timing, and overcome another timing constraint.
Ramsey Revisited
We begin by revisiting the Ramsey pulse sequence from the previous blog post. This sequence consists of two π/2 pulses, generated off resonance from the frequency ω of our qubit frequency by ∂ω, which are separated by a wait time τ. For a qubit in its ground state |0⟩, aligned along the z-axis, the first π/2 pulse will bring it into a superposition of its ground and excited states, e.g. (|0⟩ + |1⟩)/√2. The qubit state evolves during τ, where it precesses around the z-axis, and the second π/2 pulse projects the qubit state along the z-axis for readout. We’ll demonstrate how the timing of these pulses can be controlled to within tens of picoseconds using AWG capabilities of the HDAWG, SHFSG, and SHFQC.
Achieving Subsample Precision
Using an AWG, we must work within certain constraints, such as the sample rate of the device, the waveform granularity, and the minimum length of waveforms. We are flexible, however, in defining the waveform we wish to use. Let’s take a Gaussian pulse as an example.
In our SeqC language, we can define the Gaussian:
wave w_gaussian = gauss(samples, amplitude, position, width);The samples argument defines the waveform length and must be at least 16. But the position argument that defines the pulse center position can take any value – even a fractional number of samples! Therefore, we have total control over where our waveform appears in time. Figure 1 shows how a Gaussian will be sampled for different central positions.
Figure 1: A Gaussian sampled at different central positions. The points show the amplitude values of the waveform samples, which can be specified at any value within the limits of the AWG vertical resolution.
The Pulse Sequence
Our implementation of the Ramsey pulse sequence is the same as in the previous blog post, where a Gaussian pulse, pulse0, is played followed by a second Gaussian pulse. We apply two levels of temporal shift to the second pulse: we shift the waveform with sample precision, and we changing the center of the Gaussian with subsample precision. We also assign an index to the waveform so that we can play it using the command table.
Constant values chosen by the user are written in all capital letters in our example code below. We first define the pulse width, SIGMAS_PULSE (used below to calculate the total waveform length), the number of subsampling bits to use (from which we calculate the total points for subsampling, 2SUBSAMPLING_BITS) and other parameters. This is done in Python.
# Waveform paramaters
seq.constants['PULSE_WIDTH'] = 1e-9 #ns
seq.constants['SIGMAS_PULSE'] = 6
# Sequence parameters
seq.constants['SUBSAMPLING_BITS'] = 2 # Bits to use for subsampling
seq.constants['T_START'] = 0 # Start wait time, in subsamples
# units (one step is
# 1/((2**SUBSAMPLING_BITS)*SAMPLE_RATE)
# second)
seq.constants['T_END'] = 320 # Stop wait time
seq.constants['T_STEP'] = 1 # Step wait time
seq.constants['SAMPLE_STEPS'] = 16 # Sample precise steps, given by the
# AWG granularity
# Readout parameters
seq.constants['TRIGGER_LEN'] = 32
seq.constants['READOUT_LEN'] = 1024
We then create the command table with a size NUM_SHIFTED_ENTRIES that is defined by our choice of step size and number of subsampling bits. Here, we do this in Python for the HDAWG.
#Waveforms index
NUM_SHIFTED_ENTRIES = seq.constants['SAMPLE_STEPS'] \
* 2**seq.constants['SUBSAMPLING_BITS']
# Creation of the command table
ct = CommandTable(awg.commandtable.load_validation_schema())
# - Define all the shifted pulses
for i in range(NUM_SHIFTED_ENTRIES):
ct.table[i].waveform.index = i
In SeqC, we then calculate the width and length pulse parameters for our waveforms given our chosen values from above, and we also define constants to do both the subsample and sample-precise shift of the waveforms.
//Pulse parameter calculations
const PULSE_WIDTH_SAMPLE = PULSE_WIDTH*DEVICE_SAMPLE_RATE;
const PULSE_TOTAL_LEN = ceil(PULSE_WIDTH_SAMPLE*SIGMAS_PULSE*2/16)*16;
const SUBSAMPLING = pow(2,SUBSAMPLING_BITS); // Number of subsampling steps
const ALL_BITS = SUBSAMPLING_BITS + 4; // Number of bits used to encode
// the fine part of wait time
// (sample and subsample precise)
const MASK_FINE = pow(2,ALL_BITS) - 1; // Mask to extract the fine part
// of wait time
const MASK_COARSE = -pow(2,ALL_BITS); // Mask to extract the coarse part
// of wait timeWe then create our waveform definitions.
//Waveform definition
wave pulse0 = gauss(PULSE_TOTAL_LEN, PULSE_TOTAL_LEN/2, PULSE_WIDTH_SAMPLE);
wave trigger = marker(TRIGGER_LEN, 1);
//Create shifted waveforms
cvar j,k;
for (j = 0; j < SAMPLE_STEPS; j++) {
for (k = 0; k < SUBSAMPLING; k++) {
// Calculate the subsample shifted center:
cvar PULSE_CENTER = PULSE_TOTAL_LEN/2 + k/SUBSAMPLING;
// Create the k-subsamples shifted waveform:
wave pulse = gauss(PULSE_TOTAL_LEN, PULSE_CENTER, PULSE_WIDTH_SAMPLE);
// Create the j-samples shifted waveform:
wave w_shifted = join(zeros(j), pulse, zeros(16 - j));
//Assign index j to the waveform:
assignWaveIndex(1,2, w_shifted, SUBSAMPLING*j + k);
}
}
Once we have defined our waveforms, we can use the same sequence as in the previous blog post, with the added benefit that our waveforms are spaced with subsample precision:
var t = T_START;
var t_fine, coarse, t_coarse;
do {
t_fine = t & MASK_FINE; // The fine shift is the ALL_BITS
// least significant bits
coarse = t & MASK_COARSE; // Check if coarse delay is needed (boolean).
// Equivalent to (t >= ALL_BITS)
t_coarse = t >> SUBSAMPLING_BITS; //Extract the coarse wait time
executeTableEntry(TE_FIRST_PULSE); //Play first pulse, no shift
if(coarse)
playZero(t_coarse); //Evolution time t (coarse)
executeTableEntry(t_fine); //Play second pulse, fine shift
executeTableEntry(TE_READOUT_TRG); //Readout trigger
playZero(READOUT_LEN); //Readout time
t += T_STEP; //Increase wait time
} while (t < T_END); //Loop until the end
Measuring the Output
To measure the output of our device, we use a Rhode & Schwarz oscilloscope. Figure 2 shows a close-up of the second pulse of the Ramsey sequence recorded with persistence. The cursors are spaced at the nominal timing separation of 28 ps and we can clearly distinguish 16 identical Gaussian pulses spaced at this interval.
Figure 2. Captured oscilloscope screenshots of the signal output. Changing the number of subsampling bits in the code will change the spacing of the waveforms. Here, we show a waveform spacing of 28 ps.
Get the Code
All of this functionality is accessible using our LabOne Q software framework to directly communicate with the instruments through Toolkit commands. After defining our device setup, called my_setup, we connect to it:
my_session = Session(device_setup=my_setup)
my_session.connect()We apply the instrument settings:
# An example of how to apply settings
with hdawg.set_transaction():
hdawg.sigouts[0].range(1) # Set the output 1 range in volts
hdawg.system.awg.oscillatorcontrol(1) # Turn on AWG Oscillator Control
hdawg.awgs[0].outputs[0].gains[0](0.6) # Sets the Waveform Generator
# output 1 amplitude
hdawg.sigouts[0].on(1) # Turn on Wave Output 1
hdawg.sines[0].enables[0](0) # Disable Sine Generator Output 1
hdawg.awgs[0].outputs[0].modulation.mode(0) # Disable Modulation on output 1
With the pulse sequence, seq_definition, and command table, ct, defined, all that’s left is to upload them to our device and run.
hdawg.awgs[0].load_sequencer_program(seq_definition)
hdawg.awgs[0].commandtable.upload_to_device(ct)
hdawg.awgs[0].enable(1)View the full example on GitHub!
Are you interested in how the Zurich Instruments QCCS and LabOne Q can enable your experimental success? I’d love to hear about your next challenge in the quantum realm. Write to me at kent.shirer@zhinst.com.