Tomography: getting started¶

In this notebook, you will learn how to use the QLM's tomography module, qat.tomo.

Hardware model definition¶

In [1]:
%load_ext autoreload
%autoreload 2
import numpy as np
from qat.core.circuit_builder.matrix_util import get_predef_generator, get_param_generator
from qat.hardware import HardwareModel
from qat.hardware import GatesSpecification
from qat.qpus import NoisyQProc
from qat.quops import QuantumChannelPTM, QuantumChannelKraus, ParametricAmplitudeDamping
from qat.quops.converters import convert_kraus_to_ptm

with_ptm = True

if with_ptm:
    AD = QuantumChannelPTM(convert_kraus_to_ptm(ParametricAmplitudeDamping(T_1=10)(1).kraus_operators))
    ptm_dict = {key: AD*QuantumChannelPTM(convert_kraus_to_ptm([get_param_generator()[g_name](angle)]))
                for key, g_name, angle in [("X_PI2","RX", np.pi/2),
                                           ("Y_PI2", "RY", np.pi/2),
                                           ("X_PI", "RX", np.pi)]}

    target_gate_spec = GatesSpecification(gate_times={k: 0 for k in ptm_dict.keys()},
                                          quantum_channels=ptm_dict)

    qpu = NoisyQProc(hardware_model=HardwareModel(target_gate_spec),
                     sim_method="deterministic-vectorized")
else:
    AD = ParametricAmplitudeDamping(T_1=10)(1)
    kraus_dict = {key: QuantumChannelKraus([get_param_generator()[g_name](angle)])
                for key, g_name, angle in [("X_PI2","RX", np.pi/2),
                                           ("Y_PI2", "RY", np.pi/2),
                                           ("X_PI", "RX", np.pi)]}

    target_gate_spec = GatesSpecification(gate_times={k: 0 for k in kraus_dict.keys()},
                                          quantum_channels=kraus_dict)

    qpu = NoisyQProc(hardware_model=HardwareModel(target_gate_spec),
                     sim_method="deterministic-vectorized")

Choosing a gate set, preparations and measurements¶

Tomography consists in using the QPU as a black box, with the only assumption that each quantum process, be it noise or a gate itself, acts as a linear map on the density matrix of the system. (One may also in addition assume that this map is trace-preserving and completely positive, but we will deal with these properties later on). To fully determine this matrix, one needs to prepare families of input states of rank at least the dimension of the underlying Hilbert space; similarly, one needs to determine the value of the projections of the final state on a family of independent measurements.

