See also

The complete source code of this tutorial can be found in

Conditional Reset.ipynb

Tutorial: Conditional Reset#

In this tutorial, we show how to perform a conditional reset using the conditional control flow framework. A conditional reset consists of measuring the state of a qubit, and then:

  • sending a pulse to rotate the qubit to the ground state in case the qubit is found to be in an excited state

  • not sending any pulse if the qubit is found to be in the ground state

This conditional reset is potentially much faster than an idle qubit reset (i.e. waiting for a time \(\gg \tau_1\)). Quantify discriminates between excited and ground state at the Measure operation using a thresholded acquisition, and uses a default \(\pi\) pulse to set the qubit to its ground state. In this tutorial, we demonstrate conditional reset using a Qblox cluster that contains a readout module (QRM_RF), responsible for the measurement of the qubit, and a control module (QCM_RF), responsible for conditionally sending out the \(\pi\) pulse.

To run a conditional reset, we perform the following steps:

  1. Set up the quantum device, dummy hardware, and hardware configuration.

  2. Configure thresholded acquisition parameters to separate the \(|0\rangle\) and \(|1\rangle\) states.

  3. Verify that these parameters are set correctly.

  4. Run a conditional reset.

Note

Currently, the conditional reset is only implemented for the Qblox hardware.

Initial Setup#

We follow here the same setup as in the Tutorial: Running an Experiment tutorial.

First, we define a single transmon qubit as an element (BasicTransmonElement) of the QuantumDevice and populate the parameters with some reasonable values:

See also

If you want to learn more about how to set up the QuantumDevice and hardware configuration, please see our other tutorials, in particular Tutorial: Running an Experiment and Tutorial: Compiling to Hardware.

Hide code cell content
from qblox_instruments import Cluster, ClusterType
import tempfile

from quantify_core.data import handling as dh
from quantify_core.measurement.control import MeasurementControl
from quantify_scheduler import BasicTransmonElement, InstrumentCoordinator, QuantumDevice
from quantify_scheduler.qblox import ClusterComponent

measurement_control = MeasurementControl("measurement_control")
instrument_coordinator = InstrumentCoordinator("instrument_coordinator")

# Create a temporary directory for this tutorial
temp_dir = tempfile.mkdtemp()

# First, don't forget to set the data directory!
dh.set_datadir(temp_dir)

# Device parameters
ACQ_DELAY = 100e-9
FREQ_01 = 4e9
READOUT_AMP = 0.1
READOUT_FREQ = 4.3e9
PI_PULSE_AMP = 0.15
LO_FREQ_QUBIT = 3.9e9
LO_FREQ_READOUT = 4.5e9

single_qubit_device = QuantumDevice("single_qubit_device")

q0 = BasicTransmonElement("q0")
single_qubit_device.add_element(q0)

# Assign device parameters to transmon element
q0.measure.pulse_amp(READOUT_AMP)
q0.clock_freqs.readout(READOUT_FREQ)
q0.clock_freqs.f01(FREQ_01)
q0.measure.acq_delay(ACQ_DELAY)
q0.rxy.amp180(PI_PULSE_AMP)

Next, we connect to a dummy Cluster. If you are connecting to an actual cluster, you would provide the identifier argument (the IP address, device name or serial number) instead of the dummy_cfg argument.

Hide code cell content
cluster = Cluster(
    "cluster",
    dummy_cfg={
        1: ClusterType.CLUSTER_QRM_RF,
        2: ClusterType.CLUSTER_QCM_RF,
    },
)

ic_cluster = ClusterComponent(cluster)

instrument_coordinator.add_component(ic_cluster)

single_qubit_device.instr_instrument_coordinator(instrument_coordinator.name)

Finally, we define the hardware configuration:

Hide code cell content
hardware_cfg = {
    "version": "0.2",
    "config_type": "quantify_scheduler.backends.qblox_backend.QbloxHardwareCompilationConfig",
    "hardware_description": {
        f"{cluster.name}": {
            "instrument_type": "Cluster",
            "modules": {
                1: {"instrument_type": "QRM_RF"},
                2: {"instrument_type": "QCM_RF"},
            },
            "ref": "internal",
        }
    },
    "hardware_options": {
        "modulation_frequencies": {
            "q0:res-q0.ro": {"lo_freq": LO_FREQ_READOUT},
            "q0:mw-q0.01": {"lo_freq": LO_FREQ_QUBIT},
        }
    },
    "connectivity": {
        "graph": [
            (f"{cluster.name}.module1.complex_output_0", "q0:res"),
            (f"{cluster.name}.module1.complex_input_0", "q0:res"),
            (f"{cluster.name}.module2.complex_output_0", "q0:mw"),
        ]
    },
}

single_qubit_device.hardware_config(hardware_cfg)

