See also

A Jupyter notebook version of this tutorial can be downloaded here.

image0

Correcting for signal distortions due to Bias Tee#

Introducing BIAS Tee#

The Bias-T (Bias Tee) is a diplexer consisting of an inductor and a capacitor. As shown below, the capacitor port (also known as the RF port) allows the passage of high-frequency components while attenuating the low-frequency elements of the signal. In contrast, the inductor port (also known as the DC port) allows a DC signal to pass through. The output port combines the RF and DC inputs.

image1

A Bias-T is essential for spin qubit systems, especially when combining a small RF signal with a large DC offset. This configuration allows for significant attenuation of the RF line to minimize noise, while the DC component is filtered through a series of filters and the Bias-T itself.

However, the Bias-T, being a filter, inevitably introduces signal distortion that can compromise the waveform sent to the qubit. In this demo we showcase how to mitigate these undesirable effects, which manifest as an exponential decay visible on both long and short timescales (relative to the Bias-T’s time constant). We will present two optimized solutions, one for each scenario.

Additionally, we will validate these improvements by applying the corrections to a real Bias-T and comparing the output on the oscilloscope with our simulated results.

[1]:
from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np
from bias_tee_utils import (
    bias_tee_correction,
    bias_tee_distort,
    get_axis,
    plot_waveform_and_rectangle,
)

import quantify_core.data.handling as dh
from quantify_scheduler import QuantumDevice, Schedule
from quantify_scheduler.backends.graph_compilation import SerialCompiler
from quantify_scheduler.operations import (
    NumericalPulse,
    RampPulse,
    SquarePulse,
)
from quantify_scheduler.operations.pulse_compensation_library import PulseCompensation

from utils import initialize_hardware, run_schedule

Scan for clusters#

We scan for the available devices connected via ethernet using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).

!qblox-pnp list

[2]:
device_path = "devices/spin_device_2q.json"
config_path = "configs/tuning_spin_coupled_pair_hardware_config.json"
dh.set_datadir(dh.default_datadir())  # Enter your own dataset directory here!
Data will be saved in:
/root/quantify-data

Quantum device & hardware settings#

Here we initialize our QuantumDevice and our qubit parameters, checkout this tutorial for further details.

In short, a QuantumDevice contains device elements where we save our found parameters. Here we are loading a template for 2 spin qubits, but we will only use qubit 0.

[3]:
quantum_device = QuantumDevice.from_json_file(device_path)
qubit = quantum_device.get_element("q0")
quantum_device.hardware_config.load_from_json_file(config_path)
_, instrument_coordinator, cluster = initialize_hardware(quantum_device, ip=None)

Schedule definition#

The schedule_function consists of two schedules:

  • The uncompensated_sched schedule plays the instrument’s output directly without any correction.

  • In contrast, the compensated_sched schedule employs the PulseCompensation operation with the net-zero protocol to mitigate the Bias-T’s long-term decay effects. This operation calculates the required compensation pulse parameters, including amplitude and duration, and inserts the compensation pulse after the main schedule body has executed.

[4]:
def schedule_function(
    port: str,
    amp_points: np.ndarray,
    square_dur: float,
    ramp_dur: float,
    second_square_dur: float,
    reps: int = 1,
):
    # Create schedules for uncompensated and compensated pulses
    uncompensated_sched = Schedule("uncompensated", repetitions=reps)
    compensated_sched = Schedule("compensated", repetitions=reps)

    # Loop through each amplitude set point
    for amp in amp_points:
        # Create a pulse sequence schedule
        pulse_sequence = Schedule("pulse_sequence")

        # Add a square pulse with zero amplitude
        pulse_sequence.add(SquarePulse(amp=0, duration=square_dur, port=port))

        # Add a ramp pulse with the current amplitude
        pulse_sequence.add(RampPulse(amp=amp, offset=0, duration=ramp_dur, port=port))

        # Add a second square pulse with the current amplitude
        pulse_sequence.add(SquarePulse(amp=amp, duration=second_square_dur, port=port))

        # Add the pulse sequence to the uncompensated schedule
        uncompensated_sched.add(pulse_sequence)

        # Add the pulse sequence with net-zero protocol to the compensated schedule
        compensated_sched.add(
            PulseCompensation(
                body=pulse_sequence,
                max_compensation_amp={port: 0.8},
                time_grid=4e-9,
                sampling_rate=1e9,
            )
        )

    return uncompensated_sched, compensated_sched

Setting parameters for schedule_function

[5]:
decay = 17000  # Time constant for the Bias-T distortion in ns.
amp_set_points = np.linspace(0.02, 0.9, 10)  # Define an array of amplitude set points
square_duration = 300e-9  # Duration of the first square pulse in ns.
ramp_duration = 400e-9  # Duration of the ramp pulse in ns.
second_square_duration = 200e-9  # Duration of the second square pulse in ns.
repetitions = int(1e6)  # repetitions is 1e6 so that we can see it on the scope
port = qubit.ports.gate()  # Port to use for the pulse sequence

Running the uncompensated schedule#

[6]:
uncompensated_sched, _ = schedule_function(
    port,
    amp_set_points,
    square_duration,
    ramp_duration,
    second_square_duration,
    repetitions,
)
[7]:
# Let’s compile the uncompensated schedule.
compiler = SerialCompiler("my_compiler", quantum_device)
compiled_schedule_uncomp = compiler.compile(
    uncompensated_sched
)  # we are going to use it later on a real bias-T

Plot the waveform

