Collecting and post-processing tomography data¶

This notebook shows how to use the QLM functions to collect data for tomography, and how to postprocess it.

Part I: Data collection¶

In [1]:
# imports
%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.quops import QuantumChannelKraus, ParametricAmplitudeDamping
from qat.hardware import HardwareModel, GatesSpecification
from qat.qpus import NoisyQProc

Preparation and measurement sequences¶

The quantum process corresponding to a gate $G$ is a linear application (quantum channel/CPTP map). It can be fully characterized by its action on a family of linearly independent vectors of the Hilbert space of density matrices (which are of size $d^2 \times d^2$, with $d=2^{nqbits}$). In other words, one must prepare at least $d^2$ "informationally complete" (IC) states, and measure at least $d^2$ IC observables on the final state (i.e after the action of the gate $G$ to be characterized).

The sequence of such "preparation gates" and "measurement gates" (sometimes called "fiducial sequences") can be specified by the user, or chosen from predefined sequences that are returned by the function prepare_gatesets_and_fiducials. Note that this function also returns predefined gatesets because the tomography method called Gate Set Tomography (GST) requires some consistency between the gateset and the fiducial sequences.

In the following cell, we use this function to generate a simple gateset $\mathcal{G}$ and fiducial sequences $\mathcal{F}$ and $\mathcal{F}'$ (note that here $\mathcal{F}=\mathcal{F}'$):

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

