""" 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, 2*noise_p, 3*noise_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. """ from dataclasses import dataclass from typing import Optional import numpy as np import dense_evolution as de __all__ = ['MitigationResult', 'run_zne_mitigation', 'DensityMatrixZNEResult', 'run_density_matrix_zne'] _DEFAULT_NOISE_FACTORS = (1.0, 2.0, 3.0) # polynomial_extrapolate's own docstring: with exactly degree+1 points the # fit is the unique interpolating polynomial, IDENTICAL to # richardson_extrapolate -- the extra 2 points here (4x, 5x) are what # actually changes anything, trading a bit of interpolation bias for # averaging down statistical noise across more measured scales (verified # in that docstring against a real 5-seed sweep, not asserted here). _POLYNOMIAL_NOISE_FACTORS = (1.0, 2.0, 3.0, 4.0, 5.0) _POLYNOMIAL_DEGREE = 2 @dataclass class MitigationResult: n_qubits: int pauli_string: str ideal_expectation: float noise_factors: list noisy_expectations: list zne_extrapolated: float extrapolation_method: str = "richardson" 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:

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. """ 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, ) @dataclass class DensityMatrixZNEResult: n_qubits: int noise_factors: list fidelity_raw: float # Uhlmann fidelity: ideal vs raw noisy (base scale) rho fidelity_corrected: float # Uhlmann fidelity: ideal vs ZNE-corrected rho 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>