Skip to content

Dashboard Core — Mitigation (Composer's ZNE panel)

Real Zero-Noise Extrapolation (ZNE) error mitigation, wired to the same engine and real noise channels (dense_evolution.registry.NoiseModel) the rest of Composer uses — a thin dashboard-facing layer over dense_evolution.mitigation, not a separate reimplementation.

from dashboard_core.mitigation import run_zne_mitigation, run_density_matrix_zne

qasm = """
OPENQASM 2.0;
include "qelib1.inc";
qreg q[1];
creg c[1];
x q[0];
measure q -> c;
"""

result = run_zne_mitigation(qasm, pauli_string="Z", noise_model="bitflip", noise_p=0.05, seed=0)
print(result.ideal_expectation)     # -1.0
print(result.noisy_expectations)    # [-0.880, -0.790, -0.810] -- decays with noise scale
print(result.zne_extrapolated)      # -1.080 -- closer to -1.0 than any single noisy measurement

dm_result = run_density_matrix_zne(qasm, noise_model="bitflip", noise_p=0.05, seed=0)
print(dm_result.fidelity_raw, dm_result.fidelity_corrected)  # 0.940 -> 1.000

mitigation

Real Zero-Noise Extrapolation (ZNE) error mitigation, wired to the same engine and real noise channels (dense_evolution.NoiseModel) the rest of the Composer uses. Noise is scaled by running the real channel at noise_p, 2noise_p, 3noise_p and Richardson-extrapolating the measured Pauli expectation back to zero noise via dense_evolution's own zero_noise_extrapolation -- not a fabricated mitigated curve.

run_zne_mitigation

run_zne_mitigation(
    qasm_text: str,
    pauli_string: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=None,
    n_trials: int = 200,
    extrapolation_method: str = "richardson",
) -> MitigationResult

Real ZNE:

is measured at the real ideal state and at the real channel applied at noise_p * each factor, then extrapolated to zero noise -- either Richardson (dense_evolution.zero_noise_extrapolation, the exact interpolating polynomial through 3 points, the default) or a degree-2 least-squares polynomial fit through 5 points (dense_evolution.polynomial_extrapolate) -- caller's choice, not silently picked: noise_factors defaults to the 3- or 5-point set that matches whichever method was requested, unless the caller overrides it explicitly.

NoiseModel.apply_to_sv is a stochastic single-shot Kraus draw (one random outcome per call, not the channel's averaged/ensemble behavior) -- feeding a single draw straight into ZNE gives a discontinuous, meaningless curve (verified: bitflip/depolarizing jumped 1.0 -> 0.0 -> 0.0 across noise scales instead of decaying smoothly).

under a Kraus channel is Tr(rho P) = mean over the channel's trajectories, so each scale here averages n_trials independent stochastic draws -- the actual expectation value ZNE is defined against, not one random sample of it.

pauli_string uses dense_evolution's own qubit-0-is-position-0 convention (same as pauli_expectation), independent of Qiskit's little-endian display convention used elsewhere on this page -- this function never touches a Qiskit-ordered array.

Examples:

>>> from dashboard_core.mitigation import run_zne_mitigation
>>> qasm = '''
... OPENQASM 2.0;
... include "qelib1.inc";
... qreg q[1];
... creg c[1];
... x q[0];
... measure q -> c;
... '''
>>> result = run_zne_mitigation(qasm, pauli_string='Z', noise_model='bitflip',
...                              noise_p=0.05, seed=0, n_trials=200)
>>> result.ideal_expectation
-1.0
>>> abs(result.zne_extrapolated - result.ideal_expectation) < abs(result.noisy_expectations[0] - result.ideal_expectation)
True
Source code in tools/dashboard/core/mitigation.py
def run_zne_mitigation(
    qasm_text: str,
    pauli_string: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=None,
    n_trials: int = 200,
    extrapolation_method: str = "richardson",
) -> MitigationResult:
    """Real ZNE: <P> is measured at the real ideal state and at the real
    channel applied at noise_p * each factor, then extrapolated to zero
    noise -- either Richardson (dense_evolution.zero_noise_extrapolation,
    the exact interpolating polynomial through 3 points, the default) or
    a degree-2 least-squares polynomial fit through 5 points
    (dense_evolution.polynomial_extrapolate) -- caller's choice, not
    silently picked: noise_factors defaults to the 3- or 5-point set that
    matches whichever method was requested, unless the caller overrides
    it explicitly.

    NoiseModel.apply_to_sv is a *stochastic single-shot* Kraus draw (one
    random outcome per call, not the channel's averaged/ensemble
    behavior) -- feeding a single draw straight into ZNE gives a
    discontinuous, meaningless curve (verified: bitflip/depolarizing
    jumped 1.0 -> 0.0 -> 0.0 across noise scales instead of decaying
    smoothly). <P> under a Kraus channel is Tr(rho P) = mean over the
    channel's trajectories, so each scale here averages n_trials
    independent stochastic draws -- the actual expectation value ZNE is
    defined against, not one random sample of it.

    pauli_string uses dense_evolution's own qubit-0-is-position-0
    convention (same as pauli_expectation), independent of Qiskit's
    little-endian display convention used elsewhere on this page --
    this function never touches a Qiskit-ordered array.

    Examples
    --------
    >>> from dashboard_core.mitigation import run_zne_mitigation
    >>> qasm = '''
    ... OPENQASM 2.0;
    ... include "qelib1.inc";
    ... qreg q[1];
    ... creg c[1];
    ... x q[0];
    ... measure q -> c;
    ... '''
    >>> result = run_zne_mitigation(qasm, pauli_string='Z', noise_model='bitflip',
    ...                              noise_p=0.05, seed=0, n_trials=200)
    >>> result.ideal_expectation
    -1.0
    >>> abs(result.zne_extrapolated - result.ideal_expectation) < abs(result.noisy_expectations[0] - result.ideal_expectation)
    True
    """
    if extrapolation_method not in ("richardson", "polynomial"):
        raise ValueError(
            f"unknown extrapolation_method {extrapolation_method!r}, must be 'richardson' or 'polynomial'"
        )
    if noise_factors is None:
        noise_factors = (
            _POLYNOMIAL_NOISE_FACTORS if extrapolation_method == "polynomial" else _DEFAULT_NOISE_FACTORS
        )

    # Parsed with dense_evolution's own QASMParser, never Qiskit's --
    # QuantumCircuit.from_qasm_str itself segfaults on macOS (see
    # dashboard_core/engine.py's module docstring for the full story);
    # this function never needs a Qiskit circuit object at all, only the
    # ideal statevector, so there's no reason to build one here.
    parsed = de.QASMParser().parse(qasm_text)
    n_qubits = parsed.n_qubits
    if len(pauli_string) != n_qubits:
        raise ValueError(f"pauli_string length {len(pauli_string)} != n_qubits {n_qubits}")
    if noise_model not in de.NoiseModel.MODELS:
        raise ValueError(f"unknown noise model {noise_model!r}, must be one of {de.NoiseModel.MODELS}")

    # Same real anti-OOM guard as dashboard_core.engine.run_circuit_from_qasm
    # -- this kernel now runs on whatever machine a Composer visitor has,
    # not just a dev laptop, and this function was the one real gap that
    # never checked before allocating (engine.py's own functions always did).
    required_mb = (2 ** n_qubits) * 16 / 1e6
    de.chunk.SafeMemoryGuard().check_allocation(required_mb, context=f"{n_qubits}-qubit statevector")

    sim = de.DenseSVSimulator(n_qubits, use_float32=False)
    sim.run_circuit(parsed.to_tuples())
    sv_ideal = np.asarray(sim.sv)
    ideal_expectation = float(np.real(de.pauli_expectation(sv_ideal, pauli_string)))

    rng = np.random.default_rng(seed)
    noisy_expectations = []
    noisy_sems = []
    for factor in noise_factors:
        scaled_p = min(noise_p * factor, 1.0)
        trial_values = np.empty(n_trials)
        for i in range(n_trials):
            sv_noisy = de.NoiseModel.apply_to_sv(
                sv_ideal.copy(), n_qubits, noise_model, scaled_p, rng=rng,
            )
            trial_values[i] = np.real(de.pauli_expectation(sv_noisy, pauli_string))
        noisy_expectations.append(float(trial_values.mean()))
        # Standard error of the mean (sample std / sqrt(n_trials)), not
        # the per-trial spread itself -- this is the real Monte Carlo
        # uncertainty on the plotted mean, from the n_trials draws
        # already computed above, not a separate/approximate estimate.
        noisy_sems.append(float(trial_values.std(ddof=1) / np.sqrt(n_trials)))

    if extrapolation_method == "polynomial":
        zne_value = float(de.polynomial_extrapolate(noisy_expectations, list(noise_factors), degree=_POLYNOMIAL_DEGREE))
    else:
        zne_value = float(de.zero_noise_extrapolation(noisy_expectations, list(noise_factors)))

    return MitigationResult(
        n_qubits=n_qubits,
        pauli_string=pauli_string,
        ideal_expectation=ideal_expectation,
        noise_factors=list(noise_factors),
        noisy_expectations=noisy_expectations,
        zne_extrapolated=zne_value,
        extrapolation_method=extrapolation_method,
        noisy_sems=noisy_sems,
    )

run_density_matrix_zne

run_density_matrix_zne(
    qasm_text: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=_DEFAULT_NOISE_FACTORS,
    n_trials: int = 200,
) -> DensityMatrixZNEResult

Density-matrix ZNE (dense_evolution.zne_density_matrix): builds a real Monte-Carlo density-matrix estimate at each noise scale (the mean of n_trials |psi_k><psi_k| projectors from independent NoiseModel draws -- a real ensemble reconstruction, not a single trajectory), extrapolates to zero noise, and projects onto the nearest physical (PSD, trace-1) density matrix internally.

Graded (never fed back into the extrapolation) against the true ideal density matrix via dense_evolution.uhlmann_fidelity, so the reported improvement is an honest measurement of whether the correction actually helped -- matches the pattern in zne_density_matrix's own docstring (experiments/matrix_healing_zne.py: raw ~0.865, corrected ~0.947 on a 2-qubit Bell state).

noise_factors defaults to the same 3-point set as run_zne_mitigation's own Richardson default (prog.txt, dashboard_core audit point 5c: this used to have no stated reason here, unlike that function's explicit one) -- zne_density_matrix always fits via polynomial_extrapolate (degree=2), which is mathematically IDENTICAL to exact Richardson interpolation at exactly 3 points (its own "original design point") but, unlike Richardson, stays well-behaved with MORE than 3 -- so a caller can safely pass extra noise_factors here for more averaging, without the interpolation degradation that would apply to run_zne_mitigation's Richardson path at the same move (see zne_density_matrix's own docstring for the measured numbers).

