Quantum Process Tomography with Qiskit
Characterize quantum gates and channels using process tomography in Qiskit. Understand how to reconstruct the process matrix and interpret fidelity results.
Circuit diagrams
Quantum process tomography (QPT) is the gold standard for complete gate characterization. It reconstructs the full mathematical description of a quantum channel by probing it with many different input states and measuring the outputs in multiple bases. The result is a matrix that tells you exactly what your gate does to every possible input, including any noise, decoherence, or calibration errors.
The distinction from state tomography is important. State tomography answers “what state do I have?” Process tomography answers “what does my gate actually do?” State tomography characterizes a single quantum state. Process tomography characterizes an entire operation, which means you need to run state tomography on multiple different inputs and stitch the results together.
For a single qubit, the protocol works like this: prepare 4 different input states (enough to span the space of density matrices), apply the gate, measure each output in 3 different bases. Those 12 measurement settings produce enough data to fully reconstruct any single-qubit quantum channel. This works because quantum channels are linear maps on density matrices, and a linear map on a d-dimensional system is fully determined by its action on a spanning set of d^2 input states. For one qubit, d = 2, so you need 4 inputs. For two qubits, d = 4, so you need 16 inputs. The cost grows fast.
That exponential scaling is the reason you need to know when QPT’s cost is justified. For routine gate quality checks, randomized benchmarking is cheaper and more noise-robust. But when you need the full channel description (for building noise models, verifying error correction gadgets, or publishing complete gate characterization data), QPT is the tool you reach for.
How QPT Works Step by Step
The process tomography protocol has four stages: state preparation, gate application, measurement, and reconstruction. Here is what each stage looks like for a single-qubit gate.
Stage 1: Prepare input states. You need a set of input states that spans the space of 2x2 density matrices. The standard choice is four states:
| State | Vector | Eigenstate of |
|---|---|---|
| |0⟩ | (1, 0) | Z (+1) |
| |1⟩ | (0, 1) | Z (-1) |
| |+⟩ | (1, 1)/√2 | X (+1) |
| |+i⟩ | (1, i)/√2 | Y (+1) |
These four states form a tomographically complete set because their density matrices span the full space of 2x2 Hermitian matrices.
Stage 2: Apply the gate. For each of the 4 input states, apply the gate you want to characterize. This is the “black box” step.
Stage 3: Measure in multiple bases. After the gate, measure the output in 3 bases: Z (computational basis), X (apply H before measuring), and Y (apply S†H before measuring). Each basis measurement tells you the expectation value of the corresponding Pauli operator.
This gives 4 inputs × 3 measurement bases = 12 distinct circuits total. Each circuit runs for many shots (4096 minimum) to estimate the output probability distributions.
Stage 4: Reconstruct the Choi matrix. From the 12 probability distributions, a classical fitting algorithm (typically maximum likelihood estimation) reconstructs the Choi matrix of the channel. Maximum likelihood ensures the result is a valid quantum channel (completely positive and trace-preserving).
Qiskit Experiments automates this entire protocol. When you call ProcessTomography(circuit).run(backend), it generates the 12 circuits, submits them, collects results, and runs the MLE fitter. You get back the Choi matrix without touching any of the internals.
The Process Matrix
Any physical quantum operation on n qubits can be described by a superoperator. There are several equivalent matrix representations, each useful for different purposes.
Choi Matrix
The Choi matrix comes from the Choi-Jamiolkowski isomorphism, which maps a quantum channel to a quantum state. The construction is elegant: prepare a maximally entangled state between your system qubit and a reference qubit, apply the channel to the system qubit only, then measure the joint two-qubit state. The resulting density matrix is the Choi matrix.
For a single-qubit channel, the Choi matrix is 4×4 (since 4 = d² where d = 2). For a two-qubit channel, it is 16×16. The Choi matrix is positive semidefinite if and only if the channel is completely positive, which makes it a natural representation for optimization and reconstruction.
Qiskit Experiments returns the Choi matrix by default, and it is the most numerically convenient representation for analysis.
Chi Matrix (Process Matrix)
The Chi matrix (also called the process matrix) expresses the channel as a sum over Pauli operators:
where {P_i} is the set of Pauli operators {I, X, Y, Z} for a single qubit. The Chi matrix has a direct physical interpretation:
- Diagonal elements χ_ii tell you the probability of each Pauli error occurring
- Off-diagonal elements χ_ij indicate coherent errors (correlations between different Pauli channels)
- A depolarizing channel has a diagonal Chi matrix
- A pure unitary has a rank-1 Chi matrix
Chi is what you typically see plotted in papers as “process tomography results,” because its Pauli-basis structure makes the error decomposition visually intuitive.
Kraus Operators
The operator-sum (Kraus) representation writes the channel as:
where the Kraus operators satisfy the completeness relation ∑_k K_k†K_k = I.
The number of Kraus operators tells you about the channel’s nature:
- A perfect unitary has exactly one Kraus operator (the unitary matrix itself)
- Decoherence adds more Kraus operators
- A channel with many significant Kraus operators has more mixing and entropy production
Converting Between Representations
Qiskit’s quantum_info module makes it simple to convert between all three representations:
from qiskit.quantum_info import Choi, Chi, Kraus, Operator
# Start from an ideal Hadamard gate
ideal_op = Operator.from_label("H")
# Convert to each representation
choi = Choi(ideal_op)
chi = Chi(ideal_op)
kraus = Kraus(ideal_op)
print("Choi matrix shape:", choi.data.shape)
print("Chi matrix shape:", chi.data.shape)
print("Number of Kraus operators:", len(kraus.data))
# Convert between representations
choi_from_chi = Choi(chi)
chi_from_kraus = Chi(kraus)
# All representations are equivalent
import numpy as np
print("Choi round-trip match:", np.allclose(choi.data, choi_from_chi.data))
Installation
pip install qiskit qiskit-aer qiskit-experiments
Note: All code in this tutorial requires
qiskit-experiments. Install it before running any blocks.
Running Process Tomography
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit_experiments.library import ProcessTomography
# Define the gate you want to characterize: a Hadamard
qc = QuantumCircuit(1)
qc.h(0)
# Create the process tomography experiment
qpt = ProcessTomography(qc)
backend = AerSimulator()
job = qpt.run(backend, shots=4096)
result = job.block_for_results()
# Extract the reconstructed Choi matrix
choi_result = result.analysis_results("state")
choi = choi_result.value
print(choi)
Computing Process Fidelity
The process fidelity compares the reconstructed channel to the ideal unitary:
# Requires: qiskit_experiments (choi from process tomography above)
from qiskit.quantum_info import Operator, process_fidelity, Choi
# Ideal Hadamard as a Choi matrix
from qiskit_aer import AerSimulator
from qiskit.quantum_info import Choi, Operator
import numpy as np
ideal_op = Operator.from_label("H")
ideal_choi = Choi(ideal_op)
fidelity = process_fidelity(choi, ideal_choi)
print(f"Process fidelity: {fidelity:.4f}")
# Average gate fidelity is a more practical metric
from qiskit.quantum_info import average_gate_fidelity
agf = average_gate_fidelity(choi, ideal_choi)
print(f"Average gate fidelity: {agf:.4f}")
Average gate fidelity (AGF) relates to process fidelity by AGF = (d * F_process + 1) / (d + 1) where d = 2^n. AGF is the number typically quoted in hardware specifications.
Reading the Choi Matrix
The Choi matrix contains all the information about your channel, but you need to know what to look for. Here is how to diagnose different noise types from the Choi matrix structure.
Rank and Eigenvalues
The eigenvalues of the Choi matrix are your first diagnostic tool:
- A perfect unitary produces a rank-1 Choi matrix (one non-zero eigenvalue equal to d, the rest zero). This is because a unitary channel maps the maximally entangled input to a pure state.
- Depolarizing noise makes the Choi matrix a weighted sum of the ideal rank-1 Choi and the maximally mixed state (identity/d²). The eigenvalue spectrum shows one large eigenvalue and several small, equal eigenvalues.
- Amplitude damping (T1 decay) creates an asymmetric Choi matrix with suppressed off-diagonal elements. The eigenvalue spectrum is uneven, reflecting the asymmetry between |0⟩ and |1⟩.
- Coherent over-rotation (e.g., the gate rotates by π/2 + ε instead of π/2) produces a rank-1 Choi matrix that is rotated relative to the ideal. The fidelity drops, but the rank stays at 1.
# Requires: choi from process tomography above
import numpy as np
# Compute eigenvalues of the Choi matrix
eigenvalues = np.linalg.eigvalsh(choi.data)
eigenvalues_sorted = np.sort(eigenvalues)[::-1]
print("Choi matrix eigenvalues (descending):")
for i, ev in enumerate(eigenvalues_sorted):
print(f" λ_{i} = {ev:.6f}")
# Check the rank (number of significant eigenvalues)
threshold = 1e-6
rank = np.sum(eigenvalues > threshold)
print(f"\nEffective rank: {rank}")
if rank == 1:
print("Channel is approximately unitary (rank 1)")
elif np.allclose(eigenvalues_sorted[1:], eigenvalues_sorted[1], atol=1e-3):
print("Eigenvalue pattern suggests depolarizing noise")
else:
print("Non-trivial noise structure; inspect eigenvalues manually")
Detecting Coherent vs Incoherent Errors
Coherent errors (systematic over/under-rotations, crosstalk) are qualitatively different from incoherent errors (T1, T2, depolarization). The distinction matters because coherent errors can be corrected by recalibrating pulse parameters, while incoherent errors require error correction or mitigation.
A useful diagnostic: compute the unitarity of the channel. Unitarity equals 1 for a unitary channel and decreases with incoherent noise, but stays at 1 for purely coherent errors:
from qiskit.quantum_info import Choi, Operator
import numpy as np
def compute_unitarity(choi_matrix, d=2):
"""Compute the unitarity of a quantum channel from its Choi matrix.
Unitarity = 1 for unitary channels and purely coherent errors.
Unitarity < 1 indicates incoherent noise.
"""
# Convert Choi to the unital part of the channel
# Unitarity = Tr(E_u^dag E_u) / (d^2 - 1)
# where E_u is the unital part of the transfer matrix
from qiskit.quantum_info import SuperOp
sup = SuperOp(Choi(choi_matrix))
transfer = sup.data
# Remove the trace-preserving row and identity column
# to get the unital block
T = transfer[1:, 1:] # (d^2-1) x (d^2-1) block
unitarity = np.real(np.trace(T.conj().T @ T)) / (d**2 - 1)
return unitarity
# Example: check unitarity of the reconstructed channel
u = compute_unitarity(choi.data)
print(f"Unitarity: {u:.6f}")
if u > 0.99:
print("Channel is nearly unitary (errors are mostly coherent)")
else:
print("Significant incoherent noise present")
Two-Qubit Process Tomography
QPT scales as 16^n measurement circuits, so two-qubit characterization is at the practical limit:
from qiskit_experiments.library import ProcessTomography
# Characterize a CNOT gate
cnot_qc = QuantumCircuit(2)
cnot_qc.cx(0, 1)
qpt_2q = ProcessTomography(cnot_qc)
job = qpt_2q.run(backend, shots=8192)
result = job.block_for_results()
choi_2q = result.analysis_results("state").value
ideal_cnot = Operator.from_label("CX")
fidelity = process_fidelity(choi_2q, Choi(ideal_cnot))
print(f"CNOT process fidelity: {fidelity:.4f}")
Diagnosing Noise Structure
The Choi matrix can reveal the type of noise affecting your gate:
# Requires: qiskit_experiments (choi variable from process tomography)
import numpy as np
# Get the Kraus operators from the Choi matrix
from qiskit.quantum_info import Choi, Kraus
kraus = Kraus(choi)
print(f"Number of Kraus operators: {len(kraus.data)}")
# A perfect gate has exactly one Kraus operator (the unitary itself)
# More operators indicate mixing / decoherence
# Check if the channel is unital (maps identity to identity)
# Unital channels satisfy sum_k K_k K_k^dag = I
K_sum = sum(k @ k.conj().T for k in kraus.data)
print("Channel is unital:", np.allclose(K_sum, np.eye(K_sum.shape[0])))
Coherent vs Incoherent from the Chi Matrix
The Chi matrix gives another lens on noise structure. Inspect it in the Pauli basis to see which error channels are active:
from qiskit.quantum_info import Chi
import numpy as np
# Convert the reconstructed Choi to Chi
chi_matrix = Chi(choi)
chi_data = chi_matrix.data
pauli_labels = ['I', 'X', 'Y', 'Z']
print("Chi matrix (Pauli basis):")
print(" ", " ".join(f" {p} " for p in pauli_labels))
for i, row_label in enumerate(pauli_labels):
row_str = " ".join(f"{chi_data[i,j].real:+.4f}" for j in range(4))
print(f" {row_label} {row_str}")
# For a perfect Hadamard, Chi should have weight only on
# the entries corresponding to the unitary decomposition.
# Non-zero diagonal entries (X, Y, Z) indicate Pauli error rates.
# Non-zero off-diagonal entries indicate coherent error correlations.
print(f"\nDiagonal elements (Pauli error rates):")
for i, label in enumerate(pauli_labels):
print(f" χ_{label}{label} = {chi_data[i,i].real:.6f}")
Diamond Norm Distance
The diamond norm gives the worst-case distinguishability between two channels. It is the most stringent distance measure for quantum channels, relevant when you need guaranteed bounds on error rates:
from qiskit.quantum_info import diamond_norm, Choi, Operator
# Compute diamond norm distance from ideal
ideal_choi = Choi(Operator.from_label("H"))
# The diamond norm of the difference channel
# diamond_norm requires the difference of two Choi matrices
diff = Choi(choi.data - ideal_choi.data)
d_norm = diamond_norm(diff)
print(f"Diamond norm distance from ideal: {d_norm:.6f}")
# Diamond norm >= 0, with 0 meaning identical channels.
# It upper-bounds the trace distance between output states
# for *any* input, including entangled inputs with ancillas.
When to Use Chi vs Choi
- Use Chi when you want to understand the Pauli error decomposition, compare with error models, or produce publication-quality plots of the error channel. Chi is the standard for visualizing QPT results.
- Use Choi when you need numerical stability, want to check complete positivity directly (Choi is PSD if and only if the channel is CP), or need to compute fidelities and norms efficiently.
Noise Model Comparison
Run QPT on a noisy simulation to see how the channel differs from ideal:
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
from qiskit_aer import AerSimulator
noisy_sim = AerSimulator.from_backend(FakeSherbrooke())
job = qpt.run(noisy_sim, shots=8192)
result = job.block_for_results()
choi_noisy = result.analysis_results("state").value
fidelity_noisy = process_fidelity(choi_noisy, ideal_choi)
print(f"Noisy process fidelity: {fidelity_noisy:.4f}")
Resource Scaling
QPT requires a number of circuits that grows exponentially with qubit count. For n qubits, you need d² = 4^n input states and 3^n measurement bases:
| Qubits | Input states (4^n) | Measurement bases (3^n) | Total circuits | Shots at 8192 each |
|---|---|---|---|---|
| 1 | 4 | 3 | 12 | 98,304 |
| 2 | 16 | 9 | 144 | 1,179,648 |
| 3 | 64 | 27 | 1,728 | 14,155,776 |
| 4 | 256 | 81 | 20,736 | 169,869,312 |
| 5 | 1,024 | 243 | 248,832 | ~2 billion |
At 3 qubits, you already need 1,728 circuits. At 4 qubits, you need over 20,000. This makes full QPT impractical beyond 2-3 qubits on real hardware, both because of the circuit count and because statistical noise accumulates: each circuit’s measurement uncertainty propagates into the Choi matrix reconstruction. With more circuits, you need more total shots to maintain the same reconstruction accuracy.
The reconstruction itself also becomes computationally expensive. Maximum likelihood estimation on a 16×16 Choi matrix (2 qubits) is fast. On a 256×256 Choi matrix (4 qubits), it can take hours and requires careful regularization.
QPT vs Alternatives
QPT gives you the most complete information about a gate, but it is also the most expensive protocol. Here is how it compares to the alternatives.
Randomized Benchmarking (RB)
Randomized benchmarking estimates the average gate fidelity by running random sequences of Clifford gates and measuring how quickly the output decays toward the maximally mixed state. It is:
- Much cheaper: O(sequence_lengths × n_seeds) circuits, typically a few hundred total
- SPAM-robust: errors in state preparation and measurement cancel out by design
- Noise-robust: the exponential decay fit averages over many gate sequences
The tradeoff: RB gives you a single number (the average error rate per gate), not the full channel. You cannot extract the noise type, coherent vs incoherent decomposition, or build a noise model from RB alone.
Gate Set Tomography (GST)
GST is a self-consistent protocol that simultaneously characterizes gates, state preparation, and measurement. It does not assume perfect SPAM (state preparation and measurement), which makes it more accurate than standard QPT for high-precision work. However:
- Even more expensive than QPT (more circuit configurations needed for self-consistency)
- Complex analysis: requires specialized software (pyGSTi)
- Best suited for characterizing a small set of gates (1-2 qubits) to very high precision
Direct Fidelity Estimation (DFE)
DFE estimates the process fidelity without reconstructing the full Choi matrix. It works by sampling random Pauli measurements and uses far fewer circuits than full QPT. The tradeoff: you get the fidelity number but not the channel description.
When QPT Is the Right Choice
Use QPT when you need one or more of the following:
- The full channel description for building a noise model to use in simulation
- Noise type diagnosis: distinguishing coherent errors from T1/T2 decay from crosstalk
- Verification of error correction gadgets where you need to confirm the channel is close to the target
- Publication-quality gate characterization data (Chi matrix plots are the standard)
- Comparison of noise models: checking whether your theoretical noise model matches the measured channel
For everything else, start with randomized benchmarking. If RB shows a problem and you need to understand what kind of error it is, then run QPT on that specific gate.
Common Mistakes
Not Running Enough Shots
QPT reconstructs a matrix from measurement statistics, and the reconstruction quality depends directly on the statistical precision of each measurement. With too few shots, the maximum likelihood fitter produces a Choi matrix dominated by statistical noise rather than physical noise.
Rule of thumb: use at least 4096 shots per circuit for single-qubit QPT, and 8192+ for two-qubit QPT. If you are comparing small differences between channels (e.g., before and after a calibration tweak), you may need 16384 or more.
Confusing Process Fidelity with Average Gate Fidelity
These are different numbers. Process fidelity F_pro is the overlap between Choi matrices. Average gate fidelity (AGF) is the fidelity averaged uniformly over all input states. They are related by:
AGF = (d * F_pro + 1) / (d + 1)
For a single qubit (d = 2), a process fidelity of 0.95 corresponds to an AGF of 0.967. Hardware vendors report AGF because it is always higher than the process fidelity. When comparing your QPT results to published gate fidelities, make sure you are comparing the same metric.
Ignoring SPAM Errors
Standard QPT assumes that your state preparation and measurement are perfect. On real hardware, they are not. SPAM errors fold directly into the reconstructed Choi matrix, making the gate look worse than it actually is (or occasionally better, if errors cancel).
If SPAM errors are a concern:
- Run state tomography separately to quantify your SPAM quality
- Consider gate set tomography (GST), which is SPAM-robust by design
- At minimum, report your SPAM fidelities alongside your QPT results so readers can judge the reliability
Two-Qubit QPT on Non-Adjacent Qubits
If you run two-qubit QPT on qubits that are not physically adjacent, the transpiler inserts SWAP gates to route the two-qubit gate. Those SWAP gates add noise that gets folded into your reconstructed channel. You end up characterizing the routed gate (including SWAPs), not the native two-qubit gate.
Always check your backend’s coupling map and choose adjacent qubits. In Qiskit, you can verify this:
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
backend = FakeSherbrooke()
coupling_map = backend.coupling_map
print("Adjacent qubit pairs:", coupling_map.get_edges())
# Pick an adjacent pair for your two-qubit QPT
# e.g., if (0, 1) is in the coupling map, use qubits 0 and 1
Practical Considerations
QPT is resource-intensive. For a 1-qubit gate you need 12 circuits; for 2 qubits, 144. On real hardware this means significant credit usage and queue time.
When running on real hardware, batch your circuits where possible. Qiskit Experiments handles batching internally, but be aware that hardware queue times can be long for 144-circuit jobs. If the backend supports dynamic circuits or mid-circuit measurement, check whether qiskit-experiments can take advantage of these features to reduce overhead.
For routine gate quality checks, prefer randomized benchmarking (see the companion tutorial), which is more efficient and more noise-robust. Reserve QPT for situations where you need the full process description, such as building noise models for simulation or verifying an error correction gadget.
Was this tutorial helpful?