Source code for experiments.harness.noise

"""Experiment 4: Noise sweep."""

import time
from typing import Optional

import numpy as np
from numpy.random import default_rng

from experiments.harness.phi import make_random_parity
from experiments.harness.results import ExperimentResult
from experiments.harness.worker import TrialSpec, run_trials_parallel


[docs] def run_noise_sweep_experiment( n_range: range = range(4, 7), noise_rates: Optional[list[float]] = None, num_trials: int = 20, epsilon: float = 0.3, qfs_shots: int = 2000, classical_samples_prover: int = 1000, classical_samples_verifier: int = 3000, base_seed: int = 42, max_workers: int = 1, shard_index: int | None = None, num_shards: int | None = None, ) -> ExperimentResult: r"""Noise sweep: verification under increasing label-flip noise. For each :math:`n` in *n_range* and each noise rate :math:`\eta`, the MoS state is constructed from the effective label probabilities (Definition 5(iii)): .. math:: \varphi_{\mathrm{eff}}(x) = (1 - 2\eta)\,\varphi(x) + \eta The effective Fourier coefficient becomes :math:`\hat{\tilde\phi}_{\mathrm{eff}}(s) = (1 - 2\eta)\, \hat{\tilde\phi}(s)`, and the distribution class promise is :math:`a^2 = b^2 = (1 - 2\eta)^2`. As :math:`\eta \to 0.5`, the signal :math:`(1 - 2\eta) \to 0` and the protocol should eventually fail. The experiment measures the empirical acceptance and correctness rates as functions of :math:`\eta`, testing the noise-robust verification results of §6.2. The Fourier resolution threshold :math:`\vartheta` is held **fixed** at :math:`\vartheta = \varepsilon` across the entire sweep (audit fix MAJOR-3 in ``audit/noise_sweep.md``). Previously :math:`\vartheta` was adapted per noise level (:math:`\vartheta = \min(\varepsilon,\, 0.9 \cdot (1 - 2\eta))`), which silently varied two parameters along the same axis and confounded the interpretation of any non-monotonicity in the acceptance curve. .. note:: **Audit fixes** (``audit/noise_sweep.md``): - **MAJOR-1:** the :math:`\eta` range now extends to :math:`\{0.42, 0.44, 0.46, 0.48\}` so the sweep crosses the theoretical breakdown :math:`\eta_{\max} \approx 0.4470` (set by :math:`(1 - 2\eta)^2 = \varepsilon^2/8`). Previously the sweep stopped at :math:`\eta = 0.40`, never entering the failure regime. - **MAJOR-2** (not fixed in this revision): the headline acceptance dip in :math:`\eta \in [0.05, 0.30]` is dominated by squared-estimator variance against the slack :math:`\varepsilon^2/8 = 0.01125`, not Lemma 6 attenuation. For a sharper acceptance figure ``classical_samples_verifier`` could be bumped to :math:`\sim 30000`; the cleanest empirical confirmation of Lemma 6 is already ``fourier_weight_attenuation.png``. - **MAJOR-3:** :math:`\vartheta` is now held fixed across the sweep (see above). - The hard-coded ``qfs_shots=2000``, ``classical_samples_prover=1000``, ``classical_samples_verifier=3000`` are still below the analytic Hoeffding budget; these are documented limitations tracked in ``audit/FOLLOW_UPS.md``. The on-disk ``results/noise_sweep_*.pb`` was generated under the old configuration and is invalid until re-run. Parameters ---------- n_range : range Range of :math:`n` values to sweep. noise_rates : list[float] or None Values of :math:`\eta` to sweep. Default: ``[0.0, 0.05, 0.1, ..., 0.4]``. num_trials : int Trials per :math:`(n, \eta)` cell. epsilon : float Accuracy parameter :math:`\varepsilon`. qfs_shots : int QFS copy budget per trial. classical_samples_prover : int Classical samples for the prover. classical_samples_verifier : int Classical samples for the verifier. base_seed : int Base random seed. max_workers : int Number of parallel worker processes. Returns ------- ExperimentResult """ if noise_rates is None: # Audit fix MAJOR-1 (audit/noise_sweep.md): extended past # eta_max = (1 - eps/(2*sqrt(2)))/2 ~= 0.4470 for eps=0.3 so the # sweep crosses the theoretical breakdown. noise_rates = [ 0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.42, 0.44, 0.46, 0.48, ] print( f"=== Noise Sweep: n in {list(n_range)}, eta in {noise_rates}, " f"{max_workers} workers ===" ) rng = default_rng(base_seed) specs: list[TrialSpec] = [] for n in n_range: for eta in noise_rates: effective_coeff = 1.0 - 2.0 * eta a_sq = effective_coeff**2 # Audit fix MAJOR-3 (audit/noise_sweep.md): hold theta fixed # at epsilon across the entire eta sweep. Previously theta # was adapted as ``min(epsilon, 0.9*(1-2*eta))``, which # silently varied a second parameter along the eta axis. theta = epsilon for _ in range(num_trials): seed = int(rng.integers(0, 2**31)) trial_rng = default_rng(seed) phi, target_s = make_random_parity(n, trial_rng) specs.append( TrialSpec( n=n, phi=phi, noise_rate=eta, target_s=target_s, epsilon=epsilon, delta=0.1, theta=theta, a_sq=a_sq, b_sq=a_sq, qfs_shots=qfs_shots, classical_samples_prover=classical_samples_prover, classical_samples_verifier=classical_samples_verifier, seed=seed, phi_description=f"noisy_parity_eta={eta}_s={target_s}", ) ) t0 = time.time() trials = run_trials_parallel( specs, max_workers=max_workers, label="noise", shard_index=shard_index, num_shards=num_shards, ) wall = time.time() - t0 result = ExperimentResult( experiment_name="noise_sweep", timestamp=time.strftime("%Y-%m-%dT%H:%M:%S"), wall_clock_s=wall, max_workers=max_workers, trials=trials, parameters={ "n_range": list(n_range), "noise_rates": noise_rates, "num_trials": num_trials, "epsilon": epsilon, }, ) # Print per-eta summary print(f"\n {'eta':>5s} {'eff_coeff':>9s} {'accept%':>8s} {'correct%':>9s}") for eta in noise_rates: et = [t for t in trials if f"eta={eta}" in t.phi_description] ar = np.mean([t.accepted for t in et]) if et else 0.0 cr = np.mean([t.hypothesis_correct for t in et]) if et else 0.0 print(f" {eta:5.2f} {1 - 2 * eta:9.2f} {ar:8.0%} {cr:9.0%}") print(f" Wall-clock time: {wall:.1f}s") return result