See also

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

Cryoscope#

This notebook replicates the Cryoscope protocol in this paper. Here’s the arxiv link.

Cryoscope is short for cryogenic oscilloscope, and is used to sample baseband pulses. This can then be used to correct the pulse distortions.

The experiment in this tutorial is meant to be executed with a Qblox Cluster controlling a flux-tunable transmon system.

The experiments can also be executed using a dummy Qblox device that is created via an instance of the Cluster class, and is initialized with a dummy configuration. However, when using a dummy device, fake data will be generated and analyzed.

Hardware setup#

In this section we configure the hardware configuration which specifies the connectivity of our system.

Configuration file#

This is a template hardware configuration file for a single qubit system (we name the qubit q0), with a dedicated flux-control line.

The hardware setup is as follows, by cluster slot: - QCM-RF (Slot 6) - Drive line for q0 using fixed 80 MHz IF. - QCM (Slot 2) - Flux line for q0. - QRM-RF (Slot 8) - Shared readout line for q0 using a fixed LO set at 7.5 GHz.

Note that in the hardware configuration below the mixers are uncorrected, but for high fidelity experiments this should also be done for all the modules.

[1]:
import json

with open("configs/tuning_transmon_coupled_pair_hardware_config.json") as hw_cfg_json_file:
    hardware_cfg = json.load(hw_cfg_json_file)

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).

[2]:
# !qblox-pnp list
[3]:
cluster_ip = None  # To run this tutorial on hardware, fill in the IP address of the cluster here
cluster_name = "cluster0"

Connect to Cluster#

We now make a connection with the Cluster.

[4]:
from pathlib import Path

from qcodes.instrument import find_or_create_instrument

from qblox_instruments import Cluster, ClusterType

cluster0 = find_or_create_instrument(
    Cluster,
    recreate=True,
    name=cluster_name,
    identifier=cluster_ip,
    dummy_cfg=(
        {
            2: ClusterType.CLUSTER_QCM,
            4: ClusterType.CLUSTER_QRM,
            6: ClusterType.CLUSTER_QCM_RF,
            8: ClusterType.CLUSTER_QRM_RF,
        }
        if cluster_ip is None
        else None
    ),
)

Select the modules we’ll use for this experiment#

[5]:
def get_module_from_hardware_cfg(
    cluster: object, hardware_cfg: dict, module_type: str, match_index: int = 0
):
    matching_modules = [
        key
        for key, value in hardware_cfg["hardware_description"]["cluster0"]["modules"].items()
        if value["instrument_type"] == module_type
    ]
    module_nr = next(
        (
            i
            for i, module in enumerate(cluster.modules)
            if module.name.endswith(f"module{int(matching_modules[match_index])}")
        ),
        None,
    )
    return module_nr

Select the appropriate modules for this experiment: - A QCM baseband module for the flux line - A QCM-RF module for the XY drive line - A QRM-RF module for the readout

[6]:
flux_module = cluster0.modules[get_module_from_hardware_cfg(cluster0, hardware_cfg, "QCM")]
xy_module = cluster0.modules[get_module_from_hardware_cfg(cluster0, hardware_cfg, "QCM_RF")]
readout_module = cluster0.modules[get_module_from_hardware_cfg(cluster0, hardware_cfg, "QRM_RF")]

Experiment setup#

Quantum device settings#

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

In short, a QuantumDevice contains device elements where we save our found parameters.

[7]:
from quantify_scheduler.device_under_test.quantum_device import QuantumDevice

try:
    quantum_device = QuantumDevice.from_json_file("devices/transmon_device_2q.json")
except KeyError as error:
    if error.args[0].startswith("Another instrument has the name:"):
        print(f"Warning! {error.args[0]}, restart the kernel if you wish to redefine it.")
    else:
        raise
q0 = quantum_device.get_element("q0")
q1 = quantum_device.get_element("q1")
quantum_device.hardware_config(hardware_cfg)

Calibrate mixers#

[8]:
# Calibrate the mixer
xy_module.out0_lo_cal()
readout_module.out0_in0_lo_cal()

Configure measurement control loop#

We will use a MeasurementControl object for data acquisition as well as an InstrumentCoordinator for controlling the instruments in our setup.

The PlotMonitor is used for live plotting.

All of these are then associated with the QuantumDevice.

