92 lines
3.4 KiB
Python
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)
|