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.

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.

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.
    """
    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 = []
    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()))

    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,
    )

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).

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).
    """
    # 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,
    )

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.