Examples:

>>> from dashboard_core.mitigation import run_density_matrix_zne
>>> qasm = '''
... OPENQASM 2.0;
... include "qelib1.inc";
... qreg q[1];
... creg c[1];
... x q[0];
... measure q -> c;
... '''
>>> result = run_density_matrix_zne(qasm, noise_model='bitflip', noise_p=0.05, seed=0, n_trials=200)
>>> result.fidelity_corrected > result.fidelity_raw
True
Source code in tools/dashboard/core/mitigation.py
def run_density_matrix_zne(
    qasm_text: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=_DEFAULT_NOISE_FACTORS,
    n_trials: int = 200,
) -> DensityMatrixZNEResult:
    """Density-matrix ZNE (dense_evolution.zne_density_matrix): builds a
    real Monte-Carlo density-matrix estimate at each noise scale (the
    mean of n_trials |psi_k><psi_k| projectors from independent
    NoiseModel draws -- a real ensemble reconstruction, not a single
    trajectory), extrapolates to zero noise, and projects onto the
    nearest physical (PSD, trace-1) density matrix internally.

    Graded (never fed back into the extrapolation) against the true
    ideal density matrix via dense_evolution.uhlmann_fidelity, so the
    reported improvement is an honest measurement of whether the
    correction actually helped -- matches the pattern in
    zne_density_matrix's own docstring (experiments/matrix_healing_zne.py:
    raw ~0.865, corrected ~0.947 on a 2-qubit Bell state).

    noise_factors defaults to the same 3-point set as run_zne_mitigation's
    own Richardson default (prog.txt, dashboard_core audit point 5c: this
    used to have no stated reason here, unlike that function's explicit
    one) -- zne_density_matrix always fits via polynomial_extrapolate
    (degree=2), which is mathematically IDENTICAL to exact Richardson
    interpolation at exactly 3 points (its own "original design point")
    but, unlike Richardson, stays well-behaved with MORE than 3 -- so a
    caller can safely pass extra noise_factors here for more averaging,
    without the interpolation degradation that would apply to
    run_zne_mitigation's Richardson path at the same move (see
    zne_density_matrix's own docstring for the measured numbers).

    Examples
    --------
    >>> from dashboard_core.mitigation import run_density_matrix_zne
    >>> qasm = '''
    ... OPENQASM 2.0;
    ... include "qelib1.inc";
    ... qreg q[1];
    ... creg c[1];
    ... x q[0];
    ... measure q -> c;
    ... '''
    >>> result = run_density_matrix_zne(qasm, noise_model='bitflip', noise_p=0.05, seed=0, n_trials=200)
    >>> result.fidelity_corrected > result.fidelity_raw
    True
    """
    # Same QASMParser-only parsing as run_zne_mitigation above -- see its
    # comment for why a Qiskit circuit object is never built here.
    parsed = de.QASMParser().parse(qasm_text)
    n_qubits = parsed.n_qubits
    if noise_model not in de.NoiseModel.MODELS:
        raise ValueError(f"unknown noise model {noise_model!r}, must be one of {de.NoiseModel.MODELS}")

    # Density matrices are dim x dim (dim = 2**n_qubits), not just dim --
    # this holds rho_ideal, one rho per noise factor (rhos_at_scales, kept
    # alive simultaneously so zne_density_matrix can extrapolate across all
    # of them at once) and rho_corrected: (len(noise_factors) + 2) separate
    # dim*dim complex128 arrays, quadratically worse than a plain
    # statevector at the same qubit count. Same real anti-OOM guard as
    # dashboard_core.engine.run_circuit_from_qasm, sized for what this
    # function actually allocates -- this was the one real gap that never
    # checked before allocating.
    dim = 2 ** n_qubits
    required_mb = dim * dim * 16 / 1e6 * (len(noise_factors) + 2)
    de.chunk.SafeMemoryGuard().check_allocation(required_mb, context=f"{n_qubits}-qubit density matrix ZNE")

    sim = de.DenseSVSimulator(n_qubits, use_float32=False)
    sim.run_circuit(parsed.to_tuples())
    sv_ideal = np.asarray(sim.sv)
    rho_ideal = np.outer(sv_ideal, sv_ideal.conj())

    rng = np.random.default_rng(seed)
    rhos_at_scales = []
    for factor in noise_factors:
        scaled_p = min(noise_p * factor, 1.0)
        rho_acc = np.zeros((dim, dim), dtype=complex)
        for _ in range(n_trials):
            sv_noisy = de.NoiseModel.apply_to_sv(
                sv_ideal.copy(), n_qubits, noise_model, scaled_p, rng=rng,
            )
            rho_acc += np.outer(sv_noisy, sv_noisy.conj())
        rho_acc /= n_trials
        rhos_at_scales.append(rho_acc)

    rho_corrected = np.asarray(de.zne_density_matrix(rhos_at_scales, list(noise_factors)))

    fidelity_raw = float(de.uhlmann_fidelity(rho_ideal, rhos_at_scales[0]))
    fidelity_corrected = float(de.uhlmann_fidelity(rho_ideal, rho_corrected))

    return DensityMatrixZNEResult(
        n_qubits=n_qubits,
        noise_factors=list(noise_factors),
        fidelity_raw=fidelity_raw,
        fidelity_corrected=fidelity_corrected,
    )

