Example: Measuring T1#

T1 is an experiment that measures the relaxation time of a qubit.

Information stored in a qubit decays exponentially. The time constant of the decay is called the relaxation time $T_1$.

The experiment measures $T_1$ by preparing selected qubits in the excited state by playing an X gate, waiting some time, and measuring the qubit. The waiting time is swept to reveal the exponential decay of the excited state probability.

Preparing the circuit#

import os
from qiskit import QuantumCircuit
from qiskit.compiler import transpile
from iqm.qiskit_iqm import IQMProvider
from iqm.qiskit_iqm.iqm_transpilation import optimize_single_qubit_gates
from iqm.pulla.pulla import Pulla
from iqm.pulla.utils_qiskit import qiskit_to_pulla
cocos_url = os.environ['PULLA_COCOS_URL']                     # or set the URL directly here
station_control_url = os.environ['PULLA_STATION_CONTROL_URL'] # or set the URL directly here

p = Pulla(station_control_url)
compiler = p.get_standard_compiler()

provider = IQMProvider(cocos_url)
backend = provider.get_backend()
[11-26 14:16:58;W] station-control-client version '1.23' is newer minor version than '1.18' used by the station control server, some new client features might not be supported.

Prepare the circuit with X gates for each of the qubits you want to test

import numpy as np

# Preparing the circuit
qc = QuantumCircuit(3)
qc.rx(np.pi, [0, 1, 2])
qc.measure_all()

qc_transpiled = transpile(qc, backend=backend, layout_method='sabre', optimization_level=3)
qc_optimized = optimize_single_qubit_gates(qc_transpiled)
circuits, compiler = qiskit_to_pulla(p, backend, qc_optimized)

context = compiler.compiler_context()
data = circuits

In order to add arbitrary Wait instructions in to our circuit we can manipulate the Timeboxes between the stages 4 and 5. We define a function that copies the circuit we defined above and adds Wait instructions to the end of the circuit.

from iqm.cpc.compiler.compiler import CompilationStage

sweep_times = np.linspace(0, 100e-6, 41)


def generate_t1(timeboxes, context):
    """Generate multiple versions of the input circuit, where a Wait of increasing duration is inserted before measurement."""

    circuit_timebox = timeboxes[0]  # assumes only one input circuit
    modified_timeboxes = []
    for wait_time in sweep_times:
        wait_box = context["builder"].wait(circuit_timebox.locus_components, wait_time)
        # insert a wait between everything else and the final measurement
        c = circuit_timebox[:-1] + wait_box + circuit_timebox[-1]
        modified_timeboxes.append(c)

    context["circuit_metrics"] = context["circuit_metrics"] * len(sweep_times)  # Keep metadata in sync
    return modified_timeboxes, context

Now let’s inject our function from above between stages 3 and 4, right before the timeboxes are converted to schedules. Since we changed the amount of circuits, we must also multiply the number of circuit metrics.

In addition, we change the measurement mode to circuit, so that only the selected qubits are measured. The measurement mode is part of the CircuitExecutionOptions, the the standard compiler constructed by Pulla comes with default options. To make changes, we replace the whole object with a new instance of CircuitExecutionOptions:

from iqm.cpc.interface.compiler import (
    CircuitBoundaryMode,
    CircuitExecutionOptions,
    DDMode,
    HeraldingMode,
    MeasurementMode,
    MoveGateFrameTrackingMode,
    MoveGateValidationMode,
)

compiler.options = CircuitExecutionOptions(
    measurement_mode=MeasurementMode.CIRCUIT,  # here we changed `ALL` to `CIRCUIT`
    heralding_mode=HeraldingMode.NONE,
    dd_mode=DDMode.DISABLED,
    dd_strategy=None,
    circuit_boundary_mode=CircuitBoundaryMode.ALL,
    move_gate_frame_tracking=MoveGateFrameTrackingMode.FULL,
    move_gate_validation=MoveGateValidationMode.STRICT,
    active_reset_cycles=None
)
add_waits = CompilationStage(name="add_waits")
add_waits.add_passes(generate_t1)

compiler.stages.insert(3, add_waits)
playlist, context = compiler.compile(data)

Since we are working with an experiment where averaging of the results can be useful, we can set settings.options.averaging_bins to 1. By default it is the same value as number of shots but if set to 1 then the backend will average the results in to one bin. By doing this we don’t need to later on average the results.

settings, context = compiler.build_settings(context, shots=1000)
settings.options.averaging_bins = 1
settings.options

