Qiskit Advanced Free 13/61 in series 75 minutes

Characterizing Real Quantum Hardware Noise with Qiskit

Implement noise characterization using Qiskit Experiments: T1 inversion recovery, T2 Ramsey, single-qubit RB, two-qubit RB, and cross-resonance tomography on a fake backend.

What you'll learn

  • noise characterization
  • randomized benchmarking
  • Qiskit
  • T1
  • T2
  • gate fidelity
  • calibration

Prerequisites

  • Strong Python skills
  • Solid quantum computing foundations
  • Linear algebra and complex numbers

Why Characterize Noise?

Running circuits on real quantum hardware produces errors. Understanding the nature and magnitude of those errors is essential for choosing error mitigation strategies, routing circuits to the lowest-error qubits, and validating whether a device is suitable for a given algorithm. IBM’s Qiskit Experiments library provides production-grade implementations of the standard noise characterization protocols used by quantum hardware teams.

This tutorial runs all major characterization experiments on a fake backend (which simulates a real device’s noise) so you can execute the full workflow without access to physical hardware.

Setup

pip install qiskit qiskit-aer qiskit-experiments matplotlib
# Requires: qiskit_ibm_runtime
from qiskit_ibm_runtime.fake_provider import FakeNairobiV2
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

# Build an AerSimulator that mimics FakeNairobi's noise
fake_backend = FakeNairobiV2()
noise_model = NoiseModel.from_backend(fake_backend)
sim = AerSimulator.from_backend(fake_backend)

print(f"Backend: {fake_backend.name}")
print(f"Qubits:  {fake_backend.num_qubits}")

Experiment 1: T1 Measurement (Inversion Recovery)

T1 is the longitudinal relaxation time, the timescale over which a qubit initialized in |1> decays back to |0> due to energy dissipation. The measurement protocol is:

  1. Apply X gate to prepare |1>
  2. Wait for delay time tau
  3. Measure

The probability of measuring |1> decays exponentially: P(1) = exp(-tau/T1). Fitting this decay gives T1.

from qiskit_experiments.library import T1
from qiskit_experiments.framework import BatchExperiment

qubit = 0
delays_us = np.linspace(1e-6, 300e-6, 20)  # 1 to 300 microseconds

t1_exp = T1(physical_qubits=(qubit,), delays=delays_us)
t1_exp.set_transpile_options(backend=sim)

t1_data = t1_exp.run(sim, shots=1024).block_for_results()

t1_result = t1_data.analysis_results("T1")
t1_value = t1_result.value.nominal_value
t1_error = t1_result.value.std_dev

print(f"T1 for qubit {qubit}: {t1_value * 1e6:.1f} +/- {t1_error * 1e6:.1f} us")

# Plot the decay curve
fig = t1_data.figure(0)
fig.savefig("t1_decay.png")
print("Saved t1_decay.png")

A typical superconducting qubit has T1 between 50 and 300 microseconds. If T1 is short relative to your circuit execution time, amplitude damping errors dominate.

Experiment 2: T2 Ramsey (Dephasing Time)

T2 is the transverse relaxation time, measuring how long a qubit can maintain superposition coherence. T2 includes T1 contributions (T2 <= 2*T1) plus pure dephasing.

The Ramsey protocol:

  1. Apply H to prepare |+>
  2. Wait for delay tau
  3. Apply a small detuning rotation (optional, to observe oscillation)
  4. Apply H
  5. Measure

The signal oscillates and decays: P(0) = (1 + exp(-tau/T2) * cos(2pif*tau)) / 2.

# Requires: qiskit_experiments
from qiskit_experiments.library import T2Ramsey

qubit = 0
delays_us = np.linspace(1e-6, 200e-6, 25)
osc_freq = 2e3   # 2 kHz artificial detuning to make the oscillation visible

t2_exp = T2Ramsey(
    physical_qubits=(qubit,),
    delays=delays_us,
    osc_freq=osc_freq,
)
t2_exp.set_transpile_options(backend=sim)

t2_data = t2_exp.run(sim, shots=1024).block_for_results()

t2_result  = t2_data.analysis_results("T2star")
t2_value   = t2_result.value.nominal_value
freq_result = t2_data.analysis_results("Frequency")

print(f"T2* for qubit {qubit}: {t2_value * 1e6:.1f} us")
print(f"Qubit frequency offset: {freq_result.value.nominal_value:.1f} Hz")

fig = t2_data.figure(0)
fig.savefig("t2_ramsey.png")