run_coherence_zne_mitigation

run_coherence_zne_mitigation(
    qasm_text: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=_DEFAULT_NOISE_FACTORS,
    n_trials: int = 200,
) -> CoherenceZNEResult

Coherence-L1-predictive density-matrix ZNE (dense_evolution.coherence_predictive_zne_density_matrix): identical Monte-Carlo density-matrix construction to run_density_matrix_zne above, but extrapolated via the coherence-L1 signal (sum of off-diagonal density-matrix magnitudes, Baumgratz/Cramer/Plenio 2014) instead of the classical-JSD one -- validated (dense_evolution's own docstring, 200-seed phaseflip sweep) to catch phase-type noise (phaseflip, dephasing) the JSD-predictive signal is structurally blind to, since JSD only ever reads the density matrix's diagonal.

Same real anti-OOM guard, same Uhlmann-fidelity grading against the true ideal density matrix as run_density_matrix_zne -- the two functions differ in exactly one line (which dense_evolution extrapolation function is called), by design, so a caller comparing them is comparing the mitigation signal, not two different pipelines.

Source code in tools/dashboard/core/mitigation.py
def run_coherence_zne_mitigation(
    qasm_text: str,
    noise_model: str,
    noise_p: float,
    seed: Optional[int] = None,
    noise_factors=_DEFAULT_NOISE_FACTORS,
    n_trials: int = 200,
) -> CoherenceZNEResult:
    """Coherence-L1-predictive density-matrix ZNE
    (dense_evolution.coherence_predictive_zne_density_matrix): identical
    Monte-Carlo density-matrix construction to run_density_matrix_zne
    above, but extrapolated via the coherence-L1 signal (sum of
    off-diagonal density-matrix magnitudes, Baumgratz/Cramer/Plenio 2014)
    instead of the classical-JSD one -- validated (dense_evolution's own
    docstring, 200-seed phaseflip sweep) to catch phase-type noise
    (phaseflip, dephasing) the JSD-predictive signal is structurally blind
    to, since JSD only ever reads the density matrix's diagonal.

    Same real anti-OOM guard, same Uhlmann-fidelity grading against the
    true ideal density matrix as run_density_matrix_zne -- the two
    functions differ in exactly one line (which dense_evolution
    extrapolation function is called), by design, so a caller comparing
    them is comparing the mitigation signal, not two different pipelines.
    """
    parsed = de.QASMParser().parse(qasm_text)
    n_qubits = parsed.n_qubits
    if noise_model not in de.NoiseModel.MODELS:
        raise ValueError(f"unknown noise model {noise_model!r}, must be one of {de.NoiseModel.MODELS}")

    dim = 2 ** n_qubits
    required_mb = dim * dim * 16 / 1e6 * (len(noise_factors) + 2)
    de.chunk.SafeMemoryGuard().check_allocation(required_mb, context=f"{n_qubits}-qubit coherence ZNE")

    sim = de.DenseSVSimulator(n_qubits, use_float32=False)
    sim.run_circuit(parsed.to_tuples())
    sv_ideal = np.asarray(sim.sv)
    rho_ideal = np.outer(sv_ideal, sv_ideal.conj())

    rng = np.random.default_rng(seed)
    rhos_at_scales = []
    for factor in noise_factors:
        scaled_p = min(noise_p * factor, 1.0)
        rho_acc = np.zeros((dim, dim), dtype=complex)
        for _ in range(n_trials):
            sv_noisy = de.NoiseModel.apply_to_sv(
                sv_ideal.copy(), n_qubits, noise_model, scaled_p, rng=rng,
            )
            rho_acc += np.outer(sv_noisy, sv_noisy.conj())
        rho_acc /= n_trials
        rhos_at_scales.append(rho_acc)

    rho_corrected = np.asarray(de.coherence_predictive_zne_density_matrix(rhos_at_scales, list(noise_factors)))

    fidelity_raw = float(de.uhlmann_fidelity(rho_ideal, rhos_at_scales[0]))
    fidelity_corrected = float(de.uhlmann_fidelity(rho_ideal, rho_corrected))

    return CoherenceZNEResult(
        n_qubits=n_qubits,
        noise_factors=list(noise_factors),
        fidelity_raw=fidelity_raw,
        fidelity_corrected=fidelity_corrected,
    )

Not to be confused with dense_evolution.mitigation (same name, different module) — that one is the actual ZNE implementation (richardson_extrapolate, zne_density_matrix, jsd_predictive_zne_density_matrix, ...); this one is the dashboard's request/response wrapper around it.