Files
sim-numerics/tests/test_anc.py
T

92 lines
3.4 KiB
Python

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