Skip to the content.

Error Mitigation

Reduce the effect of hardware noise on quantum results using zero-noise extrapolation, probabilistic error cancellation and readout error mitigation.

Overview

Quantum computers are inherently noisy - gate errors, decoherence, and measurement errors corrupt results. Error mitigation techniques reduce these errors without requiring error-corrected quantum hardware, which makes them useful on today’s NISQ (Noisy Intermediate-Scale Quantum) devices.

Available Techniques:

The percentages are typical ranges reported in the literature, not guarantees: the actual improvement depends on the circuit, the device and how well the noise matches each technique’s assumptions.

All three techniques work on CircuitBuilder.Circuit values and take an executor function that you supply. The executor runs a circuit on whatever backend you choose (cloud hardware, or the local noisy simulator used in the examples below) and returns either an expectation value (ZNE, PEC) or a measurement histogram (REM). This keeps the mitigation code independent of the backend.

Key Concepts

Error Sources in Quantum Computing

1. Gate Errors

2. Readout Errors

3. Noise Accumulation

Error Mitigation vs Error Correction

Error Mitigation (Available Today):

Error Correction (Future):


A Noisy Executor for the Examples

The examples on this page use NoisyLocalBackend, a density-matrix simulator that applies a depolarizing channel after every gate, so the mitigation has real noise to work against. Primitives.observe runs a circuit on a backend and returns the expectation value of a Pauli observable; here the observable is Z⊗Z.

open System.Numerics
open System.Threading
open System.Threading.Tasks
open FSharp.Azure.Quantum
open FSharp.Azure.Quantum.CircuitBuilder
open FSharp.Azure.Quantum.Algorithms
open FSharp.Azure.Quantum.Core.BackendAbstraction
open FSharp.Azure.Quantum.Backends.DensityMatrixSimulator

// 1% depolarizing error per single-qubit gate, 2% per two-qubit gate
let backend = NoisyLocalBackend(depolarizing 0.01 0.02) :> IQuantumBackend

// Observable Z⊗Z on two qubits
let zz : TrotterSuzuki.PauliHamiltonian =
    { NumQubits = 2
      Terms = [ { Operators = [| 'Z'; 'Z' |]; Coefficient = Complex.One } ] }

// Executor for ZNE and PEC: circuit -> expectation value
let executor (c: Circuit) : Task<Result<float, string>> =
    task { return Primitives.observe backend c zz |> Result.mapError (fun e -> e.Message) }

// A small VQE-style ansatz to mitigate
let theta = 0.4

let vqeCircuit =
    circuit {
        qubits 2
        RY 0 theta
        CNOT 0 1
        RY 1 theta
    }

On real hardware, replace backend with a cloud backend (see Backend Switching) and keep the rest: Primitives.observe then estimates ⟨Z⊗Z⟩ from measured shots, one job per commuting group of terms, so every executor call is at least one billed job. The noisy simulator holds a full density matrix, so it is meant for small circuits (it refuses more than 8 qubits).


Zero-Noise Extrapolation (ZNE)

What is ZNE?

ZNE reduces errors by running the circuit at increasing noise levels, fitting a polynomial to the results, and extrapolating back to zero noise.

How It Works:

  1. Run the circuit at baseline noise (1.0×)
  2. Artificially increase noise (for example 1.5× and 2.0×) by making the circuit longer
  3. Measure the expectation value at each noise level
  4. Fit a polynomial curve to the measurements (least squares)
  5. Extrapolate the curve to zero noise (the constant term of the polynomial)

When to Use ZNE

Best For:

Not Suitable For:

API Reference

open FSharp.Azure.Quantum.ZeroNoiseExtrapolation

// Baseline plus two amplified noise levels, quadratic fit
let zneConfig = defaultIonQConfig |> withPolynomialDegree 2

task {
    match! ZeroNoiseExtrapolation.mitigateAsync vqeCircuit zneConfig executor CancellationToken.None with
    | Ok result ->
        printfn "Zero-noise value: %.4f" result.ZeroNoiseValue
        printfn "R² fit quality: %.4f" result.GoodnessOfFit

        for (noiseLevel, value) in result.MeasuredValues do
            printfn "  %.2fx noise -> %.4f" noiseLevel value

        // Check fit quality
        if result.GoodnessOfFit < 0.9 then
            printfn "Warning: poor fit quality - consider more noise levels"
    | Error msg -> eprintfn "ZNE failed: %s" msg
}