To prepare the input states and the measurements, the user just has to specify lists of gate sequences ${F_j}_j$ and ${F'_i}_i$ such that the corresponding input states $F_j |\rho_0\gg$ and measurements $\ll E| F'_i$ are informationnally complete, i.e are independent vectors in the Hilbert space. These two lists of gate sequences are sometimes called "preparation fiducials" and "measurement fiducials".

In the following cell, we prepare three different choices of fiducials.

In [2]:
from qat.tomo.util import prepare_gatesets_and_fiducials
gatesets, prep_fiducials, meas_fiducials = prepare_gatesets_and_fiducials()

Launching the tomography¶

Two distinct tomography methods are implemented in the QLM:

  • Quantum Process Tomography (QPT) assumes that state preparations and final measurements are know a priori. Using this knowledge, one can use the collected tomography data to characterize the quantum channel one wants to determine.
  • Gateset Tomography (GST) handles state preparations and final measurements at the same level as noisy gates, and uses a self-consistent post-processing of the collected tomography data to characterize the full gate set, including the preparation and measurement operations.

In the QLM, tomography is performed by the function tomography. The output is a DefaultGatesSpecification object containing the updated quantum channels (compared to the target gates).

In [3]:
from qat.tomo import perform_tomography
from qat.quops.converters import convert_kraus_to_ptm
tomo_gates_spec = perform_tomography(qpu, 
                             method="lgst",  # which method to use (can also be "lgst")
                             prep_gates=prep_fiducials["gb1"],
                             meas_gates=meas_fiducials["gb1"],
                             target_gates_spec=target_gate_spec,  #expected gates (a priori knowledge, used by QPT)
                             #target_gates_spec=None,  #expected gates (a priori knowledge, used by QPT)
                             n_shots=0,  # number of runs for each circuit
                             qbits=[0],  # index of qubit(s) on which gate is applied
                             gate_list=["X_PI2","Y_PI2"],  # list of gate names to characterize
                             enforce_TP=False,
                             verbose=True)

# we can know print the corresponding quantum channels. Here, we use the Pauli Transfer Matrix (PTM) representation.
for gate_name in ["X_PI2"]:
    print("==== Gate ", gate_name)
    if with_ptm:
        G_tomo = tomo_gates_spec.quantum_channels[gate_name].ptm
        G_theo = target_gate_spec.quantum_channels[gate_name].ptm
    else:
        #G_tomo = convert_kraus_to_ptm(tomo_gates_spec._quantum_channels[gate_name].kraus_operators)
        G_tomo = tomo_gates_spec.quantum_channels[gate_name].ptm
        G_theo = convert_kraus_to_ptm(target_gate_spec.quantum_channels[gate_name].kraus_operators)
    print("G from tomography=\n",G_tomo)
    print("expected G=\n", G_theo)
    print("err = ", np.linalg.norm(G_tomo-G_theo))
==== Gate  X_PI2
G from tomography=
 [[ 1.00000000e+00  8.07410128e-16  1.78371510e-16  6.35083197e-16]
 [-2.92471561e-16  9.51229425e-01 -1.24245287e-16  4.32128657e-16]
 [-8.14318885e-16 -7.06126926e-16  4.29604794e-16 -9.51229425e-01]
 [ 9.51625820e-02  1.48673084e-16  9.04837418e-01  2.01502314e-16]]
expected G=
 [[ 1.00000000e+00  0.00000000e+00 -5.55111512e-17 -1.23259516e-32]
 [ 0.00000000e+00  9.51229425e-01  0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  2.11215362e-16 -9.51229425e-01]
 [ 9.51625820e-02  0.00000000e+00  9.04837418e-01  2.00914267e-16]]
err =  2.1022672275737607e-15

We can now, using the noise model we just characterized, predict the outcome of a given circuit. Here, instead of using one of the QLM's simulators, we directly compute the final expectation value using the PTM representation of operators and superoperators:

In [4]:
if with_ptm:
    E_theo = np.array([[0.5,0, 0, -0.5]])
    rho_theo = np.array([[0.5], [0], [0], [0.5]])
    E_tomo = tomo_gates_spec.meas# if tomo_gates_spec.meas is not None else E_theo
    rho_tomo = tomo_gates_spec.state_prep# if tomo_gates_spec.state_prep is not None else rho_theo

    print("<<E tomo| = ", E_tomo)
    print("| rho tomo>> = ", rho_tomo)

    for gate_name in ["X_PI2"]:
        print("==== Gate G = %s ========="% gate_name)
        G_tomo = tomo_gates_spec.quantum_channels[gate_name].ptm
        G_theo = target_gate_spec.quantum_channels[gate_name].ptm
        print("G tomo = ", G_tomo)
        print("G theor = ", G_theo)
        print("|0> --- X(pi/2)  ---> |0> - i|1>  --- (measure) ---> 0.5")
        print("tr(M G(rho_0))_tomo = ", 2*E_tomo.dot(G_tomo.dot(rho_tomo)))
        print("tr(M G(rho_0))_theo = ", 2*E_theo.dot(G_theo.dot(rho_theo)))
        print("|0> --- X(pi/2) X(pi/2)  ---> |1> --- (measure) ---> 1")
        print("tr(M G^2(rho_0))_tomo = ", 2*E_tomo.dot(G_tomo.dot(G_tomo.dot(rho_tomo))))
        print("tr(M G^2(rho_0))_theo = ", 2*E_theo.dot(G_theo.dot(G_theo.dot(rho_theo))))
<<E tomo| =  [[ 5.00000000e-01  4.33341983e-17 -2.14272094e-17 -5.00000000e-01]]
| rho tomo>> =  [[ 5.00000000e-01]
 [-1.83880688e-16]
 [-3.10515502e-16]
 [ 5.00000000e-01]]
==== Gate G = X_PI2 =========
G tomo =  [[ 1.00000000e+00  8.07410128e-16  1.78371510e-16  6.35083197e-16]
 [-2.92471561e-16  9.51229425e-01 -1.24245287e-16  4.32128657e-16]
 [-8.14318885e-16 -7.06126926e-16  4.29604794e-16 -9.51229425e-01]
 [ 9.51625820e-02  1.48673084e-16  9.04837418e-01  2.01502314e-16]]
G theor =  [[ 1.00000000e+00  0.00000000e+00 -5.55111512e-17 -1.23259516e-32]
 [ 0.00000000e+00  9.51229425e-01  0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  2.11215362e-16 -9.51229425e-01]
 [ 9.51625820e-02  0.00000000e+00  9.04837418e-01  2.00914267e-16]]
|0> --- X(pi/2)  ---> |0> - i|1>  --- (measure) ---> 0.5
tr(M G(rho_0))_tomo =  [[0.45241871]]
tr(M G(rho_0))_theo =  [[0.45241871]]
|0> --- X(pi/2) X(pi/2)  ---> |1> --- (measure) ---> 1
tr(M G^2(rho_0))_tomo =  [[0.8827727]]
tr(M G^2(rho_0))_theo =  [[0.8827727]]

Tomography with a BatchGenerator¶

The QLM also provides a high-level interface to generate the circuits needed to perform tomography of different gates on a QPU. The generator is initialized with the tomography method that will be used

  • Linear inversion Quantum Process Tomography (lqpt)

  • Maximum Likelihood Quantum Process Tomography (mlqpt)

  • or Linear inversion Gateset Tomography (lgst)

and a target gate specs in the case of Quantum Process Tomography.

An application is returned when the batch generator is piped to the QPU that we want to perform tomography on.

Finally, the application can be executed (by simply calling it with the gate list that we want to collect the data on) to submit tomography job to the QPU.

The result that is returned from the tomography job is an instance of GatesSpecification, similarly to the output of the perform_tomography method above. Therefore the same information can be extracted on the returned object (the tested gates’ noise models, their durations, as well as the initialization and the readout (or measurement) on the given QPU).

In [5]:
from qat.generators import TomographyGenerator

# Create a tomography generator with lqpt method, in the case of QPT method, a target_gates_spec is required
tomo_generator = TomographyGenerator('lqpt', target_gates_spec=target_gate_spec)

# Pipe the tomography batch generator to the noisy QPU
stack = tomo_generator | qpu

# The list of gate names to characterize
gate_list = ["X_PI2", "Y_PI2"]

# Execute the generator on the list of gates and qubits, also specifying the prep gates and measure gates
result = stack.execute(prep_fiducials["gb1"], meas_fiducials["gb1"], gate_list, qbits=[0], nbshots=0)

# we can know print the corresponding quantum channels. Here, we use the Pauli Transfer Matrix (PTM) representation.
for gate_name in ["X_PI2"]:
    print("==== Gate ", gate_name)
    if with_ptm:
        G_tomo = result.quantum_channels[gate_name].ptm
        G_theo = target_gate_spec.quantum_channels[gate_name].ptm
    else:
        G_tomo = result.quantum_channels[gate_name].ptm
        G_theo = convert_kraus_to_ptm(target_gate_spec.quantum_channels[gate_name].kraus_operators)
    print("G from tomography=\n",G_tomo)
    print("expected G=\n", G_theo)
    print("err = ", np.linalg.norm(G_tomo-G_theo))
==== Gate  X_PI2
G from tomography=
 [[ 1.00000000e+00  3.05311332e-16  6.65931473e-16  3.52523809e-16]
 [-3.51058310e-16  9.51229425e-01 -1.99629698e-16 -5.21775154e-17]
 [-2.24954755e-16 -2.58672359e-16  3.55190715e-16 -9.51229425e-01]
 [ 9.51625820e-02  1.96599767e-16  9.04837418e-01 -6.28037858e-17]]
expected G=
 [[ 1.00000000e+00  0.00000000e+00 -5.55111512e-17 -1.23259516e-32]
 [ 0.00000000e+00  9.51229425e-01  0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  2.11215362e-16 -9.51229425e-01]
 [ 9.51625820e-02  0.00000000e+00  9.04837418e-01  2.00914267e-16]]
err =  1.5458835909342365e-15