"""Stage 17 - the third domain: the step size of an adaptive noise canceller. Root finding: the method lands second behind Chandrupatla 1997. ODE step control: level with a five-constant PI controller, behind the three-constant classical one. Softening somebody else's acceptance test: fails, three times over. Active noise control is the domain where the object we have - a step size that is a safety factor on an exact step - is exactly the object the field argues about, and where the standard answers are openly constant-ridden. The acoustic paths, the noise regimes and the scenarios come from our earlier ANC benchmark (SEM repository, branch feature/sem-anc-benchmark), so the environment is one that existed before this question was asked. Metric: noise reduction in dB on the second half of each run, so that the adaptation transient is not counted. Higher is better. Ten seeds per cell. """ from __future__ import annotations import math import numpy as np from anc.bench import (FS, SCENARIOS, make_primary_path, make_secondary_path, run_nlms) from anc.rules import ALL N = 8000 # one second SEEDS = 10 SNRS = (30.0, 15.0) def main(): print(f"{len(SCENARIOS)} scenarios x {len(SNRS)} noise levels x {SEEDS} " f"seeds, {N / FS:g} s each\n") grand = {r.name: [] for r in ALL} for snr in SNRS: print("=" * 88) print(f"reference SNR {snr:g} dB noise reduction, dB (higher is " f"better)") print("=" * 88) print(f"{'scenario':22s}" + "".join(f"{r.name[:13]:>13s}" for r in ALL)) print("-" * 88) for sname, gen in SCENARIOS: row = {r.name: [] for r in ALL} for s in range(SEEDS): rng = np.random.default_rng(1000 + s) noise = gen(N, rng) prim = make_primary_path(rng) sec = make_secondary_path(rng) for r in ALL: nr = run_nlms(noise, prim, sec, r, snr_db=snr, seed=s) if math.isfinite(nr): row[r.name].append(nr) grand[r.name].append(nr) cells = [] for r in ALL: v = row[r.name] cells.append(f"{(sum(v) / len(v)) if v else float('nan'):>13.2f}") print(f"{sname:22s}" + "".join(cells), flush=True) print() print("=" * 88) print("OVERALL") print("=" * 88) print(f"{'rule':26s}{'constants':>11s}{'mean NR dB':>13s}" f"{'worst cell':>13s}{'diverged':>11s}") print("-" * 88) for r in ALL: v = grand[r.name] n_bad = SEEDS * len(SCENARIOS) * len(SNRS) - len(v) print(f"{r.name:26s}{r.constants:>11d}" f"{(sum(v) / len(v)) if v else float('nan'):>13.2f}" f"{min(v) if v else float('nan'):>13.2f}{n_bad:>11d}") print("\n'diverged' counts runs whose residual power was not finite - the " "failure mode\na step size that is too large actually produces.") if __name__ == "__main__": main()