print("gateset (G) =", gatesets["gb1"])
print("prep_gates (F) =", prep_gates["gb1"])
print("meas_gates (F') =", meas_gates["gb1"])
gateset (G) = [('X_PI2', [0]), ('Y_PI2', [0])]
prep_gates (F) = [[], [('X_PI2', [0])], [('Y_PI2', [0])], [('X_PI2', [0]), ('X_PI2', [0])]]
meas_gates (F') = [[], [('X_PI2', [0])], [('Y_PI2', [0])], [('X_PI2', [0]), ('X_PI2', [0])]]

Note that here by convention, 'X_PI2' means $R_x(\pi/2)$ (and so on).

The preparation and measurement fiducial sequences are meant to be applied after preparation of a fixed initial state (usually $\rho=|0\rangle\langle 0|$) or measurement of a fixed observable (usually $|1\rangle \langle 1|$), respectively.

Data collection¶

Tomography starts by computing the expectation value $\tilde{G}^{(k)}_{i,j}$ of a series of circuits consisting of the gate sequences $F_j^{(1)},\dots F_j^{(n_j)},G^{(k)},F_i^{'(1)},\dots, F_i^{'(n'_i)}$ with $F_j \in \mathcal{F}$, $G^{(k)} \in \mathcal{G}$, $F'_i \in \mathcal{F}'$.

This is done in the function collect_tomography_data, which can be used in two ways:

  • when no "qpu" parameter is set, the function merely generates the *.circ files corresponding the circuits which we need to run (this mode is just here temporarily)
In [3]:
# when no option "qpu" is set: we only dump the circuits to *.circ files
from qat.tomo import collect_tomography_data
_ = collect_tomography_data(prep_gates["gb1"],
                            meas_gates["gb1"],
                            gate_list=["X_PI2"],
                            qbits=[0])
In [4]:
!ls *.circ
Gtilde_0_prep_0_meas_0.circ  Gtilde_0_prep_2_meas_1.circ
Gtilde_0_prep_0_meas_1.circ  Gtilde_0_prep_2_meas_2.circ
Gtilde_0_prep_0_meas_2.circ  Gtilde_0_prep_2_meas_3.circ
Gtilde_0_prep_0_meas_3.circ  Gtilde_0_prep_3_meas_0.circ
Gtilde_0_prep_1_meas_0.circ  Gtilde_0_prep_3_meas_1.circ
Gtilde_0_prep_1_meas_1.circ  Gtilde_0_prep_3_meas_2.circ
Gtilde_0_prep_1_meas_2.circ  Gtilde_0_prep_3_meas_3.circ
Gtilde_0_prep_1_meas_3.circ  qft.circ
Gtilde_0_prep_2_meas_0.circ
  • when the "qpu" is specified, the circuits are simulated or actually run using the user-supplied QPU:
In [5]:
# in the cell below, we build a toy QPU and call the function

# here I create a QPU from a noise model; this QPU can be replaced
# by an actual quantum processor
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")

G_tilde, G_tilde_err_sq, g_tilde,\
g_tilde_err_sq, E_tilde, E_tilde_err_sq,\
rho_tilde, rho_tilde_err_sq = collect_tomography_data(prep_gates["gb1"],
                                                      meas_gates["gb1"],
                                                      gate_list=["X_PI2"],
                                                      qpu=qpu,
                                                      qbits=[0],
                                                      n_shots=512,
                                                      add_gst_measurements=True)


print(G_tilde)
print(type(E_tilde), E_tilde.shape)
[array([[0.23535156, 0.5       , 0.25390625, 0.25488281],
       [0.5       , 0.23632812, 0.24707031, 0.        ],
       [0.25195312, 0.25      , 0.5       , 0.22851562],
       [0.24316406, 0.        , 0.25976562, 0.25488281]])]
<class 'numpy.ndarray'> (1, 4)

In the cell above, we added an option add_gst_measurements. When set to true, it adds to the above-mentioned $G^{(k)}_{ij}$ measurements other measurements needed for GST:

  • circuits of the type $F_j^{(1)},\dots F_j^{(n_j)}, F_i^{'(1)},\dots, F_i^{'(n'_i)}$, with corresponding average $ \tilde{g}_{ij}$,
  • circuits of the type $F_j^{(1)},\dots F_j^{(n_j)}$, with corresponding average $ \tilde{\rho}_{j}$,
  • circuits of the type $F_i^{'(1)},\dots, F_i^{'(n'_i)}$, with corresponding average $\tilde{E}_{i}$.

All the variable names *_err_sq correspond to the squared error on the mean relative to the respective circuits.

A word of warning: the gate names given in the preparation/measurement sequences and in the gateset have to be consistent with the native gateset of the QPU.

Dumping data to file and reading from file¶

The data collected from the circuits used in collect_tomography_data can easily be dumped to a 'json' file, and later read out from it. Here is a example:

In [6]:
data = {"G_tilde": [g.tolist() for g in G_tilde],
        "G_tilde_err": [g.tolist() for g in G_tilde_err_sq],
        "g_tilde": g_tilde.tolist(),
        "E_tilde": E_tilde.tolist(),
        "rho_tilde": rho_tilde.tolist(),
        "prep_gates": prep_gates["gb1"], 
        "meas_gates": meas_gates["gb1"],
        "gate_list": ["X_PI2"]
       }

import io, json
with io.open('data.txt', 'w') as f:
    f.write(json.dumps(data, ensure_ascii=False))

with open('data.txt') as json_file:  
    data2 = json.load(json_file)

Part II: Post-processing¶

The next step is to use the measured data to characterize the quantum processor's hardware model.

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

with_ptm = False

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



from qat.tomo import process_tomography_data
nqbits = 1
gspec = process_tomography_data('lqpt', 
                              prep_gates["gb1"],
                              meas_gates["gb1"],
                              G_tilde,
                              G_tilde_err_sq,
                              g_tilde,
                              E_tilde,
                              rho_tilde,
                              nqbits, target_gates_spec=target_gate_spec,
                              gate_list=["X_PI2"], enforce_CP=False,
                              enforce_TP=False, verbose=True)
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
err = 1.3976753948711207e-15
In [8]:
gspec.quantum_channels['X_PI2'].ptm
Out[8]:
array([[ 0.98828125,  0.0390625 , -0.01171875, -0.03125   ],
       [-0.02734375,  1.        , -0.02734375,  0.078125  ],
       [-0.01171875,  0.05078125, -0.06640625, -1.03125   ],
       [ 0.0078125 ,  0.00390625,  1.0078125 ,  0.0078125 ]])