"""Active noise control: the step size of an NLMS canceller. The third domain, and the third instance of the same pattern. What is pinned here is the pattern and the mistake, not a victory. """ from __future__ import annotations import math import os import sys import numpy as np import pytest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from anc.bench import SCENARIOS, make_primary_path, make_secondary_path, run_nlms from anc.rules import Fixed, InSampleMeasured, KwongJohnston, Measured N, SEEDS = 8000, 4 SNRS = (30.0, 15.0) def sweep(rule, snrs=SNRS): """Both noise levels, as the experiment uses. Measuring at 30 dB alone is the wrong test and was: with almost no measurement noise there is nothing for a safety factor to hedge against, the exact step mu = 1 is near optimal, and any hedging costs a little. The whole point of the rule appears at 15 dB. A claim about the grid has to be tested on the grid.""" vals = [] for snr in snrs: for s in range(SEEDS): for _sname, gen in SCENARIOS: rng = np.random.default_rng(1000 + s) noise = gen(N, rng) nr = run_nlms(noise, make_primary_path(rng), make_secondary_path(rng), rule, snr_db=snr, seed=s) if math.isfinite(nr): vals.append(nr) return sum(vals) / len(vals), min(vals), len(vals) def test_the_in_sample_observable_is_uninformative(): """The mistake, pinned. NLMS with mu = 1 drives the a posteriori error of the current sample to exactly zero, so a rule that measures it reads 'the model was perfect' and proposes mu = 1 for ever. It is therefore the fixed mu = 1 rule under another name, and the numbers say so.""" a, _lo, _n = sweep(InSampleMeasured()) b, _lo2, _n2 = sweep(Fixed(1.0)) assert abs(a - b) < 0.1, (a, b) def test_measuring_out_of_sample_is_better_than_measuring_in_sample(): """The fix: what mu hedges against is the part of the error the filter cannot cancel, and that is observable as the floor the error keeps hitting - not as the residual of the sample just fitted.""" good, _lo, _n = sweep(Measured()) bad, _lo2, _n2 = sweep(InSampleMeasured()) assert good > bad, (good, bad) def test_the_measured_rule_beats_the_four_constant_scheme(): """Kwong-Johnston is the classical variable-step-size rule and carries four constants. This is the comparison the transfer is about.""" ours, _lo, _n = sweep(Measured()) kj, _lo2, _n2 = sweep(KwongJohnston()) assert ours > kj + 3.0, (ours, kj) def test_and_loses_to_one_well_chosen_constant(): """The other half, and the same in all three domains: measuring the constant beats the elaborate multi-constant schemes and does not beat a single constant chosen correctly for the benchmark.""" ours, _lo, _n = sweep(Measured()) best_fixed, _lo2, _n2 = sweep(Fixed(0.5)) assert best_fixed > ours, (best_fixed, ours) assert best_fixed - ours < 3.0, (best_fixed, ours) def test_no_rule_diverges(): """A step size that is too large makes the filter blow up; none of these may.""" for rule in (Fixed(0.1), Fixed(0.5), Fixed(1.0), KwongJohnston(), Measured()): _m, _lo, n = sweep(rule) assert n == SEEDS * len(SCENARIOS) * len(SNRS), (rule.name, n)