T2 directly limits the maximum useful circuit depth. A circuit with total execution time much longer than T2 will produce mostly noise.

Experiment 3: Single-Qubit Randomized Benchmarking

Randomized Benchmarking (RB) measures the average Clifford gate error rate without the overhead of full process tomography. The protocol:

  1. Apply a random sequence of m Clifford gates to a qubit
  2. Apply the inverse (one more Clifford that undoes the sequence)
  3. Measure: ideal result is always |0>
  4. Repeat for many random sequences and average
  5. Plot survival probability vs sequence length m

The decay is exponential: P(survival) = A * p^m + B, where the depolarizing parameter p relates to the error per Clifford (EPC) by EPC = (1 - p) / 2.

from qiskit_experiments.library import StandardRB

qubit = 0
lengths = [1, 5, 10, 25, 50, 100, 200, 400]
num_samples = 20   # random sequences per length

rb_exp = StandardRB(
    physical_qubits=(qubit,),
    lengths=lengths,
    num_samples=num_samples,
    seed=42,
)
rb_exp.set_transpile_options(backend=sim)

rb_data = rb_exp.run(sim, shots=1024).block_for_results()

epc_result = rb_data.analysis_results("EPC")
epc = epc_result.value.nominal_value

print(f"Single-qubit EPC (qubit {qubit}): {epc:.2e}")
print(f"Average gate fidelity:           {1 - epc:.6f}")
print(f"Average gate error:              {epc:.2e}")

fig = rb_data.figure(0)
fig.savefig("rb_1q.png")

Single-qubit EPC below 1e-3 is considered high quality. Values above 1e-2 indicate significant problems.

Experiment 4: Two-Qubit Randomized Benchmarking

Two-qubit gates (CX, CZ, ECR) have much higher error rates than single-qubit gates. Two-qubit RB characterizes the average error of the two-qubit Clifford group, which includes the native two-qubit gate:

from qiskit_experiments.library import StandardRB

# Use a connected qubit pair from the fake backend
coupling_map = fake_backend.coupling_map
qubit_pair = list(coupling_map.get_edges())[0]
print(f"Benchmarking qubit pair: {qubit_pair}")

lengths_2q = [1, 3, 5, 10, 20, 50, 100]
num_samples_2q = 15

rb_2q_exp = StandardRB(
    physical_qubits=qubit_pair,
    lengths=lengths_2q,
    num_samples=num_samples_2q,
    seed=42,
)
rb_2q_exp.set_transpile_options(backend=sim)

rb_2q_data = rb_2q_exp.run(sim, shots=1024).block_for_results()

epc_2q = rb_2q_data.analysis_results("EPC").value.nominal_value
print(f"Two-qubit EPC (qubits {qubit_pair}): {epc_2q:.2e}")
print(f"Two-qubit gate fidelity:            {1 - epc_2q:.6f}")

Typical superconducting two-qubit EPC values range from 5e-3 to 3e-2. Trapped ion devices often achieve 1e-3 or better.

Experiment 5: Interleaved RB for Individual Gate Characterization

Standard RB gives the average Clifford error. Interleaved RB isolates the error of a specific gate by interleaving it between random Cliffords:

from qiskit_experiments.library import InterleavedRB
from qiskit.circuit.library import CXGate

qubit_pair = list(coupling_map.get_edges())[0]

irb_exp = InterleavedRB(
    interleaved_element=CXGate(),
    physical_qubits=qubit_pair,
    lengths=[1, 3, 5, 10, 20, 50],
    num_samples=15,
    seed=42,
)
irb_exp.set_transpile_options(backend=sim)

irb_data = irb_exp.run(sim, shots=1024).block_for_results()

gate_error = irb_data.analysis_results("EPC").value.nominal_value
print(f"CX gate error (qubits {qubit_pair}): {gate_error:.2e}")
print(f"CX gate fidelity:                   {1 - gate_error:.6f}")

Experiment 6: Batch Characterization Across All Qubits

For a device-level overview, run T1 and single-qubit RB across every qubit simultaneously using BatchExperiment:

# Requires: qiskit_ibm_runtime
from qiskit_experiments.framework import BatchExperiment

num_qubits = fake_backend.num_qubits
delays_us  = np.linspace(1e-6, 200e-6, 15)
rb_lengths = [1, 10, 50, 100, 300]