Readout Calibration#

To discriminate between the ground state and the excited state with ConditionalReset, we first need to configure the ThresholdedAcquisition parameters acq_threshold and acq_rotation (see Tutorial: Acquisitions). We do so by preparing a qubit in either its ground state or its excited state, performing a measurement, and repeating this process 500 times. In the measured IQ plane we expect to find all data points clustered in two distinct groups that correspond to the two different states, and the acq_threshold and acq_rotation parameters define the line between the two groups.

We run this calibration using MeasurementControl and a predefined Schedule called readout_calibration_sched:

import numpy as np
from qcodes import ManualParameter

from quantify_scheduler import Schedule, ScheduleGettable
from quantify_scheduler.operations import Measure
from quantify_scheduler.schedules import readout_calibration_sched


single_qubit_device.cfg_sched_repetitions(1)

states = ManualParameter(name="States", unit="", label="")
states.batched = True

prepared_states = np.asarray([0, 1] * 500)
readout_calibration_kwargs = {"qubit": "q0", "prepared_states": prepared_states}
gettable = ScheduleGettable(
    single_qubit_device,
    schedule_function=readout_calibration_sched,
    schedule_kwargs=readout_calibration_kwargs,
    real_imag=True,
    batched=True,
    max_batch_size=200,
)

measurement_control.settables(states)
measurement_control.setpoints(prepared_states)
measurement_control.gettables(gettable)
measurement_control.verbose(False)

dataset = measurement_control.run("Readout Calibration")
dataset
<xarray.Dataset> Size: 24kB
Dimensions:  (dim_0: 1000)
Coordinates:
    x0       (dim_0) int64 8kB 0 1 0 1 0 1 0 1 0 1 0 1 ... 1 0 1 0 1 0 1 0 1 0 1
Dimensions without coordinates: dim_0
Data variables:
    y0       (dim_0) float64 8kB 0.0007881 -0.01229 ... -0.005863 -0.01218
    y1       (dim_0) float64 8kB 0.003489 -0.0001137 ... 0.003838 0.0008266
Attributes:
    tuid:                             20241121-223917-960-975ef1
    name:                             Readout Calibration
    grid_2d:                          False
    grid_2d_uniformly_spaced:         False
    1d_2_settables_uniformly_spaced:  False

See also

More information on configuring MeasurementControl can be found in the user guide of quantify-core, and in the tutorials Running and Experiment and ScheduleGettable

To determine the qubit threshold parameters, we use the ReadoutCalibrationAnalysis:

from quantify_core.analysis.readout_calibration_analysis import (
    ReadoutCalibrationAnalysis,
)

analysis = ReadoutCalibrationAnalysis(dataset)
analysis.run()
analysis.display_figs_mpl()
../_images/d5bbee2c2437b79347a2c85e426e3346928808b0e40845ed29b911d3a27742c3.png

The image above shows that the measured IQ points are clustered in two groups as expected. We can now fit a line between the two groups and from there obtain the acq_threshold and the acq_rotation parameters, that we add to the qubit configuration:

import numpy as np

fit_results = analysis.fit_results["linear_discriminator"].params
acq_threshold = fit_results["acq_threshold"].value
acq_rotation = (np.rad2deg(fit_results["acq_rotation_rad"].value)) % 360

q0.measure.acq_threshold(acq_threshold)
q0.measure.acq_rotation(acq_rotation)

Verifying parameters#

We can quickly verify that the qubit parameters are set correctly by running again the readout_calibration_sched schedule with "ThresholdedAcquisition" as acquisition protocol. If the calibration was done correctly, we expect that when the state is prepared in the \(|0\rangle\) state or \(|1\rangle\) state, the thresholded acquisition will return 0 or 1 respectively. The results are then verified using a confusion matrix:

from sklearn.metrics import ConfusionMatrixDisplay
import matplotlib.pyplot as plt

single_qubit_device.cfg_sched_repetitions(1)

states = ManualParameter(name="States", unit="", label="")
states.batched = True

prepared_states = np.asarray([0, 1] * 500)
readout_calibration_kwargs = {
    "qubit": "q0",
    "prepared_states": prepared_states,
    "acq_protocol": "ThresholdedAcquisition",
}

gettable = ScheduleGettable(
    single_qubit_device,
    schedule_function=readout_calibration_sched,
    schedule_kwargs=readout_calibration_kwargs,
    real_imag=True,
    batched=True,
    max_batch_size=200,
)

measurement_control.settables(states)
measurement_control.setpoints(prepared_states)
measurement_control.gettables(gettable)
dataset = measurement_control.run("Readout Calibration Verification")

prepared_states = dataset.x0.values
measured_states = dataset.y0.values