mitigateAsync runs the executor once per noise level, one after another, and returns Task<Result<ZNEResult, string>>. ZNEResult holds ZeroNoiseValue, MeasuredValues (noise level, value pairs), PolynomialCoefficients and GoodnessOfFit (R²). It does not know the ideal value, so it cannot report an error reduction; compare against a known reference yourself if you have one.

Configuration Options

ZNEConfig has three fields: NoiseScalings (one NoiseScaling per noise level), PolynomialDegree and MinSamples.

// Identity insertion: IdentityInsertion r inserts X·X pairs (an identity) to lengthen the circuit.
// The noise level recorded for the fit is 1 + r.
let identityConfig : ZNEConfig =
    { NoiseScalings = [ IdentityInsertion 0.0; IdentityInsertion 0.5; IdentityInsertion 1.0 ]
      PolynomialDegree = 2
      MinSamples = 1024 }

// Pulse stretching: PulseStretching s uses s itself as the noise level.
let stretchConfig =
    defaultRigettiConfig
    |> withNoiseScalings [ PulseStretching 1.0; PulseStretching 1.5; PulseStretching 2.0; PulseStretching 2.5 ]

Cost Analysis

Circuit Executions:

Example Cost (illustrative prices):

Choosing Polynomial Degree

The fit needs at least degree + 1 noise levels; with fewer, mitigateAsync returns an Error.

// Linear (degree 1): E(λ) = a + bλ. Fast, simple, less accurate
let linear = zneConfig |> withPolynomialDegree 1

// Quadratic (degree 2): E(λ) = a + bλ + cλ². Recommended default
let quadratic = zneConfig |> withPolynomialDegree 2

// Cubic (degree 3): needs at least 4 noise levels; more points make the fit more stable
let cubic =
    zneConfig
    |> withPolynomialDegree 3
    |> withNoiseScalings [ for s in [ 1.0; 1.5; 2.0; 2.5; 3.0 ] -> PulseStretching s ]

Working Example

See complete example: examples/ErrorMitigation/ZNE_Example.fsx


Probabilistic Error Cancellation (PEC)

What is PEC?

PEC reduces errors by inverting noise channels using quasi-probability decomposition. It actively cancels errors rather than just extrapolating.

How It Works:

  1. Describe the noise with a depolarizing noise model (single-qubit and two-qubit error rates)
  2. Decompose each noisy gate into a quasi-probability mixture of the gate followed by Pauli corrections (some weights are negative)
  3. Sample circuits from the decomposition (Monte Carlo)
  4. Combine the weighted results to cancel the modelled noise

When to Use PEC

Best For:

Not Suitable For:

API Reference

open FSharp.Azure.Quantum.ProbabilisticErrorCancellation

// Depolarizing noise model (here: matches the simulator above)
let noiseModel : ProbabilisticErrorCancellation.NoiseModel =
    { SingleQubitDepolarizing = 0.01 // 1% error per single-qubit gate
      TwoQubitDepolarizing = 0.02    // 2% error per two-qubit gate
      ReadoutError = 0.0 }           // not used by PEC; handle readout with REM

let pecConfig : PECConfig =
    { NoiseModel = noiseModel
      Samples = 1000   // Monte Carlo samples: more = lower variance, higher cost
      Seed = Some 42 } // reproducible sampling

task {
    match! ProbabilisticErrorCancellation.mitigateAsync vqeCircuit pecConfig executor CancellationToken.None with
    | Ok result ->
        printfn "Corrected value:   %.4f" result.CorrectedExpectation
        printfn "Uncorrected value: %.4f" result.UncorrectedExpectation
        printfn "Relative change:   %.1f%%" (result.ErrorReduction * 100.0)
        printfn "Samples used:      %d" result.SamplesUsed
        printfn "Overhead:          %.0fx" result.Overhead
    | Error msg -> eprintfn "PEC failed: %s" msg
}

