Noise and error correction¶
Add a noise model to a circuit, then estimate or sample under noise, from zero-cost analytical damping to exact density-matrix channels. For error correction with Stim and Sinter, see QEC sampling with Stim and Sinter.
import math
import maestro
from maestro.circuits import QuantumCircuit
bell = QuantumCircuit()
bell.h(0)
bell.cx(0, 1)
noise = maestro.NoiseModel()
noise.set_all_depolarizing(num_qubits=2, p=0.05)
result = maestro.noisy_estimate(bell, ["ZZ", "XX", "YY"], noise)
print("noisy:", result["expectation_values"])
print("ideal:", result["ideal_expectation_values"])
noisy: [0.871111111111111, 0.871111111111111, -0.871111111111111]
ideal: [0.9999999999999998, 0.9999999999999998, -0.9999999999999998]
noisy_estimate runs the circuit once without noise and damps each expectation
value analytically, so it costs nothing extra. The other functions trade speed
for more physical detail:
Function |
Cost |
Noise mechanism |
Use for |
|---|---|---|---|
|
One noiseless run |
Analytical Pauli damping |
Screening many circuits quickly |
|
N noisy runs |
Random Pauli errors after each gate |
Noise that builds up with depth |
|
N noisy runs |
Gate-by-gate Pauli errors, plus shot noise |
Sampled counts under noise |
|
N noisy runs |
Systematic rotation errors |
Calibration errors |
|
N noisy runs |
Coherent errors, plus shot noise |
Sampled counts under coherent errors |
|
N noisy runs |
Every configured noise layer |
Device-level expectation values |
|
N noisy runs |
Every configured noise layer, plus shot noise |
Device-level sampling |
|
N inner products |
Every noise layer |
Fidelity to the ideal state |
|
Path integral |
Readout error correction |
Single-outcome probabilities |
N is noise_realizations. Analytical and Monte Carlo noise compares the analytical
and Monte Carlo modes on one circuit.
Building a noise model¶
NoiseModel holds error rates per qubit. Pauli noise applies after every gate
that touches the qubit. Here each of four flipped qubits gets a different
channel, and ⟨Z⟩ shows which ones the channel can disturb:
nm = maestro.NoiseModel()
nm.set_depolarizing(qubit=0, p=0.01) # X, Y and Z each with probability p/3
nm.set_dephasing(qubit=1, p=0.02) # Z errors, which leave <Z> alone
nm.set_bit_flip(qubit=2, p=0.01) # X errors
nm.set_qubit_noise(qubit=3, px=0.005, py=0.002, pz=0.01) # any Pauli channel
flipped4 = QuantumCircuit()
for q in range(4):
flipped4.x(q)
print(maestro.noisy_estimate(flipped4, ["ZIII", "IZII", "IIZI", "IIIZ"], nm)["expectation_values"])
[-0.9866666666666667, -1.0, -0.98, -0.986]
set_all_depolarizing(num_qubits, p) sets the same depolarizing rate on every
qubit.
Noise at a point in the circuit¶
A Pauli channel can also sit at a fixed point in the circuit, like a Stim noise
instruction. probabilities has 4**len(qubits) entries; base-4 digit k of an
index selects I, X, Y or Z on qubits[k].
p = 0.01
channel = QuantumCircuit()
channel.h(0)
channel.cx(0, 1)
channel.pauli_channel([0], [1 - p, p / 3, p / 3, p / 3]) # DEPOLARIZE1(p)
channel.pauli_channel([0, 1], [1 - p] + [p / 15] * 15) # DEPOLARIZE2(p)
channel.measure_all()
# Statevector and MPS draw one Pauli per noise realization.
counts = channel.full_noise_execute(maestro.NoiseModel(), shots=1000, noise_realizations=1000)["counts"]
print(counts)
{'10': 4, '01': 3, '11': 509, '00': 484}
To keep a basis change that belongs to a measurement or reset out of a
NoiseModel’s gate noise, pass noiseless=True to h, s or sdg.
Gate-by-gate Monte Carlo¶
noisy_estimate_montecarlo injects random Pauli errors after each gate and
averages over many noisy runs, so noise accumulates with circuit depth.
noisy_execute does the same for sampled counts:
mc = maestro.noisy_estimate_montecarlo(
bell, ["ZZ", "XX"], noise, noise_realizations=200, noise_seed=42
)
print("Monte Carlo:", mc["expectation_values"])
measured = QuantumCircuit()
measured.h(0)
measured.cx(0, 1)
measured.measure_all()
print(maestro.noisy_execute(measured, noise, shots=1024, noise_realizations=64, noise_seed=42)["counts"])
Monte Carlo: [0.8699999999999999, 0.7899999999999998]
{'10': 62, '01': 50, '00': 461, '11': 451}
In the estimate functions, noise_realizations is the number of independent
noise samples averaged. In the execute functions, it is the number of batches
the shots are split into, so at most shots batches run.
Coherent errors¶
Coherent noise applies deterministic rotations after gates instead of random Pauli flips, modelling systematic over-rotation and phase drift:
coherent = maestro.NoiseModel()
# Rotate by epsilon = 2*arcsin(sqrt(p)) about a body diagonal; its Pauli twirl is DEPOLARIZE1(p).
coherent.set_coherent_depolarizing(qubit=0, p=0.01)
coherent.set_coherent_dephasing(qubit=1, p=0.01) # about Z
coherent.set_coherent_rotation(qubit=0, rx=0.01, ry=0.0, rz=0.05) # explicit angles
print(maestro.coherent_estimate(bell, ["ZZ", "XX"], coherent, noise_realizations=100)["expectation_values"])
[0.9866579562352326, 0.9334392239085396]
Each setter replaces only the component it names, so qubit 0 above has both the diagonal rotation and the explicit angles.
Thermal relaxation (T1 and T2)¶
Thermal relaxation models energy decay (T1) and dephasing (T2) from gate durations:
thermal = maestro.NoiseModel()
thermal.set_thermal_relaxation(qubit=0, gate_time_s=30e-9, t1_s=100e-6, t2_s=80e-6) # 1-qubit gates
thermal.set_thermal_relaxation_2q(qubit=0, gate_time_s=300e-9, t1_s=100e-6, t2_s=80e-6) # 2-qubit gates
print(thermal.has_thermal_relaxation())
True
T2 must be at most 2·T1. Density-matrix and supported MPO backends apply the
exact thermal channel. Sampled statevector and MPS runs approximate
T1 < T2 ≤ 2·T1 with an effective T2 = T1 and emit one RuntimeWarning naming
the affected qubits; use a density matrix for faithful evolution in that range.
Exact channels (density matrix and MPO)¶
Some channels have no Pauli-trajectory form and need an exact mixed-state
backend, DensityMatrix or MatrixProductOperator:
exact = maestro.NoiseModel()
exact.set_phase_damping(qubit=0, gamma=0.05)
exact.set_generalized_amplitude_damping(qubit=1, gamma=0.02, excited_population=0.05)
exact.set_correlated_phase_flip(q1=0, q2=1, probability=0.01, correlation=0.8)
# Any channel given as Kraus operators with sum_k E_k^† E_k = I, here a bit flip.
p_flip = 0.01
exact.set_kraus_channel([0], [
[[math.sqrt(1 - p_flip), 0], [0, math.sqrt(1 - p_flip)]],
[[0, math.sqrt(p_flip)], [math.sqrt(p_flip), 0]],
])
flipped = QuantumCircuit()
flipped.x(0)
flipped.cx(0, 1)
for method in ["DensityMatrix", "MatrixProductOperator"]:
config = maestro.SimulatorConfig(simulation_type=getattr(maestro.SimulationType, method))
print(method, flipped.full_noise_estimate(["ZI", "IZ"], exact, config=config)["expectation_values"])
DensityMatrix [-0.9603999999999975, -0.9424000000000017]
MatrixProductOperator [-0.9604000000000024, -0.9424000000000017]
The statevector backend rejects these channels with a ValueError instead of
approximating them. set_phase_damping_from_time(qubit, gate_time_s, t_phi_s)
sets phase damping from a dephasing time.
Crosstalk and two-qubit errors¶
pairwise = maestro.NoiseModel()
pairwise.set_crosstalk(q1=0, q2=1, strength=0.005) # ZZ crosstalk between neighbours
pairwise.set_2q_depolarizing(q1=0, q2=1, p=1.8e-3) # after two-qubit gates such as CX or CZ
print(pairwise.has_crosstalk(), pairwise.has_any_2q_depolarizing())
True True
Readout error¶
Readout error flips the classical result when a measurement writes its bit. Rates follow the measured qubit, and later classical conditions see the noisy result:
readout = maestro.NoiseModel()
readout.set_readout_error(qubit=0, p_meas1_prep0=0.003, p_meas0_prep1=0.06) # asymmetric
readout.set_readout_error(qubit=1, p_meas1_prep0=0.003, p_meas0_prep1=0.06)
ones = QuantumCircuit()
ones.x(0)
ones.x(1)
ones.measure_all()
print(maestro.noisy_execute(ones, readout, shots=2000, noise_seed=7)["counts"])
# Probability of reading "11" once readout error is included, from the path integral
print(bell.noisy_prob("11", readout)["probability"]) # 0.5 * (1 - 0.06)**2
{'00': 5, '10': 116, '01': 115, '11': 1764}
0.44179999999999986
noisy_prob expands to first order in the readout error rates, so outcomes two
flips away from "11", such as "00", don’t contribute.
set_all_readout_error(num_qubits, p_error) sets a symmetric rate on every
qubit.
The noise_seed argument (also accepted as seed) seeds noise injection and, unless
SimulatorConfig.seed is set, measurement and readout randomness as well. An
explicit config seed takes precedence for simulator randomness. Without a seed,
each call uses fresh random streams. Each noise realization derives its own
simulator seed, so seeded runs are reproducible without repeating readout draws
across batches. coherent_execute follows the same rules.
Idle noise during delays¶
set_idle_noise configures relaxation, dephasing and coherent detuning that act
during qc.delay(qubit, duration). Active OU processes also evolve through the
delay:
idle = maestro.NoiseModel()
idle.set_idle_noise(
qubit=0,
t1=100e-6, # seconds
t2=80e-6, # at most 2 * t1
excited_population=0.01,
detuning_hz=5e3,
)
ramsey = QuantumCircuit()
ramsey.h(0)
ramsey.delay(0, 20e-6)
ramsey.h(0)
ramsey.measure_all()
print(ramsey.full_noise_execute(idle, shots=1024, seed=3)["counts"])
{'0': 812, '1': 212}
Every layer together¶
full_noise_execute, full_noise_estimate and noisy_fidelity apply every
configured layer in physical order:
device = maestro.NoiseModel()
device.set_all_correlated_ou(num_qubits=2, sigma=15.0, alpha=0.5, gate_time=100e-9)
device.set_all_coherent_depolarizing(num_qubits=2, p=0.002)
device.set_all_readout_error(num_qubits=2, p_error=0.01)
print("counts:", measured.full_noise_execute(device, shots=1024, noise_realizations=64, seed=5)["counts"])
print("values:", bell.full_noise_estimate(["ZZ", "XX"], device, noise_realizations=100, seed=5)["expectation_values"])
fid = bell.noisy_fidelity(device, noise_realizations=100, seed=5)
print(f"fidelity: {fid['fidelity']:.4f} +/- {fid['std_error']:.4f}")
counts: {'01': 17, '10': 11, '11': 502, '00': 494}
values: [0.9950287395340883, 0.9900981329174434]
fidelity: 0.9937 +/- 0.0003
Error correction with Stim and Sinter¶
To sample Stim circuits through Maestro, including from sinter collect with a
decoder, see QEC sampling with Stim and Sinter.