Configure measurement control loop#

We will use a MeasurementControl object for data acquisition as well as an InstrumentCoordinator for controlling the instruments in our setup.

The PlotMonitor is used for live plotting.

All of these are then associated with the QuantumDevice.

[9]:
import logging

from quantify_core.measurement.control import MeasurementControl
from quantify_core.visualization.pyqt_plotmon import PlotMonitor_pyqt as PlotMonitor
from quantify_scheduler.instrument_coordinator import InstrumentCoordinator
from quantify_scheduler.instrument_coordinator.components.qblox import ClusterComponent


def configure_measurement_control_loop(
    device: QuantumDevice, cluster: Cluster, live_plotting: bool = False
):
    meas_ctrl = find_or_create_instrument(MeasurementControl, recreate=True, name="meas_ctrl")
    ic = find_or_create_instrument(InstrumentCoordinator, recreate=True, name="ic")

    # Add cluster to instrument coordinator
    ic_cluster = ClusterComponent(cluster)
    ic.add_component(ic_cluster)

    if live_plotting:
        # Associate plot monitor with measurement controller
        plotmon = find_or_create_instrument(PlotMonitor, recreate=False, name="PlotMonitor")
        meas_ctrl.instr_plotmon(plotmon.name)

    # Associate measurement controller and instrument coordinator with the quantum device
    device.instr_measurement_control(meas_ctrl.name)
    device.instr_instrument_coordinator(ic.name)

    return (meas_ctrl, ic)


# Only create meas_ctrl, instrument_coordinator if they don't exist yet.
if "meas_ctrl" not in globals() and "instrument_coordinator" not in globals():
    meas_ctrl, instrument_coordinator = configure_measurement_control_loop(quantum_device, cluster0)
else:
    logging.debug(
        "meas_ctrl or instrument_coordinator already existed, they have not been reinstantiated."
    )

Set data directory#

This directory is where all of the experimental data as well as all of the post processing will go.

[10]:
import quantify_core.data.handling as dh

# Enter your own dataset directory here!
dh.set_datadir(Path("example_data").resolve())

Configure external flux control#

We need to have some way of controlling the external flux.

This can be done by setting an output bias on a QCM baseband module of the cluster which is then connected to the flux-control line.

Here we are nullifying the external flux on the qubit.

[11]:
# hardware_cfg["connectivity"]["graph"] contains a graph of how each physical output port
# connects to the device.
# Here we select the outputs that correspond to the flux, and then create a dict with
# a key:value pair `q*`:`cluster*.module*.out*_offset`
flux_settables = {
    element[1].split(":")[0]: eval(
        ".".join(element[0].split(".")[:2]) + f".out{element[0][-1]}_offset"
    )
    for element in hardware_cfg["connectivity"]["graph"]
    if element[1][-3:] == ":fl"
}

for flux_settable in flux_settables.values():
    flux_settable.inter_delay = 100e-9  # Delay time in seconds between consecutive set operations.
    flux_settable.step = 0.3e-3  # Stepsize in V that this Parameter uses during set operation.
    flux_settable()  # get before set to avoid jumps
    flux_settable(0.0)

Experiment#

As in the single qubit tuneup tutorial, the sweep setpoints for all experiments in this section are only examples. The sweep setpoints should be changed to match your own system. In this section we assume that each individual qubit has already been characterized, and that they have been biased to their sweetspots.

The Cryoscope method allows us to “capture” the flux pulse similar to an oscilloscope. It does this by first setting up two Ramsey-style experiments in which the gap between the two pi pulses is fixed: one where the second pi half pulse has the same phase, and another where the second pi half pulse is 90 degrees phase shifted. This allows us to measure the phase of the qubit on the equator of the Bloch sphere. In between the two pi half pulses, a flux pulse of small incremental duration is played in each iteration of the Ramsey style experiment, while the rest of the time between the pip half pulses is just idle time.

The flux pulse changes the qubit frequency while it is played, and the time integral of this frequency change gives the total phase accrued by the qubit. If we measure this cumulative phase for small incremental changes in the duration of the flux pulse, then we can effectively work out the frequency change in the qubit over time as the flux pulse is played by taking the derivative of the phase accrued vs time. Then using an already measured flux arc (qubit frequency vs flux), we can convert this frequency change over time to the actual flux seen by the qubit over time.