mitigateAsync runs the sampled circuits one after another and returns Task<Result<PECResult, string>>. ErrorReduction is the relative difference between the corrected and uncorrected values, |corrected − uncorrected| / |uncorrected|; without the ideal value the library cannot measure the true error reduction. Overhead is the number of sampled circuits (Samples); the baseline adds one more execution.

Noise Model Characterization

The noise model comes from you. The library does not characterize a device; take the error rates from the provider’s published calibration data or from your own benchmarking runs.

// Illustrative values - check the provider's current calibration data
let trappedIonNoise : ProbabilisticErrorCancellation.NoiseModel =
    { SingleQubitDepolarizing = 0.0003 // 0.03% error
      TwoQubitDepolarizing = 0.005     // 0.5% error
      ReadoutError = 0.01 }

let superconductingNoise : ProbabilisticErrorCancellation.NoiseModel =
    { SingleQubitDepolarizing = 0.001  // 0.1% error
      TwoQubitDepolarizing = 0.01      // 1% error
      ReadoutError = 0.02 }

Cost Analysis

Circuit Executions:

Example Cost (illustrative prices):

Overhead Estimation

Overhead depends on:

Each gate’s decomposition has a normalization factor Σ pᵢ > 1, and the variance of the estimate grows with the product of these factors over all gates, so the number of samples needed for a given precision grows exponentially with circuit depth.

Working Example

See complete example: examples/ErrorMitigation/PEC_Example.fsx


Readout Error Mitigation (REM)

What is REM?

REM reduces measurement errors by calibrating a confusion matrix that maps prepared states to measured states, then applying the inverse transformation to measured histograms.

How It Works:

  1. Calibration (one-time): prepare every basis state ( 00⟩, 01⟩, 10⟩, 11⟩ for 2 qubits)
  2. Measure each state many times to build the confusion matrix M[measured, prepared]
  3. Invert the matrix (LU decomposition)
  4. Runtime: apply the inverse matrix to correct each measured histogram

When to Use REM

Best For:

Not Suitable For:

API Reference

REM executors take a circuit and a shot count and return a histogram. ReadoutErrorMitigation reads bitstrings with the highest qubit first (the rightmost character is qubit 0), while Primitives.sample writes qubit 0 first, so the executor below reverses each key. On a cloud backend Primitives.sample returns the job’s own counts and accepts only the shot count the backend was created with, so create it with the shots REM asks for (withCalibrationShots), or one backend per shot count.

open FSharp.Azure.Quantum.ReadoutErrorMitigation

// REM executor: circuit -> shots -> histogram (keys with the highest qubit first)
let sampleExecutor (c: Circuit) (shots: int) : Task<Result<Map<string, int>, string>> =
    task {
        return
            Primitives.sample backend c shots
            |> Result.map (fun histogram ->
                histogram
                |> Map.toList
                |> List.map (fun (bits, count) -> System.String(Array.rev (bits.ToCharArray())), count)
                |> Map.ofList)
            |> Result.mapError (fun e -> e.Message)
    }

// Configure REM (defaultConfig: 10,000 shots, 95% confidence, clip negatives, 1% filter)
let remConfig = defaultConfig |> withCalibrationShots 10000

task {
    // Step 1: Calibrate (one-time per backend session): 2^n calibration circuits
    match! measureCalibrationMatrixAsync "noisy-local" 2 remConfig sampleExecutor CancellationToken.None with
    | Error msg -> eprintfn "Calibration failed: %s" msg
    | Ok calibration ->
        // Matrix.[measured, prepared]; each column sums to 1
        printfn "P(measure 00 | prepared 00): %.4f" calibration.Matrix.[0, 0]
        printfn "P(measure 01 | prepared 00): %.4f" calibration.Matrix.[1, 0]

        // Step 2: Run your circuit
        let bell =
            circuit {
                qubits 2
                H 0
                CNOT 0 1
            }

        match! sampleExecutor bell 10000 with
        | Error msg -> eprintfn "Execution failed: %s" msg
        | Ok rawCounts ->
            // Step 3: Apply correction
            match correctReadoutErrors rawCounts calibration remConfig with
            | Ok corrected ->
                printfn "Raw counts: %A" rawCounts
                printfn "Corrected counts: %A" corrected.Histogram
                printfn "95%% intervals: %A" corrected.ConfidenceIntervals
                printfn "Normalization check: %.4f" corrected.GoodnessOfFit
            | Error msg -> eprintfn "Correction failed: %s" msg
}