ConfusionMatrixDisplay.from_predictions(
    prepared_states, measured_states, cmap="Blues", normalize=None
)
plt.title("Confusion Matrix")
plt.xlabel("Measured State")
plt.ylabel("Prepared State")

plt.show()
../_images/2fe20ae204c8f5937fb3b2a4681bed90e81fe3434ff0a011fb1458762225fe27.png

As expected, the threshold that we set did a good job of discriminating the qubit states (the discrimination is not perfect because the data points belonging to the two states slightly overlap).

Conditional Reset#

The conditional reset is implemented in Quantify as a gate. When we have a single Reset at the beginning of a schedule, we simply replace the Reset gate with the ConditionalReset gate, for example

schedule = Schedule()
#schedule.add(Reset("q0"))
schedule.add(ConditionalReset("q0"))
...

In other cases, however, we need to pass extra arguments to ConditionalReset that we illustrate below.

Example: Modifying the T1 schedule#

In this example, we use the schedule function t1_sched using the ConditionalReset instead of the standard Reset. When using multiple consecutive ConditionalReset on the same qubit, we need to increment the acq_index for each one, similar to when adding multiple Measure to the schedule. We also need to ensure that all acquisition protocols in the schedule are equal to "ThresholdedAcquisition".

from quantify_scheduler.qblox.operations import ConditionalReset
from quantify_scheduler.operations import X, Reset


# original T1 schedule
def t1_sched(
    times: np.ndarray,
    qubit: str,
    repetitions: int = 1,
) -> Schedule:

    schedule = Schedule("T1", repetitions)
    for i, tau in enumerate(times):
        schedule.add(Reset(qubit), label=f"Reset {i}")
        schedule.add(X(qubit), label=f"pi {i}")
        schedule.add(
            Measure(qubit, acq_index=i),
            ref_pt="start",
            rel_time=tau,
            label=f"Measurement {i}",
        )
    return schedule


# updated T1 schedule
def t1_sched(
    times: np.ndarray,
    qubit: str,
    repetitions: int = 1,
) -> Schedule:

    schedule = Schedule("T1", repetitions)
    for i, tau in enumerate(times):
        schedule.add(
            ConditionalReset(qubit, acq_index=i, acq_channel=0),
            label=f"Reset {i}",
        )
        schedule.add(X(qubit), label=f"pi {i}")
        schedule.add(
            Measure(
                qubit,
                acq_index=i,
                acq_protocol="ThresholdedAcquisition",
                acq_channel=1,
            ),
            ref_pt="start",
            rel_time=tau,
            label=f"Measurement {i}",
        )
    return schedule

Running the T1 schedule using MeasurementControl#

The dataset returned by MeasurementControl will in this case have four rows of data, which are:

  • y0: contains the data establishing whether a qubit was reset or not

  • y1: contains the actual (thresholded) measurement

  • y2 and y3: filled with NaNs (currently MeasurementControl expects ScheduleGettable to return IQ values)

Below we run MeasurementControl again as before

single_qubit_device.cfg_sched_repetitions(1024)  # run and average 1024 times

# Configure the settable
time = ManualParameter("sample", label="Sample time", unit="s")
time.batched = True

times = np.array(list(np.linspace(start=1.6e-7, stop=4.976e-5, num=125)))

# Configure the gettable
gettable = ScheduleGettable(
    quantum_device=single_qubit_device,
    schedule_function=t1_sched,
    schedule_kwargs={"qubit": "q0", "times": times},
    batched=True,
    num_channels=2,
)

# Configure MeasurementControl
measurement_control.settables(time)
measurement_control.setpoints(times)
measurement_control.gettables(gettable)

dataset = measurement_control.run("t1")

Above we also passed num_channels=2 to the ScheduleGettable so that it knows to expect measurements on the same qubit, but separate acquisition channels.

We now plot the contents of this dataset:

plt.plot(dataset.x0, np.abs(dataset.y1))
plt.xlabel("time [s]")
plt.ylabel('Probability of |1⟩ state')
plt.title('T1 Relaxation Experiment')
plt.show()
../_images/b1bd6e9d4cc35d81b0604c9950f8729935fee674acee6e94f5baeef3c052f6f2.png

Note

Often, we can use an Analysis class on datasets to visualize the data and extract relevant parameters such as T1Analysis

Note on Execution Order#

Parallel ConditionalReset operations are not supported. For example, compiling the following schedule will raise a RuntimeError:

schedule = Schedule("")
schedule.add(ConditionalReset("q0"))
schedule.add(ConditionalReset("q1"), ref_pt="start")

and will have to be scheduled sequentially,

schedule = Schedule("")
schedule.add(ConditionalReset("q0"))
schedule.add(ConditionalReset("q1"))

Closing Remarks#

You can find more technical information and explanations of the different limitations in our reference guide.