[12]:
import numpy as np

from quantify_scheduler.device_under_test.quantum_device import DeviceElement
from quantify_scheduler.operations.gate_library import X90, Y90, Measure, Reset, X
from quantify_scheduler.operations.pulse_library import IdlePulse, SquarePulse
from quantify_scheduler.schedules.schedule import Schedule


def cryoscope_sched(
    qubit: DeviceElement,
    time_axis: np.ndarray,
    x_or_y_axis: np.ndarray,
    amplitude: float = 0.1,
    start_pad: float = 40e-9,
    end_pad: float = 100e-9,
    repetitions: int = 1,
) -> Schedule:
    """
    Generate a schedule for performing a Cryoscope experiment.

    Parameters
    ----------
    qubit
        The name of the qubit e.g., :code:`"q0"` to perform the experiment on.
    time_axis
        The time axis for which the cryoscope experiment is run.
    x_or_y_axis
        Defines the pi half pulse after the wait time as an X90 or Y90.
    amplitude
        The amplitude of the flux pulse.
    start_pad
        The starting pad time of the flux pulse.
    end_pad
        The ending pad time of the flux pulse.
    repetitions
        The amount of times the Schedule will be repeated.

    Returns
    -------
    :
        An experiment schedule.

    """
    schedule = Schedule("Cryoscope", repetitions)

    # Calculate the additional time required to make
    # one iteration take a multiple of 4 ns to complete.
    idle_pulse_time = (
        4e-9
        - (
            qubit.reset.duration()
            + start_pad
            + time_axis[-1]  # Gap between pi half pulses excluding padding.
            + end_pad
            + 2 * qubit.rxy.duration()
            + qubit.measure.pulse_duration()
        )
        % 4e-9
    )

    # Create a dict x_or_y_op that assigns an X or Y gate to the binary keys
    x_or_y_op = {0: X90(qubit.name), 1: Y90(qubit.name)}

    # This IdlePulse is needed to have a starting point relative
    # to which a relative time can be assigned to the Reset pulse.
    schedule.add(IdlePulse(4e-9))
    # Loop through the time axis. The last point is not used because
    # it is used for the calibration points later.
    for i, (time, x_or_y) in enumerate(zip(time_axis[:-2], x_or_y_axis[:-2])):
        # Wait for additional time calculated above and then Reset the qubit.
        schedule.add(Reset(qubit.name), label=f"Reset {i}", rel_time=idle_pulse_time)
        # Move the qubit to the equator on the bloch sphere.
        pi_half = schedule.add(X90(qubit.name))
        if time > 0:
            # Add the flux pulse of time t to the schedule
            schedule.add(
                SquarePulse(
                    amp=amplitude, duration=time, port=qubit.ports.flux(), clock="cl0.baseband"
                ),
                ref_op=pi_half,
                rel_time=start_pad,
            )
        # Wait for the gap time and play the second pi half pulse.
        # This pi half pulse is either an X90 or Y90.
        schedule.add(
            x_or_y_op[x_or_y], ref_op=pi_half, rel_time=start_pad + time_axis[-1] + end_pad
        )
        # Measure the qubit.
        schedule.add(Measure(qubit.name, acq_index=i), label=f"Measurement {i}")

    # Calibration points measured by preparing ground and excited states.
    schedule.add(Reset(qubit.name), label="Reset Cal 0")
    schedule.add(Measure(qubit.name, acq_index=i + 1), label="Calibration 0")
    schedule.add(Reset(qubit.name), label="Reset Cal 1")
    schedule.add(X(qubit.name))
    schedule.add(Measure(qubit.name, acq_index=i + 2), label="Calibration 1")

    return schedule

Create a CryoscopeAnalysis class#

This class extends the class SingleQubitTimedomainAnalysis from quantify_core.analysis.single_qubit_timedomain: - Specify that the run() method uses calibration points - Extend the process_data() method to populate self.dataset_processed with an xarray dataset: - coords: "Time (ns)" - axis: "frequency_change" - axis: "reconstructed_phi" - Add method create_figures()

[13]:
from typing import Callable

import matplotlib.pyplot as plt
from scipy.signal import savgol_filter
from xarray.core.dataset import Dataset