correctReadoutErrors returns CorrectedResults: the corrected Histogram (non-integer counts), ConfidenceIntervals, the CalibrationUsed and a GoodnessOfFit normalization check. The calibration must match the histogram’s qubit count, otherwise the call returns an Error.

ReadoutErrorMitigation.mitigateAsync circuit backendName config executor ct runs calibration, execution and correction in one call and returns Task<Result<CorrectedResults, string>>. It recalibrates every time, so when you run several circuits, calibrate once with measureCalibrationMatrixAsync and reuse the matrix with correctReadoutErrors.

Multi-Qubit Calibration

For n qubits, calibration prepares all 2ⁿ basis states:

// 1 qubit: 2 states (|0⟩, |1⟩)
let cal1 = measureCalibrationMatrixAsync "noisy-local" 1 remConfig sampleExecutor CancellationToken.None

// 3 qubits: 8 states (|000⟩, |001⟩, ..., |111⟩)
let cal3 = measureCalibrationMatrixAsync "noisy-local" 3 remConfig sampleExecutor CancellationToken.None

Calibration Cost:

Handling Negative Counts

Problem: Matrix inversion can produce negative counts (unphysical)

Options (REMConfig.ClipNegative):

// Clip to zero (default): negative values become 0, then the vector is renormalized
let clipping = remConfig |> withClipNegative true

// Keep the quasi-probabilities M^-1 × measured unchanged, negative entries included:
// the unbiased estimate that expectation values need
let unclipped = remConfig |> withClipNegative false

Entries below MinProbability (default 1% of shots) are dropped from the histogram, negative ones included; lower it with withMinProbability if you need small probabilities. Only clipping renormalises. For an unbiased quasi-distribution, as expectation values need, set both withClipNegative false and withMinProbability 0.0: then nothing is dropped or renormalised.

Cost Analysis

Circuit Executions:

Example Cost (illustrative prices):

Working Example

See complete example: examples/ErrorMitigation/REM_Example.fsx


Combined Strategies

Why Combine Techniques?

Error mitigation techniques target different error sources:

Combining a gate-error technique with REM addresses both kinds of error. The library has no single “apply everything” function; you combine the techniques by composing executors.

1. REM + ZNE (Best Value)

Cost: Low-Medium (one execution per ZNE noise level) Use For: Most applications on real hardware

The ZNE executor samples the circuit, corrects the histogram with REM, and computes the expectation value from the corrected counts:

/// ⟨Z⊗Z...⟩ from a histogram: +1 for even parity, -1 for odd parity
let parityExpectation (histogram: Map<string, float>) =
    let total = histogram |> Map.fold (fun acc _ count -> acc + count) 0.0

    histogram
    |> Map.fold
        (fun acc bits count ->
            let ones = bits |> Seq.filter ((=) '1') |> Seq.length
            let sign = if ones % 2 = 0 then 1.0 else -1.0
            acc + sign * count / total)
        0.0

/// Executor for ZNE/PEC that applies REM before computing the expectation value
let remCorrectedExecutor (calibration: CalibrationMatrix) (c: Circuit) : Task<Result<float, string>> =
    task {
        let! counts = sampleExecutor c 4000

        return
            counts
            |> Result.bind (fun measured -> correctReadoutErrors measured calibration remConfig)
            |> Result.map (fun corrected -> parityExpectation corrected.Histogram)
    }

// Step 1: Calibrate REM (one-time), then Step 2: run ZNE with the corrected executor
task {
    match! measureCalibrationMatrixAsync "noisy-local" 2 remConfig sampleExecutor CancellationToken.None with
    | Error msg -> eprintfn "Error: %s" msg
    | Ok calibration ->
        match! ZeroNoiseExtrapolation.mitigateAsync vqeCircuit zneConfig (remCorrectedExecutor calibration) CancellationToken.None with
        | Ok result -> printfn "Mitigated value: %.4f" result.ZeroNoiseValue
        | Error msg -> eprintfn "Error: %s" msg
}