[8]:
x, y = get_axis(compiled_schedule_uncomp)
plot_waveform_and_rectangle(x, y, decay, second_square_duration, amp_set_points)
../../../_images/applications_quantify_spin_net_zero_15_0.png
../../../_images/applications_quantify_spin_net_zero_15_1.png

The second plot reveals a clear decay, both in the long-term mean value (indicated by the red dashed line) and on a shorter timescale within the highlighted region.

To verify this Bias-T behavior, we use a real device with a 17 µs decay (matching the simulation parameters).

We connect the QCM output to the Bias-T’s RF port and then connect the combined port to an oscilloscope. Then we run the uncompensated complied schedule compiled_schedule_uncomp.

[9]:
_ = run_schedule(compiled_schedule_uncomp, quantum_device)

Scope output#

The output from the oscilloscope is shown below. Note that the effects of the Bias-T on both short and long pulses are clearly visible, closely matching our results from the simulation.

image2

[10]:
instrument_coordinator.stop()

Compensating for long-term decay: `PulseCompensation <https://quantify-os.org/docs/quantify-scheduler/v0.22/autoapi/quantify_scheduler/operations/index.html#quantify_scheduler.operations.PulseCompensation>`__#

To see the effect of `PulseCompensation <https://quantify-os.org/docs/quantify-scheduler/v0.22/autoapi/quantify_scheduler/operations/index.html#quantify_scheduler.operations.PulseCompensation>`__ operation, we compile and run the compensated_sched part of the schedule_function.

[11]:
_, compensated_sched = schedule_function(
    port,
    amp_set_points,
    square_duration,
    ramp_duration,
    second_square_duration,
    repetitions,
)

# Let’s compile the schedule.
compiler = SerialCompiler("my_compiler", quantum_device)
compiled_schedule_net_zero = compiler.compile(
    compensated_sched
)  # we are going to use it later on a real bias-T

x, y = get_axis(compiled_schedule_net_zero)

# Plot the waveform
new_y = plot_waveform_and_rectangle(x, y, decay, second_square_duration, amp_set_points)
../../../_images/applications_quantify_spin_net_zero_21_0.png
../../../_images/applications_quantify_spin_net_zero_21_1.png

To confirm, we run the complied schedule compiled_schedule_net_zero with the real Bias-T, and observe the output at the scope.

[12]:
_ = run_schedule(compiled_schedule_net_zero, quantum_device)

Scope output#

image3

The output figure above shows that the Net-zero protocol effectively compensates for the DC decay caused by the Bias-T, leaving only the short pulse distortion to be corrected (as seen in the highlighted region).

Mitigating short-term decay#

To perfectly account for the Bias-T, we use the function bias_tee_correction. It pre-distorts the signal to compensate for short-term decay. Hence, the resulting in the following waveform (shown in the plot).

[13]:
pre_distorded_signal = bias_tee_correction(y, decay)
plt.figure()
plt.title("Predistorded net zero signal output by the instrument")
plt.plot(x, pre_distorded_signal)
plt.axhline(0, c="red", linestyle="--")
plt.xlabel("Time (seconds)")
plt.ylabel("Amplitude ")
[13]:
Text(0, 0.5, 'Amplitude ')
../../../_images/applications_quantify_spin_net_zero_26_1.png

Finally, we send the pre-distorted waveform through a Bias-T, that is simulated using bias_tee_distort function. The resulted signal output is free from any Bias-T defects.

[14]:
final_compensated_signal = bias_tee_distort(pre_distorded_signal, decay)
plt.figure()
plt.title("Final output signal by the instrument after Bias-T")
plt.plot(x, final_compensated_signal, label="Final output after correction for Bias-T")
plt.axhline(0, c="red", linestyle="--")
plt.xlabel("Time (seconds)")
plt.ylabel("Amplitude ")
[14]:
Text(0, 0.5, 'Amplitude ')
../../../_images/applications_quantify_spin_net_zero_28_1.png

Using this filter alone, one can also correct long-term decay. But this may clip the outputs, which is not recommended. Combining the Net-zero protocol with the Bias-T filter enables you to deliver the exact signal required for the QPU, without clipping the output.

To verify the pre-distortion with the real Bias-T, we initialize another schedule, final_compensated_sched. Here, we add the pre_distorded_signal using NumericalPulse from quantify_scheduler.operations.pulse_library. This allows us to play any arbitrary waveform.

[15]:
# Initialize the final schedule with a single repetition
final_compensated_sched = Schedule("final", repetitions=int(1e6))
t_x = (x * 1e9 // 4).astype(int) * 4  # sampling the time at 4 ns resolution

# Add a NumericalPulse to the schedule with the pre-distorted signal
final_compensated_sched.add(
    NumericalPulse(
        samples=pre_distorded_signal,  # The pre-distorted signal waveform
        t_samples=t_x * 1e-9,  # Time samples in seconds
        port=qubit.ports.gate(),  # Port where the pulse is applied
    )
)

compiler = SerialCompiler("my_compiler", quantum_device)
# Compile the schedule for execution
compiled_schedule = compiler.compile(final_compensated_sched)
/builds/qblox/packages/software/qblox_instruments_docs/.venv/lib/python3.9/site-packages/scipy/interpolate/_interpolate.py:712: RuntimeWarning: invalid value encountered in divide
  slope = (y_hi - y_lo) / (x_hi - x_lo)[:, None]
[16]:
_ = run_schedule(compiled_schedule, quantum_device)

Scope output#

image3

As shown in the output above, the final waveform is free from distortion in both the short and long pulse regimes.

If you want to try it with different Bias-T, make sure you change the time-constant variable decay accordingly in the above script.

[17]:
instrument_coordinator.stop()