from quantify_core.analysis.single_qubit_timedomain import SingleQubitTimedomainAnalysis
from quantify_core.visualization.mpl_plotting import (
    set_suptitle_from_dataset,
    set_xlabel,
    set_ylabel,
)


class CryoscopeAnalysis(SingleQubitTimedomainAnalysis):
    """
    Analysis class for the Cryoscope experiment.

    This class extends the SingleQubitTimedomainAnalysis class, which in turn extends the
    BaseAnalysis class:
    - BaseAnalysis.run() runs all steps in the AnalysisSteps class:
        1. process_data                  # Empty
        2. run_fitting                   # Empty
        3. analyze_fit_results           # Empty
        4. create_figures                # Empty
        5. adjust_figures                # Defined
        6. save_figures                  # Defined
        7. save_quantities_of_interest   # Defined
        8. save_processed_dataset        # Defined
        9. save_fit_results              # Defined
    - SingleQubitTimedomainAnalysis extends BaseAnalysis:
        - run() defines self.calibration_points
        - process_data() populates dataset_processed.S21 and dataset_processed.pop_exc
    - CryoscopeAnalysis extends SingleQubitTimedomainAnalysis:
        - process_data() is extended by calculating:
            - x_vals, y_vals
            - unfiltered_phase
            - filtered_phase
            - unwrapped_phase
            - phase_derivative
            - frequency_change
            - reconstructed_phi
        - create_figures() is defined
    """

    def __init__(  # noqa: D107
        self,
        dataset: Dataset = None,
        tuid: str = None,
        label: str = "",
        settings_overwrite: dict = None,
        plot_figures: bool = True,
        frequency_change_to_flux: Callable = None,
        savgol_filter_params: dict = {"window_length": 2, "polyorder": 1},
    ) -> None:
        super().__init__(dataset, tuid, label, settings_overwrite, plot_figures)
        self.frequency_change_to_flux = frequency_change_to_flux
        self.savgol_filter_params = savgol_filter_params

    def run(self):
        """
        Run the SingleQubitTimedomainAnalysis with calibration_points.

        This removes the calibration points (last two) and converts
        the rest of the IQ values to a population (pop_exc).
        """
        return super().run(calibration_points=True)

    def process_data(self):  # noqa: D102
        super().process_data()

        # Translate and scale the populations from X and Y measurements
        # from the range [0,1] to [-1,1]
        x_vals = 2 * (self.dataset_processed["pop_exc"].values[:-2:2] - 0.5)
        y_vals = 2 * (self.dataset_processed["pop_exc"].values[1:-2:2] - 0.5)

        # Find phase from the X,Y coordinates
        unfiltered_phase = np.angle(x_vals + 1j * y_vals)
        # Store the unfiltered phase for debugging purposes
        self.dataset_processed["unfiltered_phase"] = (["t"], unfiltered_phase)

        # First unwrap the phase
        unwrapped_phase = np.unwrap(unfiltered_phase)
        # Store the unwrapped phase for debugging purposes
        self.dataset_processed["unwrapped_phase"] = (["t"], unwrapped_phase)

        # Use the savgol_filter to both filter and take the derivative of the unwrapped phase.
        # The parameters of the savgol_filter may need to be changed after this is run on an
        # actual device.
        filtered_phase_derivative = savgol_filter(
            unwrapped_phase,
            window_length=self.savgol_filter_params["window_length"],
            polyorder=self.savgol_filter_params["polyorder"],
            deriv=1,
        )

        # Store the filtered phase derivative for debugging purposes
        self.dataset_processed["filtered_phase_derivative"] = (["t"], filtered_phase_derivative)

        # Rescale the filtered phase derivative to units of frequency change
        frequency_change = filtered_phase_derivative / (
            self.dataset_processed.x1[2].values - self.dataset_processed.x1[0].values
        )
        # Store the frequency change
        self.dataset_processed["frequency_change"] = (["t"], frequency_change)

        # if frequency_change_to_flux is provided, convert the frequency to flux (reconstructed_phi) and plot this.
        if self.frequency_change_to_flux is not None:
            reconstructed_phi = self.frequency_change_to_flux(frequency_change)
            self.dataset_processed["reconstructed_phi"] = (["t"], reconstructed_phi)
        else:
            print(
                "frequency_change_to_flux was not provided, reconstructed_phi has not been calculated."
            )

    def create_figures(self):  # noqa: D102
        fig, ax = plt.subplots()
        fig_id = "Cryoscope"
        self.figs_mpl[fig_id] = fig
        self.axs_mpl[fig_id] = ax

        # if frequency_change_to_flux is provided, plot it.
        if self.frequency_change_to_flux is not None:
            ax.plot(
                1e9 * self.dataset_processed["reconstructed_phi"].coords["t"],
                self.dataset_processed["reconstructed_phi"],
                label="Measured",
            )
            set_ylabel(r"Reconstructed $\Phi/\Phi_0$")
        # if frequency_change_to_flux is not provided, plot the frequency change.
        else:
            ax.plot(
                1e9 * self.dataset_processed["frequency_change"].coords["t"],
                self.dataset_processed["frequency_change"] / 1e6,
                label="Measured",
            )
            set_ylabel("Frequency change (MHz)")
        set_xlabel("Time (ns)")
        set_suptitle_from_dataset(fig, self.dataset)