t1_exps = []
rb_exps = []
for q in range(num_qubits):
    t1_q = T1(physical_qubits=(q,), delays=delays_us)
    rb_q = StandardRB(physical_qubits=(q,), lengths=rb_lengths,
                      num_samples=10, seed=q)
    t1_exps.append(t1_q)
    rb_exps.append(rb_q)

batch_t1 = BatchExperiment(t1_exps, flatten_results=False)
batch_t1.set_transpile_options(backend=sim)
batch_t1_data = batch_t1.run(sim, shots=512).block_for_results()

print("\nT1 Summary:")
for q in range(num_qubits):
    child = batch_t1_data.child_data(q)
    try:
        t1_q = child.analysis_results("T1").value.nominal_value
        print(f"  Qubit {q}: T1 = {t1_q * 1e6:.1f} us")
    except Exception:
        print(f"  Qubit {q}: T1 fit failed")

Interpreting Results and Comparing to Published Specs

IBM Quantum publishes calibration data for each device. After running your own characterization, compare:

# Requires: qiskit_ibm_runtime
# Fetch published specs from the fake backend
print("\nPublished device specs (from fake backend calibration):")
for q in range(fake_backend.num_qubits):
    props = fake_backend.qubit_properties[q]
    t1_pub = props.t1
    t2_pub = props.t2
    freq_pub = props.frequency
    print(f"  Qubit {q}: T1={t1_pub*1e6:.1f}us, T2={t2_pub*1e6:.1f}us, "
          f"freq={freq_pub/1e9:.4f} GHz")

# Compare to your measured values
print("\nComparing measured T1 to published:")
for q in range(num_qubits):
    published_t1 = fake_backend.qubit_properties[q].t1 * 1e6
    print(f"  Qubit {q}: published={published_t1:.1f}us")

Large discrepancies between measured and published values indicate:

  • The device has drifted since last calibration (common over hours or days)
  • Crosstalk or readout errors are corrupting the measurement
  • The wrong qubits are being characterized

Building a Noise Model from Characterization Data

Use your measured values to build a custom noise model for simulation:

from qiskit_aer.noise import NoiseModel, thermal_relaxation_error, depolarizing_error

def noise_model_from_data(t1_values_us, t2_values_us, epc_1q_values, 
                           epc_2q_dict, gate_time_ns=50):
    """Build an Aer noise model from experimental characterization data."""
    noise_model = NoiseModel()
    gate_time_s = gate_time_ns * 1e-9
    
    for q, (t1_us, t2_us) in enumerate(zip(t1_values_us, t2_values_us)):
        t1_s = t1_us * 1e-6
        t2_s = min(t2_us * 1e-6, 2 * t1_s)  # enforce T2 <= 2*T1
        
        # Thermal relaxation during single-qubit gates
        thermal_err = thermal_relaxation_error(t1_s, t2_s, gate_time_s)
        # Depolarizing error from RB
        dep_err = depolarizing_error(epc_1q_values[q], 1)
        combined = thermal_err.compose(dep_err)
        noise_model.add_quantum_error(combined, ["rz", "sx", "x"], [q])
    
    for (q0, q1), epc_2q in epc_2q_dict.items():
        cx_err = depolarizing_error(epc_2q, 2)
        noise_model.add_quantum_error(cx_err, ["cx"], [q0, q1])
    
    return noise_model

# Example with placeholder data
t1_vals = [120.0, 95.0, 140.0, 80.0, 110.0, 130.0, 90.0]
t2_vals = [80.0,  60.0, 100.0, 55.0, 75.0,  90.0,  65.0]
epc_1q  = [3e-4, 5e-4, 2.5e-4, 7e-4, 4e-4, 3.5e-4, 6e-4]
epc_2q  = {(0, 1): 8e-3, (1, 2): 12e-3, (2, 3): 9e-3}

custom_noise = noise_model_from_data(t1_vals, t2_vals, epc_1q, epc_2q)
print("Custom noise model built from characterization data.")
print(custom_noise)

Key Takeaways

  • T1 (inversion recovery) measures energy relaxation; T2 (Ramsey) measures coherence decay. Both limit the maximum useful circuit depth.
  • Standard RB gives the average Clifford gate fidelity with far fewer circuits than process tomography.
  • Interleaved RB isolates the error of a single gate by comparing to the standard RB baseline.
  • Two-qubit gate errors typically dominate over single-qubit errors by one to two orders of magnitude.
  • BatchExperiment runs multiple characterization protocols in parallel, giving a full device overview in a single submission.
  • Comparing measured values to published calibration data reveals device drift and crosstalk effects.

Was this tutorial helpful?