Example: Measuring T1

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.

import os
from iqm.pulla.pulla import Pulla
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()

Preparing the circuit#

High-level QuantumOperations like Quantum gates can be implemented using different GateImplementations. To control low-level aspects of the execution, we can create custom operations, the implementation of which we can control.

Here we invent a custom operation "custom" and write an implementation "Wait", which only inserts a delay of certain duration. In principle, the custom gate implementation could include any number of low-level instructions, but for this task we only need a wait. https://iqm-finland.github.io/docs/iqm-pulse/using_builder.html for more details about lower level controls.

In later cells, we mix the custom gate with other, more familiar gates.

from iqm.pulse.gate_implementation import CompositeGate

class Wait(CompositeGate):
    # GateImplementation for our custom operation. Only adds a delay to a qubit, and nothing else.

    def _call(self, wait_time: float):
        # This will be called when "custom" is encountered during circuit compilation.
        return self.builder.wait(self.locus, wait_time, rounding=True)

We must register the operation, so that the compiler knows what to do with it:

compiler.add_implementation(
    op_name='custom',
    implementation=Wait,
    impl_name='Wait',
    quantum_op_specs={'params': ('wait_time', )}, # Wait has this parameter
    overwrite=True
)

We need to select the physical qubits to work on. These are available on the QPU:

qubits = compiler.builder.chip_topology.qubits_sorted
qubits
('QB1', 'QB2', 'QB3', 'QB4', 'QB5')

Out of these, we select a few:

qubits = qubits[0:2]

Now we create all the circuits. In each circuit, we do a PRX(pi), or X, then our custom operation that waits, then measure all qubits. We create a circuit for each delay time we want on the time axis.

from iqm.cpc.interface.compiler import Circuit, CircuitOperation as Op
import numpy as np

time_axis = np.linspace(0.0, 300e-6, 51) # In seconds
circuits = []
for wait_time in time_axis:
    
    instructions=[]
    for qubit in qubits:
        instructions += [
            Op("prx", (qubit,), args={"angle": np.pi, "phase": 0.0}),
            Op("custom", (qubit,), args={"wait_time": wait_time}),
        ]                                 
    instructions.append(Op("measure", qubits, args={"key": "M"}))
    circuits.append(Circuit("T1", instructions))
    
circuits[-1]
Circuit(name='T1', instructions=[CircuitOperation(name='prx', locus=('QB1',), args={'angle': 3.141592653589793, 'phase': 0.0}, implementation=None), CircuitOperation(name='custom', locus=('QB1',), args={'wait_time': 0.0003}, implementation=None), CircuitOperation(name='prx', locus=('QB2',), args={'angle': 3.141592653589793, 'phase': 0.0}, implementation=None), CircuitOperation(name='custom', locus=('QB2',), args={'wait_time': 0.0003}, implementation=None), CircuitOperation(name='measure', locus=('QB1', 'QB2'), args={'key': 'M'}, implementation=None)])

Then compile the circuits. We tweak the settings so that the shots are averaged by the server, so that we don’t need to. The results therefore return as sampled probabilities.

playlist, context = compiler.compile(circuits)
settings, context = compiler.build_settings(context, shots=500)

# Average over shots. In normal circuit execution, this would be equal to shots:
settings.options.averaging_bins = 1  
job = p.execute(playlist, context, settings, verbose=False)
[04-17 11:08:57;I] Submitted sweep with ID: 65faa830-2059-471f-978e-b77ecda15b3a
[04-17 11:08:57;I] Created task in queue with ID: 7de1f2fe-b7c4-45fe-86a4-bbf090ca79d6
[04-17 11:08:57;I] Sweep link: http://varda.qc.iqm.fi/station/sweeps/65faa830-2059-471f-978e-b77ecda15b3a
[04-17 11:08:57;I] Task link: http://varda.qc.iqm.fi/station/tasks/7de1f2fe-b7c4-45fe-86a4-bbf090ca79d6
[04-17 11:08:57;I] Waiting for the sweep to finish...
[04-17 11:08:58;I] Celery task ID: 7de1f2fe-b7c4-45fe-86a4-bbf090ca79d6
[04-17 11:09:12;I] Sweep status: SweepStatus.SUCCESS

Extract the results

import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

def exp_func(x, a, b, c):
    return a * np.exp(- x / b) + c

plt.figure()
x = time_axis * 1e6

for i, qubit in enumerate(qubits):
    
    y = np.array([circuit_result["M"][0][i] for circuit_result in job.result])
    popt, _ = curve_fit(exp_func, time_axis, y, p0=[1,50e-6,0])  
    print(f"{qubit} T1 = {popt[1]*1e6 : .1f} µs", )
    
    plt.plot(x, y, 'o', label=qubit)
    plt.plot(x, exp_func(np.array(time_axis), *popt), 'k:')
    
plt.xlabel("Delay time (µs)")
plt.ylabel("Excited state probablility")
plt.legend();
QB1 T1 =  28.1 µs
QB2 T1 =  21.8 µs
_images/759e5948037100a762822a9b7c9446bdceda8bd058081d297d769636d36f8d45.png

We can also visualise the final playlist. We should see that each circuit is different and the waits at the end are increasing towards the end.

from iqm.pulse.playlist.visualisation.base import inspect_playlist
from IPython.core.display import HTML

HTML(inspect_playlist(playlist, range(5))) # Show first 5 circuits