Subsampling Techniques for Achieving Waveform Precision in Picoseconds¶
This notebook demonstrates how to realize timing precision at a fraction of a sample and it will cover:
- Fine shift with analytical function
- Subsampling shift with
sinckernel on individual waveforms - Same as the previous bullet point, but on an output using the HDAWG real-time FIR filter
A more complete explanation of the techniques shown there can be found in this blog post.
Imports¶
import matplotlib.pyplot as plt
import numpy as np
from laboneq.simple import *
Analytical functions¶
Here we define a gaussian, whose center is shifted by a fraction of a sample:
@pulse_library.register_pulse_functional
def gaussian_subsample_shift(
x, sigma=1 / 3, shift=0.4, length=..., sampling_rate=..., **_
):
shift_sample = shift / sampling_rate / length * 2.0
return np.exp(-((x - shift_sample) ** 2) / (2 * sigma**2))
when discretized, for different shifts, it will look like this:
We can recognize the Gaussian profile and how it is shifting with different positions, but the discrete sampling seems to distort our waveform. In reality, these discrete jumps will be filtered out by the antialiasing low-pass filter incorporated into the instrument. If we take that into account, the result is a smooth shift as we programmed:
Now, let's plot the function we defined to verify that. We can use the method generate_sampled_pulse to get a waveform to plot:
length = 7e-9 # s
shift = 0.5 # samples
plt.figure()
# Not shifted
time, wfm = gaussian_subsample_shift(length=length, shift=0.0).generate_sampled_pulse()
plt.plot(time * 1e9, wfm.real, "x-", label="shift 0.0 samples")
# Shifted by half sample
time, wfm = gaussian_subsample_shift(
length=length, shift=shift
).generate_sampled_pulse()
plt.plot(time * 1e9, wfm.real, "x-", label=f"shift {shift:.1f} samples")
plt.xlabel("Time (ns)")
plt.ylabel("Amplitude (a.u.)")
plt.legend()
plt.show()
Finally, we can use it like any other pulse.
First, we define a device setup, signal lines, basic calibration and create a session
# Create the `DeviceSetup` and add the information about the data server it should connect to.
device_setup = DeviceSetup("ZI_HDAWG")
device_setup.add_dataserver(
host="localhost",
port="8004",
)
# Create a `HDAWG` instrument, which was imported from `laboneq.simple`, and add it to the `DeviceSetup`.
# When creating the instrument instance, you need to specify the device ID under `address`; for example, `dev8123`.
hdawg = HDAWG(
uid="hdawg",
interface="1GbE",
address="dev8123",
device_options="HDAWG8/MF/ME/CNT/PC",
reference_clock_source="internal",
)
device_setup.add_instruments(hdawg)
# Create a flux lines for a qubit (signal type is "rf")
device_setup.add_connections(
"hdawg",
create_connection(to_signal="q0/flux", ports="SIGOUTS/0", type="rf"),
)
# Configure the `Calibration` of the signal lines of the `DeviceSetup`.
config = Calibration()
config["q0/flux"] = SignalCalibration(voltage_offset=0.0, range=5.0)
# Apply the configuration to the DeviceSetup
device_setup.set_calibration(config)
# Connect to the LabOne data server via the LabOne Q Session
session = Session(device_setup)
session.connect(do_emulation=True) # do_emulation=True when at a physical setup
We can then define a simple experiment where we played shifted version of such gaussian
@dsl.experiment(signals=["q0_flux"])
def experiment_subsampling_analytical(count):
with dsl.acquire_loop_rt(count=count):
# Sweeps various delays
with dsl.sweep(
name="delay_sweep",
parameter=SweepParameter("delays", np.linspace(0.0, 1.0, 11)),
) as delay:
# Plays a shifted gaussian
with dsl.section(
name="play-flux-pulse_q0", trigger={"q0_flux": {"state": 1}}
):
dsl.play(
"q0_flux",
gaussian_subsample_shift(amplitude=1, length=7e-9, shift=delay),
)
# Add a gap after each pulse
with dsl.section(name="gap-flux-pulse_q0"):
dsl.delay("q0_flux", time=0.5e-6)
# Map the ExperimentSignals "q0_flux" to the logical signal lines defined in the `DeviceSetup`
dsl.map_signal("q0_flux", "q0/flux")
experiment = experiment_subsampling_analytical(10)
compiled_experiment = session.compile(experiment)
Plot a simulation
from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation
plot_simulation(compiled_experiment, start_time=0, length=0.5e-5)
and eventually runs it on real hardware
_ = session.run()
Non-analytical waveforms¶
Here we show how to shift sampled waveforms
# Generate a sampled waveform
# Here we use (again!) a gaussian, but anything will work
length = 7e-9 # s
time, wfm = pulse_library.gaussian(length=7e-9).generate_sampled_pulse()
wfm = np.real(wfm)
For that we use a FIR sinc filter:
shift = 0.5 # samples
from scipy.signal import lfilter
from laboneq.contrib.example_helpers.subsampling import fractional_delay_filter
# Length of the filter
# Must be odd, or 0.5 samples of shift should be accounted for
N = 41
# Calculate the FIR filter
fir, tau_g = fractional_delay_filter(
mu=shift,
fs_hz=2.0e9, # must use 2.4e9 with a standalone HDAWG
f_bw_hz=900e6,
N=N,
)
# zero-pad the waveform. That assume the original waveform is zero at its extrema
wfm_padded = np.concatenate((np.zeros(N), wfm, np.zeros(N)))
# Use lfilter to filter the waveform with the FIR filter
wfm_shifted = lfilter(fir, 1.0, wfm_padded)
# Cut the now useless padding
# There is a N/2 group delay accounted for
wfm_shifted = wfm_shifted[3 * N // 2 : -N // 2]
Let's plot them!
plt.figure()
# Not shifted
plt.plot(time * 1e9, wfm, "x-", label="shift 0.0 samples")
# Shifted by half sample
plt.plot(time * 1e9, wfm_shifted, "x-", label=f"shift {shift:.1f} samples")
plt.xlabel("Time (ns)")
plt.ylabel("Amplitude (a.u.)")
plt.legend()
plt.show()
Such arrays of samples can be used in the experiment definition, see the LabOneQ User Manual at Create a Sampled Pulse from an Array of Sampling Points
Real-time HDAWG FIR filter¶
The HDAWG has a real-time FIR filter that we can use for this purpose (PC option needed).
Instead of shifting all the waveforms, we program it, and the entire output will be shifted. This is useful for deskewing signal lines.
The function laboneq.contrib.example_helpers.subsampling.get_delay_settings takes a delay as an input and calculates the FIR filter and the port_delay necessary to reach that delay. In addition, the function laboneq.contrib.example_helpers.subsampling.get_signal_calibration goes one step further and returns a SignalCalibration object that can be readily applied to a line.
Let's define a single pulse experiment:
@dsl.experiment(signals=["q0_flux"])
def experiment_subsampling_rt():
with dsl.acquire_loop_rt(count=1):
with dsl.section(name="play-flux-pulse_q0", trigger={"q0_flux": {"state": 1}}):
dsl.play(
"q0_flux",
pulse_library.gaussian(amplitude=1, length=7e-9),
)
# Map the ExperimentSignals "q0_flux" to the logical signal lines defined in the `DeviceSetup`
dsl.map_signal("q0_flux", "q0/flux")
experiment = experiment_subsampling_rt()
and let's calculate and add the correct calibration to add the delay
from laboneq.contrib.example_helpers.subsampling import get_signal_calibration
my_exp_calibration = Calibration()
my_exp_calibration["q0_flux"] = get_signal_calibration(
delay=1.95e-9, fs_hz=2.4e9
) # use 2.0e9 when the HDAWG is used with SHFs instruments
experiment.set_calibration(my_exp_calibration)
compiled_experiment = session.compile(experiment)
and eventually runs it on real hardware
_ = session.run()
The shift can be observed with a fast scope by triggering on the marker output.