Define the flux dependence of the qubit frequency#

This is qubit specific, and assumes that the qubit has already been characterized. In the simplest case of a symmetric qubit (i.e. one with identical JJs in the SQUID), only \(E_\text{J}\) and \(E_\text{C}\) need to be provided.

[14]:
def flux_to_frequency_change(flux: np.ndarray):
    """
    Convert frequency to flux.

    Currently this assumes a symmetric qubit and fixed parameters.

    Args:
    ----
        flux (np.ndarray): an array of flux values, in units of flux quantum

    """
    h = 1 / (2 * np.pi)
    e_c = h * 300e6
    e_j = (h * q0.clock_freqs.f01() + e_c) ** 2 / (8 * e_c)
    return (1 / h) * (
        np.sqrt(8 * e_j * e_c * np.abs(np.cos(np.pi * flux))) - e_c
    ) - q0.clock_freqs.f01()


def frequency_change_to_flux(freq_change: np.ndarray):
    r"""
    Convert flux to frequency.

    \Phi(\tau) = \pm \Phi_0/\pi \arccos((Ec+h*freq_change)**2/(8*Ej*Ec)),
    or in units of flux quantum:
    |\Phi(\tau)/\Phi_0| = arccos((Ec+h*freq_change)**2/(8*Ej*Ec))/pi.
    """
    # Assuming the qubit is parked at the maximum frequency, we can clip positive frequency changes to zero
    freq_change = np.clip(freq_change, None, 0)
    h = 1 / (2 * np.pi)
    e_c = h * 300e6
    e_j = (h * q0.clock_freqs.f01() + e_c) ** 2 / (8 * e_c)
    return (
        np.arccos((h * (q0.clock_freqs.f01() + freq_change) + e_c) ** 2 / (8 * e_j * e_c)) / np.pi
    )

Define the shape of the flux pulse#

  • When testing with a dummy cluster, this should mimic the distortions of the cables.

  • When running on a real device, this should be a true step function.

[15]:
from qblox_instruments.simulations import exponential_overshoot_correction


def flux_pulse_shape(t: np.ndarray):
    """
    Shape of the flux pulse send to the qubit.

    Predistortion calculated using the Qblox simulator for the exponential overshoot correction
    using values in the cluster to simulate the real time predistortions,
    and an exponential overshoot distortion is added to the signal
    that is fixed to simulate a real distortion.
    """
    signal = 0.1 * (1 + 0.05 * np.exp(-t / 72e-9))
    signal[np.where(t <= 0)] = 0.0
    return exponential_overshoot_correction(
        signal,
        flux_module.out0_exp0_amplitude(),
        max(flux_module.out0_exp0_time_constant(), 6),
    )

Define fake data when running on a dummy cluster#