(options)

  • options: (Name: "options", class:SettingNode) 0
    end_delay 300 μs Delay from end of sequence to next trigger
    averaging_bins 1 Average the repeats into this many bins
    playlist_repeats 1 000 Number of times to repeat execution of corresponding playlist
  • Executing the modified circuit#

    After inspecting the settings tree and the final playlist we are ready to execute the playlist.

    res = p.execute(playlist, context, settings)
    
    [11-26 14:17:02;I] Submitted sweep with ID: f954824d-66de-4c65-a438-cfc20f897d5e
    [11-26 14:17:02;I] Created task in queue with ID: b1d7683d-7c2d-4781-b619-0f33ffde81bc
    [11-26 14:17:02;I] Sweep link: http://xld11-s2.xld11.iqm.fi/station/sweeps/f954824d-66de-4c65-a438-cfc20f897d5e
    [11-26 14:17:02;I] Task link: http://xld11-s2.xld11.iqm.fi/station/tasks/b1d7683d-7c2d-4781-b619-0f33ffde81bc
    [11-26 14:17:02;I] Waiting for the sweep to finish...
    [11-26 14:17:20;I] Sweep status: SweepStatus.SUCCESS
    [11-26 14:17:20;I] [{'meas_3_0_2': [[0.9769999980926514], [0.8379999995231628], [0.7300000190734863], [0.6620000004768372], [0.5899999737739563], [0.5260000228881836], [0.4970000088214874], [0.43700000643730164], [0.3959999978542328], [0.33899998664855957], [0.3089999854564667], [0.3179999887943268], [0.2750000059604645], [0.2669999897480011], [0.2409999966621399], [0.23600000143051147], [0.21699999272823334], [0.18000000715255737], [0.19200000166893005], [0.1550000011920929], [0.15700000524520874], [0.12600000202655792], [0.14499999582767487], [0.1120000034570694], [0.11699999868869781], [0.11400000005960464], [0.12099999934434891], [0.08900000154972076], [0.12200000137090683], [0.10899999737739563], [0.09300000220537186], [0.10499999672174454], [0.07800000160932541], [0.09099999815225601], [0.07900000363588333], [0.07599999755620956], [0.07500000298023224], [0.10599999874830246], [0.07900000363588333], [0.06700000166893005], [0.07400000095367432]], 'meas_3_0_1': [[0.9190000295639038], [0.746999979019165], [0.6629999876022339], [0.6110000014305115], [0.5619999766349792], [0.5009999871253967], [0.48100000619888306], [0.4440000057220459], [0.40799999237060547], [0.382999986410141], [0.3619999885559082], [0.28700000047683716], [0.25999999046325684], [0.2529999911785126], [0.22599999606609344], [0.2240000069141388], [0.21699999272823334], [0.19699999690055847], [0.18199999630451202], [0.12700000405311584], [0.14800000190734863], [0.12600000202655792], [0.15000000596046448], [0.11400000005960464], [0.11800000071525574], [0.10499999672174454], [0.10400000214576721], [0.09700000286102295], [0.10700000077486038], [0.08900000154972076], [0.07400000095367432], [0.09000000357627869], [0.07100000232458115], [0.06199999898672104], [0.050999999046325684], [0.07100000232458115], [0.06300000101327896], [0.03999999910593033], [0.04699999839067459], [0.061000000685453415], [0.04899999871850014]], 'meas_3_0_0': [[0.9480000138282776], [0.8080000281333923], [0.7250000238418579], [0.6460000276565552], [0.6010000109672546], [0.5389999747276306], [0.49799999594688416], [0.460999995470047], [0.3930000066757202], [0.41200000047683716], [0.35899999737739563], [0.3190000057220459], [0.28999999165534973], [0.2669999897480011], [0.2630000114440918], [0.2329999953508377], [0.20399999618530273], [0.18799999356269836], [0.17599999904632568], [0.1679999977350235], [0.1550000011920929], [0.12999999523162842], [0.13500000536441803], [0.13600000739097595], [0.14300000667572021], [0.09200000017881393], [0.10400000214576721], [0.09099999815225601], [0.08500000089406967], [0.05999999865889549], [0.07100000232458115], [0.06700000166893005], [0.07599999755620956], [0.06199999898672104], [0.05999999865889549], [0.05400000140070915], [0.04399999976158142], [0.04600000008940697], [0.04899999871850014], [0.03200000151991844], [0.057999998331069946]]}]
    

    Visualising the results#

    After the circuit has executed succesfully we can visualise our results.

    import matplotlib.pyplot as plt
    
    channels = res.result[0].keys()
    
    for channel in channels:
        plt.plot(sweep_times * 1e6, res.result[0][channel], ".")
        plt.title(f"T1 of {channel}")
        plt.xlabel("Wait time (µs)")
        plt.ylabel(r"Probability of state $|1\rangle$")
        plt.show()
    
    _images/fcb4f744e776b3e75b7f9b88eaa500aaa2ba96d4b90a6cd9c42fc3d8727a5635.png _images/c404463b02a218e97a37a1323f32de68c4775e523f0c58e23e56248875666151.png _images/dca6288adb16126dc09e7e5cd08110824ae9c5cb277abca5f5b997e9a95d01c5.png

    We can also visualise the finalised schedule. We should see that each segment is different and the waits at the end are increasing towards the end.

    TIP: If the X gates are hard to distinguish from the visualiser you can set a value for the Instruction duration threshold. By setting it to e.g. 1000 you squeeze/truncate all instructions that have a length over 1000 ns to a constant length within the visualiser. By clicking on the waits you can still inspect the absolute duration of the Wait instructions.

    from iqm.pulse.playlist.visualisation.base import inspect_playlist
    from IPython.core.display import HTML
    
    HTML(inspect_playlist(playlist, range(5)))