Mitigation API Reference
July 7, 2026 · View on GitHub
The mitigation package provides six error mitigation techniques for
noise reduction on NISQ hardware. Each technique exploits a different
aspect of the noise: ZNE extrapolates through noise amplification,
PEC cancels errors via quasi-probability sampling, CPDR trains a
regression model on near-Clifford circuits, DD suppresses decoherence
on idle qubits, symmetry verification exploits the XY Hamiltonian's
Z2 parity conservation, and Mitiq integration provides production-grade
implementations.
6 modules, 17 public symbols.
Architecture
Raw Noisy Circuit
│
├── ZNE: gate_fold_circuit() → zne_extrapolate()
│ Amplify noise → Richardson extrapolation → zero-noise estimate
│
├── PEC: pauli_twirl_decompose() → pec_sample()
│ Quasi-probability decomposition → Monte Carlo sampling
│
├── CPDR: generate_training_circuits() → cpdr_mitigate()
│ Near-Clifford training → linear regression → corrected value
│
├── DD: insert_dd_sequence()
│ XY4/X2/CPMG pulses on idle qubits → decoherence suppression
│
├── Symmetry: parity_postselect() / symmetry_expand()
│ Z₂ conservation → discard/redistribute wrong-parity shots
│
└── Mitiq: zne_mitigated_expectation() / ddd_mitigated_expectation()
Production Mitiq wrappers → battle-tested implementations
Compound Mitigation Strategy
The modules are designed to be composed. The recommended compound mitigation pipeline for Kuramoto-XY circuits:
- DD at transpiler level (idle qubit protection)
- Symmetry verification at measurement level (free error detection)
- CPDR or ZNE at estimation level (value correction)
CPDR is preferred over ZNE for deep circuits (> 50 layers) because it does not require depth amplification. ZNE is simpler and better for shallow circuits (< 30 layers).
Module Reference
1. zne — Zero-Noise Extrapolation
Reference: Giurgica-Tiron et al., "Digital zero noise extrapolation for quantum error mitigation", IEEE QCE 2020.
Principle
Amplify the circuit's effective noise by folding the unitary:
G → G (G†G)^k, then measure expectation at multiple noise levels
and extrapolate to zero noise via polynomial fit.
gate_fold_circuit(circuit, scale)
Global unitary folding. scale must be an odd positive integer:
- scale=1: original circuit (no folding)
- scale=3:
G G† G(one fold, 3x noise) - scale=5:
G G† G G† G(two folds, 5x noise)
Measurements are stripped before folding and re-appended. The inverse
G† is computed via circuit.inverse().
from scpn_quantum_control.mitigation import gate_fold_circuit
folded_3 = gate_fold_circuit(circuit, scale=3)
folded_5 = gate_fold_circuit(circuit, scale=5)
Raises ValueError if scale is not a positive odd integer.
zne_extrapolate(noise_scales, expectation_values, order=1)
Richardson extrapolation to zero noise. Fits polynomial of degree order
through (scale, expectation) data points and evaluates at scale=0.
from scpn_quantum_control.mitigation import zne_extrapolate
result = zne_extrapolate(
noise_scales=[1, 3, 5],
expectation_values=[0.85, 0.72, 0.61],
order=1,
)
print(result.zero_noise_estimate) # ≈ 0.91
print(result.fit_residual) # L2 residual
ZNEResult
| Field | Type | Description |
|---|---|---|
noise_scales | list[int] | Folding factors used |
expectation_values | list[float] | Measured values at each scale |
zero_noise_estimate | float | Extrapolated zero-noise value |
fit_residual | float | RMS of polynomial fit residual |
2. pec — Probabilistic Error Cancellation
Reference: Temme et al., "Error Mitigation for Short-Depth Quantum Circuits", PRL 119, 180509 (2017).
Principle
Decompose the ideal (noiseless) operation as a quasi-probability distribution over noisy implementable operations. Sample from this distribution with signed weights to cancel the error on average.
For a single-qubit depolarizing channel with error rate p:
I_ideal = q_I · I + q_X · X + q_Y · Y + q_Z · Z
where:
q_I = 1 + 3p/(4-4p)
q_XYZ = -p/(4-4p)
The negative coefficients mean some samples are subtracted. The sampling overhead (variance increase) is gamma = sum(|q_i|) per gate, and gamma^n_gates total.
pauli_twirl_decompose(gate_error_rate, n_qubits=1)
Returns quasi-probability coefficients for the local Pauli inverse channel.
For n_qubits=1, the basis is [I, X, Y, Z]. For n_qubits>1, the output is
the tensor product over the local [I, X, Y, Z] basis and therefore has length
4**n_qubits.
from scpn_quantum_control.mitigation import pauli_twirl_decompose
coeffs = pauli_twirl_decompose(0.01)
# array([ 1.00757576, -0.00252525, -0.00252525, -0.00252525])
two_qubit_coeffs = pauli_twirl_decompose(0.01, n_qubits=2)
assert len(two_qubit_coeffs) == 16
n_qubits must be a positive integer. The coefficient decomposition supports
local tensor-product depolarising channels; correlated multi-qubit noise models
require a separate characterised inverse channel.
Rust acceleration: scpn_quantum_engine.pec_coefficients() produces
identical single-qubit coefficients. Parity verified in
test_rust_path_benchmarks.py.
pec_sample(circuit, gate_error_rate, n_samples, observable_qubit=0, rng=None)
Monte Carlo PEC estimation of <Z> on observable_qubit:
- For each sample: insert random Pauli corrections after each gate, sampled from the quasi-probability distribution
- Simulate the corrected circuit
- Accumulate
gamma_total * sign * <Z>over all samples - Average = mitigated expectation
from scpn_quantum_control.mitigation import pec_sample
result = pec_sample(circuit, gate_error_rate=0.01, n_samples=1000)
print(result.mitigated_value)
print(result.overhead) # gamma^n_gates
PECResult
| Field | Type | Description |
|---|---|---|
mitigated_value | float | PEC-corrected expectation |
overhead | float | Total sampling overhead gamma^n_gates |
n_samples | int | Number of Monte Carlo samples |
sign_distribution | list[float] | Per-sample signs (for diagnostics) |
3. cpdr — Clifford Perturbation Data Regression
Reference: Zhang et al., "Clifford Perturbation Data Regression for Quantum Error Mitigation", arXiv:2412.09518 (Dec 2024).
Principle
Generate near-Clifford training circuits by snapping non-Clifford
rotation gates to their nearest Clifford angle (multiples of pi/2)
and adding small perturbations. Since near-Clifford circuits can be
simulated efficiently, compute their ideal expectation values. Run the
same circuits on the noisy backend. Fit a linear regression
noisy = slope * ideal + intercept. Invert the regression to correct
the target circuit's noisy output.
CPDR outperforms ZNE on IBM Eagle/Heron for deep circuits because it does not require circuit depth amplification. The training circuits have the same depth as the target — they just have different angles.
generate_training_circuits(target_circuit, n_training=20, perturbation_scale=0.1, seed=42)
Creates n_training near-Clifford variants:
- Find all rotation gates (RZ, RY, RX, P, U1, RXX, RYY, RZZ)
- For each rotation: snap to nearest Clifford angle, add N(0, sigma) perturbation
- Re-append measurements
Clifford angles: {0, pi/2, pi, 3pi/2, 2pi}.
compute_ideal_values(circuits, observable_qubits=None)
Compute ideal <Z> via statevector simulation. For near-Clifford circuits
this is exact (though the implementation uses full statevector, not
Clifford-specific simulation).
compute_noisy_values_from_counts(counts_list, n_qubits, observable_qubits=None)
Extract <Z> from hardware measurement counts. Handles arbitrary
bitstring formats.
fit_regression(ideal_values, noisy_values)
Linear regression: noisy = slope * ideal + intercept.
Returns (slope, intercept, R^2).
cpdr_mitigate(raw_noisy_value, ideal_training, noisy_training)
Apply CPDR correction: corrected = (raw - intercept) / slope.
from scpn_quantum_control.mitigation import cpdr_mitigate
result = cpdr_mitigate(
raw_noisy_value=0.72,
ideal_training=[0.9, 0.85, 0.88, ...],
noisy_training=[0.73, 0.70, 0.71, ...],
)
print(result.mitigated_value)
print(result.regression_r_squared)
cpdr_full_pipeline(target_circuit, target_counts, run_on_backend, ...)
End-to-end: generate training → simulate ideal → run noisy → regress → correct.
The run_on_backend callable takes a list of circuits and returns counts.
CPDRResult
| Field | Type | Description |
|---|---|---|
raw_value | float | Uncorrected noisy measurement |
mitigated_value | float | CPDR-corrected value |
n_training_circuits | int | Number of training circuits used |
regression_r_squared | float | R^2 of the regression fit |
regression_slope | float | Slope of noisy vs ideal |
regression_intercept | float | Intercept |
4. dd — Dynamical Decoupling
Reference: Viola et al., PRL 82, 2417 (1999).
Principle
Insert pi-pulse sequences on idle qubits to refocus low-frequency noise. The pulse sequence averages out the qubit-environment coupling to first order, extending the effective T2.
DDSequence
Enum with three supported sequences:
| Sequence | Gates | Reference | Best For |
|---|---|---|---|
XY4 | X-Y-X-Y | Viola et al. 1999 | General dephasing + amplitude noise |
X2 | X-X | Spin echo | Pure dephasing |
CPMG | Y-X-Y-X | Meiboom & Gill 1958 | Correlated noise |
insert_dd_sequence(circuit, idle_qubits, sequence=DDSequence.XY4)
Appends DD pulses to the specified idle qubits after existing gates.
from scpn_quantum_control.mitigation import DDSequence, insert_dd_sequence
protected = insert_dd_sequence(circuit, idle_qubits=[2, 3], sequence=DDSequence.XY4)
For transpiler-level insertion (respecting circuit timing), use
HardwareRunner.transpile_with_dd() which integrates with Qiskit's
PadDynamicalDecoupling pass.
5. symmetry_verification — Z2 Parity Conservation
Reference: Bonet-Monroig et al., "Low-cost error mitigation by symmetry verification", Phys. Rev. A 98, 062339 (2018).
Principle
The XY Hamiltonian conserves total Z2 parity: [H_XY, P] = 0 where P = tensor(Z_i). This is proven by the DLA decomposition: DLA(N) = su(2^(N-1)) + su(2^(N-1)), where the two sectors are the even- and odd-parity subspaces.
Consequence: any measurement outcome whose parity differs from the initial state's parity is guaranteed to be a hardware error. This provides error detection at zero circuit overhead.
Two strategies:
- Post-selection: discard wrong-parity outcomes (cleaner, loses shots)
- Symmetry expansion: flip LSB to correct parity (preserves shot count, small bias)
bitstring_parity(bitstring)
Returns 0 (even) or 1 (odd) = count of '1' bits mod 2.
initial_state_parity(omega)
Computes the dominant parity sector of |psi_0> = tensor(Ry(omega_i)|0>). For small angles (omega_i << pi), state is near |00...0> → even parity.
parity_postselect(counts, expected_parity)
Filters measurement counts by parity. Returns SymmetryVerificationResult:
| Field | Type | Description |
|---|---|---|
raw_counts | dict | Original counts |
verified_counts | dict | Parity-correct counts |
rejected_counts | dict | Parity-violating counts |
raw_shots | int | Total shots |
verified_shots | int | Accepted shots |
rejected_shots | int | Rejected shots |
rejection_rate | float | Fraction rejected |
expected_parity | int | 0 or 1 |
from scpn_quantum_control.mitigation import parity_postselect
result = parity_postselect(counts, expected_parity=0)
print(f"Rejection rate: {result.rejection_rate:.1%}")
# On noiseless simulator: 0%. On hardware: typically 5-30%.
symmetry_expand(counts, expected_parity)
Redistributes wrong-parity outcomes by flipping the least-significant bit. Preserves total shot count at the cost of small bias.
parity_verified_expectation(counts, n_qubits, expected_parity)
Computes per-qubit <Z> from parity-verified counts only.
Returns (exp_vals, std_vals, rejection_rate).
parity_verified_R(z_counts, x_counts, y_counts, n_qubits, expected_parity)
Computes order parameter R with compound symmetry mitigation:
- Z-basis: parity post-selection (strict)
- X-basis: symmetry expansion (relaxed, basis rotation breaks direct parity)
- Y-basis: symmetry expansion
Returns dict with R_raw, R_verified, improvement, improvement_pct.
6. mitiq_integration — Production Wrappers
Reference: LaRose et al., Quantum 6, 774 (2022).
Wraps Mitiq's battle-tested ZNE and DDD implementations. Mitiq handles circuit folding, noise scaling, and extrapolation internally.
Requires: pip install mitiq
is_mitiq_available()
Returns True if Mitiq is installed. All other functions raise ImportError
if Mitiq is missing.
zne_mitigated_expectation(circuit, executor=None, scale_factors=None, shots=8192)
Run Mitiq ZNE with Richardson extrapolation:
from scpn_quantum_control.mitigation.mitiq_integration import zne_mitigated_expectation
value = zne_mitigated_expectation(circuit, scale_factors=[1, 3, 5])
Default executor: AerSimulator with parity expectation.
ddd_mitigated_expectation(circuit, executor=None, shots=8192)
Run Mitiq DDD (Digital Dynamical Decoupling) with XX rule:
from scpn_quantum_control.mitigation.mitiq_integration import ddd_mitigated_expectation
value = ddd_mitigated_expectation(circuit)
7. symmetry_decay — GUESS (symmetry-decay ZNE)
Reference: Oliva del Moral et al., arXiv:2603.13060 (2026). See
symmetry_decay_guess.md for the full theory
and tutorial.
Physics-informed zero-noise extrapolation that uses a Hamiltonian symmetry observable with a known ideal value as the noise guide. For the SCPN Kuramoto-XY framework, the conserved total magnetisation is the natural choice — it commutes with and is measured for free in any Z-basis run.
Rust-accelerated via scpn_quantum_engine.fit_symmetry_decay and
scpn_quantum_engine.guess_extrapolate_batch (rayon-parallel batch
correction).
learn_symmetry_decay(ideal_symmetry_value, noisy_symmetry_values, noise_scales)
Fit the noise-decay exponent from log-linear regression on the symmetry observable measurements at multiple noise scale factors:
Returns a SymmetryDecayModel with the learned alpha and
fit_residual.
from scpn_quantum_control.mitigation.symmetry_decay import (
learn_symmetry_decay,
xy_magnetisation_ideal,
)
n = 4
s_ideal = xy_magnetisation_ideal(n, "ground") # = +n
model = learn_symmetry_decay(
ideal_symmetry_value=s_ideal,
noisy_symmetry_values=[3.92, 3.65, 3.10], # measured ⟨ΣZ⟩ at g=1,3,5
noise_scales=[1, 3, 5],
)
print(model.alpha, model.fit_residual)
guess_extrapolate(target_noisy_value, symmetry_noisy_value, decay_model)
Apply the GUESS correction (Eq. 5 of Oliva del Moral et al.) to a single target observable:
from scpn_quantum_control.mitigation.symmetry_decay import guess_extrapolate
result = guess_extrapolate(
target_noisy_value=0.45,
symmetry_noisy_value=3.92,
decay_model=model,
)
print(result.mitigated_value, result.correction_factor)
xy_magnetisation_ideal(n_qubits, initial_state="ground")
Helper that returns the ideal for the
standard initial states used in the SCPN framework: "ground"
(returns ) or "neel" (returns ).
Phase 1 hardware result (April 2026)
GUESS was used in the Phase 1 ibm_kingston campaign as both the noise calibration and the headline scientific result. With learned from the parity-leakage decay across depths 2–30, the corrected sector-resolved leakage rates produced the promoted raw-count hardware observation of the DLA parity asymmetry: 7 of 8 depths individually significant at Welch , Fisher's combined , mean asymmetry for depths .
Comparison of Techniques
| Technique | Circuit Overhead | Shot Overhead | Best For |
|---|---|---|---|
| ZNE | 3-5x depth amplification | None | Shallow circuits, simple noise |
| PEC | None | gamma^n_gates (exponential) | Short circuits, high precision |
| CPDR | None | n_training runs | Deep circuits, coherent errors |
| DD | Extra gates on idle qubits | None | Idle-time decoherence |
| Symmetry | None | Shot loss from rejection | XY Hamiltonian circuits |
| Mitiq ZNE | Same as ZNE | None | Production systems |
Rust Acceleration
| Function | Module | Speedup | Parity |
|---|---|---|---|
pec_coefficients | pec | exact | verified to machine precision |
The PEC quasi-probability decomposition has a Rust path in
scpn_quantum_engine.pec_coefficients(). Other mitigation modules
operate on circuits and counts, where Rust acceleration is not applicable.
Dependencies
| Module | External | Required? |
|---|---|---|
zne | qiskit | Yes |
pec | qiskit | Yes |
cpdr | qiskit | Yes |
dd | qiskit | Yes |
symmetry_verification | numpy | Yes |
mitiq_integration | mitiq, qiskit-aer | Optional |
Testing
42 tests across 7 test files:
test_zne.py— Folding correctness, extrapolation accuracy, edge casestest_pec.py— Coefficient validation, sampling convergence, overhead scalingtest_cpdr.py— Training generation, regression fit, pipeline roundtriptest_dd.py— Sequence insertion, qubit validation, gate countstest_symmetry_verification.py— Parity detection, post-selection, expansiontest_mitiq_integration.py— Mitiq wrapper correctness (skipif not installed)test_pipeline_wiring_performance.py— End-to-end mitigation pipeline benchmarks
Pipeline Performance
Measured on ML350 Gen8 (128 GB RAM, Xeon E5-2620v2):
| Operation | System | Wall Time |
|---|---|---|
gate_fold_circuit (scale=5) | 4 qubits, depth 20 | 0.2 ms |
zne_extrapolate (3 points, linear) | — | 0.01 ms |
pauli_twirl_decompose (p=0.01) | 1 qubit | 0.01 ms |
pec_sample (1000 samples) | 2 qubits | 450 ms |
generate_training_circuits (20) | 4 qubits | 3 ms |
cpdr_mitigate | — | 0.05 ms |
insert_dd_sequence (XY4) | 4 idle qubits | 0.1 ms |
parity_postselect (10k shots) | 4 qubits | 0.5 ms |
symmetry_expand (10k shots) | 4 qubits | 0.6 ms |