Benefits:

2. REM + PEC (Maximum Accuracy)

Cost: High (Samples + 1 executions) Use For: Critical high-accuracy applications

The same corrected executor plugs into PEC:

task {
    match! measureCalibrationMatrixAsync "noisy-local" 2 remConfig sampleExecutor CancellationToken.None with
    | Error msg -> eprintfn "Error: %s" msg
    | Ok calibration ->
        match! ProbabilisticErrorCancellation.mitigateAsync vqeCircuit pecConfig (remCorrectedExecutor calibration) CancellationToken.None with
        | Ok result -> printfn "Mitigated value: %.4f" result.CorrectedExpectation
        | Error msg -> eprintfn "Error: %s" msg
}

Benefits:

Automatic Strategy Selection

ErrorMitigationStrategy.selectStrategy recommends a technique from the circuit size, the target backend, a budget and an accuracy target. The recommendation carries default ZNE/PEC configurations, a fallback, a cost estimate and a human-readable reason.

open FSharp.Azure.Quantum.Core

let criteria : ErrorMitigationStrategy.SelectionCriteria =
    { CircuitDepth = 25
      QubitCount = 2
      Backend = { Id = "ionq.simulator"; Provider = "IonQ"; Name = "IonQ Simulator"; Status = "Available" }
      MaxCostUSD = Some 50.0
      RequiredAccuracy = None
      Calibration = None } // or Some calibration from measureCalibrationMatrixAsync

let recommended = ErrorMitigationStrategy.selectStrategy criteria
printfn "%s (estimated cost %.0fx)" recommended.Reasoning recommended.EstimatedCostMultiplier

What selectStrategy picks:

Situation Primary technique Fallback
Budget below $1 REM none
Fewer than 10 gates REM none
Required accuracy above 0.9 and budget above $100 PEC + ZNE + REM ZNE + REM
10-49 gates and budget above $10 ZNE + REM REM
50 or more gates ZNE + REM REM
Budget below $10 REM none
Otherwise ZNE + REM REM

ErrorMitigationStrategy.applyStrategy histogram recommended applies a recommendation to a finished histogram. Only the readout (REM) part can be applied after the fact, and only when the criteria carried a calibration matrix; otherwise the counts pass through unchanged and the result has CorrectionApplied = false. ZNE and PEC re-execute the circuit, so run them with their own mitigateAsync functions as shown above.

Strategy Selection Guide

Application Recommended Strategy Cost
Prototyping REM only Low
Production REM + ZNE Low-Medium
High-value REM + PEC High

Performance Comparison

Error Reduction Effectiveness

Typical ranges from the literature; your results will vary.

Technique Gate Errors Readout Errors Extra executions Recommendation
ZNE 30-50% 0% one per noise level Good value
PEC 50-80% 0% Samples + 1 Critical use only
REM 0% 50-90% 2ⁿ calibration circuits, once Almost always
REM+ZNE 30-50% 50-90% one per noise level Best default
REM+PEC 50-80% 50-90% Samples + 1 Maximum accuracy

Circuit Depth Limits

Technique Shallow (≤10 gates) Medium (10-30 gates) Deep (>30 gates)
ZNE ✅ Excellent ✅ Good ⚠️ Moderate
PEC ✅ Excellent ⚠️ Expensive ❌ Impractical
REM ✅ Excellent ✅ Excellent ✅ Excellent

Troubleshooting

Common Issues

1. Poor ZNE Fit Quality (R² < 0.9)

Symptoms: Low goodness-of-fit score

Solutions:

2. PEC Overhead Too High

Symptoms: Too many samples needed for a stable estimate

Solutions:

3. REM Produces Negative Counts

Symptoms: Unphysical negative counts after correction

Solutions:

4. Combined Strategies Don’t Improve Accuracy

Symptoms: Mitigation makes results worse

Solutions:

Working Examples

See complete, runnable examples in examples/ErrorMitigation/:

See Also

References


Last Updated: September 2026