[16]:
def get_fake_cryoscope_data():
    """Convert the flux pulse shape to cryoscope data."""
    # Convert the flux pulse shape to frequency change.
    freq_change = flux_to_frequency_change(flux_pulse_shape(time_axis()[::2]))
    freq_change -= freq_change[0]
    cumulative_phase_change = np.cumsum(2 * np.pi * freq_change) * np.diff(time_axis()[:4:2])[0]
    # Define IQ values for ground state and excited state
    ground_state = 0
    excited_state = 1 + 5.0j
    # compute the IQ values for the X measurements
    x_measurements = (np.cos(cumulative_phase_change) * 0.5 + 0.5) * excited_state
    # compute the IQ values for the Y measurements
    y_measurements = (np.sin(cumulative_phase_change) * 0.5 + 0.5) * excited_state

    # Assign the last of the x and y measurements
    # to ground and excited state IQ values
    x_measurements[-1] = ground_state
    y_measurements[-1] = excited_state

    # reshape to the way that an actual measurement would look like
    result = np.concatenate((x_measurements, y_measurements)).reshape(2, -1).T.flatten()

    return [np.real(result), np.imag(result)]


def fake_get():
    """Run the previous get function but only return fake data."""
    gettable.old_get()
    return get_fake_cryoscope_data()

Define the schedule gettables for the measurement#

[17]:
from qcodes import ManualParameter

from quantify_scheduler.gettables import ScheduleGettable

time_axis = ManualParameter(name="time_axis", unit="(ns)", label="Time")
time_axis.batched = True

x_or_y = ManualParameter(name="x_or_y", unit="", label="axis")
x_or_y.batched = True

cryoscope_kwargs = {
    "time_axis": time_axis,
    "x_or_y_axis": x_or_y,
    "amplitude": 0.156,
    "qubit": quantum_device.get_element("q0"),
}

gettable = ScheduleGettable(
    quantum_device,
    schedule_function=cryoscope_sched,
    schedule_kwargs=cryoscope_kwargs,
    real_imag=True,
    batched=True,
)

# replace the get method for the gettable in case the cluster is a dummy
if cluster_ip is None:
    gettable.old_get = gettable.get
    gettable.get = fake_get

# Set the number of repetitions (or averages)
quantum_device.cfg_sched_repetitions(1)

Define the time spacing between pulses#

This allows for transients to decay.

[18]:
from quantify_scheduler.backends.qblox import constants

constants.PULSE_STITCHING_DURATION = 16e-9

Measure the phase vs the duration of the detuning flux pulse#

[19]:
time_axis_setpoints = np.arange(-5e-9, 200e-9, 1e-9)
x_or_y_setpoints = [0, 1]
meas_ctrl.settables([x_or_y, time_axis])
meas_ctrl.setpoints_grid([x_or_y_setpoints, time_axis_setpoints])
# Pass the ScheduleGettable class with schedule_function=cryoscope_sched on to the measurement control
meas_ctrl.gettables(gettable)
hw_cfg = quantum_device.hardware_config()
[20]:
if hw_cfg.get("hardware_options", {}).get("distortion_corrections") is not None:
    # In case the cells below have already been run, we need to reset the distortion_corrections hardware option again.
    print(
        f"Predistortion filter removed. Previous value: {hw_cfg['hardware_options'].pop('distortion_corrections', None)}"
    )
# Run the measurement control loop
cryoscope_ds = meas_ctrl.run(f"Cryoscope Experiment A {cryoscope_kwargs['amplitude']}")
# Analyze the date from the measurement
if cluster_ip is None:
    savgol_filter_params = {"window_length": 2, "polyorder": 1}
else:
    savgol_filter_params = {"window_length": 10, "polyorder": 1}
cryoscope_result = CryoscopeAnalysis(
    dataset=cryoscope_ds,
    label="Cryoscope",
    settings_overwrite={"mpl_transparent_background": False},
    frequency_change_to_flux=frequency_change_to_flux,
    savgol_filter_params=savgol_filter_params,
).run()
cryoscope_result.display_figs_mpl()
Starting batched measurement...
Iterative settable(s) [outer loop(s)]:
         --- (None) ---
Batched settable(s):
         x_or_y, time_axis
Batch size limit: 410

/usr/local/lib/python3.9/site-packages/quantify_scheduler/backends/types/qblox.py:1220: ValidationWarning: Setting `auto_lo_cal=on_lo_interm_freq_change` will overwrite settings `dc_offset_i=0.0` and `dc_offset_q=0.0`. To suppress this warning, do not set either `dc_offset_i` or `dc_offset_q` for this port-clock.
  warnings.warn(
/usr/local/lib/python3.9/site-packages/quantify_scheduler/backends/types/qblox.py:1235: ValidationWarning: Setting `auto_sideband_cal=on_interm_freq_change` will overwrite settings `amp_ratio=1.0` and `phase_error=0.0`. To suppress this warning, do not set either `amp_ratio` or `phase_error` for this port-clock.
  warnings.warn(
../../../_images/applications_quantify_transmon_cryoscope_41_3.png

Calculate the predistortion needed to send a square pulse#

In order to find the optimal parameters for the overshoot correction, we define the residual as the difference between exponential_overshoot_correction() applied to the measured \(\Phi/\Phi_0\), and the ideal pulse shape.

[21]:
from copy import deepcopy


# Define the residual for the fitting function
def residual(params: list, distorted_data: np.ndarray):
    # Params: [amp,tau,scale]
    distorted_data = deepcopy(distorted_data.values)
    # Remove the offset: subtract the value of the first data point from the data array
    distorted_data -= distorted_data[0]
    # Scale the data by scaling parameter
    distorted_data /= 2 * params[2]
    # Define the ideal pulse as a step function with height 0.5 starting at t=0
    ideal = 0.5 * np.ones(len(distorted_data))
    ideal[:5] = 0

    return exponential_overshoot_correction(distorted_data, params[0], params[1]) - ideal

Find a best fit for the predistortion filter that would result in a clean pulse at the qubit using the least squares method.

[22]:
from scipy.optimize import least_squares

time = cryoscope_result.dataset_processed.t
# Convert xarray.DataArray to np.array, store the results separately in memory
distorted_data = cryoscope_result.dataset_processed.reconstructed_phi
# Find the overshoot correction parameters that best approximate the ideal pulse shape
res = least_squares(residual, x0=(0.33, 73, 0.1), args=(distorted_data,))
/builds/qblox/packages/software/qblox_instruments_docs/qblox_instruments/qblox_instruments/simulations/predistortions.py:80: UserWarning: Qblox simulator plugin WARNING: Output will be clipped.The result of the simulation cannot be trusted.
  warnings.warn(

Plot the measured pulse shape versus the corrected and predistorted pulse shapes

[23]:
ideal = res.x[2] * np.ones(len(distorted_data))
ideal[:5] = 0

fig, ax = plt.subplots()
ax.plot(time, distorted_data, label="measured")
ax.plot(
    time, exponential_overshoot_correction(distorted_data, res.x[0], res.x[1]), label="corrected"
)
ax.plot(time, exponential_overshoot_correction(ideal, res.x[0], res.x[1]), label="predistorted")
if cluster_ip is None:
    ax.set_ylim(0.9 * res.x[2], 1.1 * res.x[2])
ax.set_xlabel("Time (ns)")
ax.set_ylabel(r"Reconstructed $\Phi$")
ax.legend()
ax.grid()
../../../_images/applications_quantify_transmon_cryoscope_47_0.png

Repeat the measurement of the phase vs the duration of the detuning flux pulse#

This time with the predistortion filter applied.

[24]:
from quantify_scheduler.backends.types.qblox import QbloxHardwareDistortionCorrection

# Set the hardware distortions for the relevant flux line.
hw_cfg["hardware_options"]["distortion_corrections"] = {
    "q0:fl-cl0.baseband": QbloxHardwareDistortionCorrection(exp0_coeffs=[res.x[1], res.x[0]])
}

quantum_device.hardware_config(hw_cfg)

cryoscope_ds = meas_ctrl.run(f"Cryoscope Experiment A {cryoscope_kwargs['amplitude']}")

cryoscope_result = CryoscopeAnalysis(
    dataset=cryoscope_ds,
    label="Cryoscope",
    settings_overwrite={"mpl_transparent_background": False},
    frequency_change_to_flux=frequency_change_to_flux,
).run()
cryoscope_result.display_figs_mpl()
Starting batched measurement...
Iterative settable(s) [outer loop(s)]:
         --- (None) ---
Batched settable(s):
         x_or_y, time_axis
Batch size limit: 410

/usr/local/lib/python3.9/site-packages/quantify_scheduler/backends/types/qblox.py:1235: ValidationWarning: Setting `auto_sideband_cal=on_interm_freq_change` will overwrite settings `amp_ratio=1.0` and `phase_error=0.0`. To suppress this warning, do not set either `amp_ratio` or `phase_error` for this port-clock.
  warnings.warn(
../../../_images/applications_quantify_transmon_cryoscope_49_3.png