test: the noise probe reads the trigger's band and HF reject

Six legs over one retained buffer, each a full rising-edge search through `BufferSearch` at
level 0, run back to back without yielding so every leg sees the same samples: the bare
comparator on a spiked sine, a 0.8 band, a 50 Hz reject, both, and the reject's group delay
on `square` -- which carries no impulses, so a one-pole's crossing of zero after a step is
exactly `tau*ln2` and the delay is an exact prediction rather than a coincidence.

    trigger band:  bare=49/2 band=7/2 ratio=7.00
    trigger hf:    hf=7/2 band+hf=7/2 band=7
    trigger delay: clean=4 filtered=4 delay=0.002206 want=0.002206 ratio=1.000

`ratio >= 5`, both HF legs within 2 of the band, `ratio` between 0.7 and 1.4, `state=2` on
every leg, no Gtk-CRITICAL. `bare` and `band` are proportional to the retained buffer, so
neither is a fixed number; the ratios and the agreement between legs are the assertions.

The probe enables the signals it measures itself rather than relying on another probe's side
effects -- `enable_all_probe` ends by switching one off again, which is how `sine` was missing
from the first run of this one -- and arms in two phases because that enable is a structural
change that replaces the store, so the ring has to fill before there is anything to search.
Its "no stream" line now names the channels that *are* there: an index alone says a probe
found nothing, not what it saw.
This commit is contained in:
Martino Ferrari committed 2026-10-13 15:35:03 +02:00
1 parent 1583741e03
commit fec0a1032e
5 files changed
+184

No files matched your search

+33
View File
@@ -1276,3 +1276,36 @@ which must be untouched:
channels and an edit does not move it, so the store survives: the count *grows* (the stream keeps filling
while the edit runs) and never falls to 0, which is what a rebuild would do. Every channel then still reads
`N samples vs M time slots` with `N == M`, so no channel was left behind either.
## `[noise-probe]`
```
[noise-probe] trigger band: bare=49/2 band=7/2 ratio=7.00
[noise-probe] trigger hf: hf=7/2 band+hf=7/2 band=7
[noise-probe] trigger delay: clean=4 filtered=4 delay=0.002206 want=0.002206 ratio=1.000
```
`UDPSCOPE_PROBE_NOISE_MS`, and it needs the mock's **spiked** sine -- `--spike-every 0.005 --spike-amp 0.5
--spike-width 2` -- where every other probe uses the plain mock. It arms in two phases: the first enables the
signals it measures *itself*, because a probe must not depend on another probe's side effects
(`enable_all_probe` ends by switching one off again, which is how `sine` went missing from the first run of
this one), and waits 2.5 s for the ring to refill after that structural change. The legs then each search the
whole retained buffer through `BufferSearch`, one after another **without yielding**, so all six see the same
buffer.
- **`trigger band:`** -- `bare` is the bare comparator on the spiked sine (every impulse that lands near zero
carries it across, so the count is dominated by spikes), `band` the same with a 0.8 band. **`ratio` must be
at or above 5**, and `band` about the number of sine cycles retained. Neither count is a *fixed* number:
both scale with the retained buffer's length (7 at a 2.5 s buffer, ~30 at 6 s), so the ratio and the
agreement between the legs are the assertions. A `ratio` near 1 means the band is not being applied --
`decide()` is not reached from `feed_edge`.
- **`trigger hf:`** -- `hf` (no band, 50 Hz reject) and `band+hf` (both) must be **within 2 of `band`**: the
reject removes the impulses on its own. `hf` near `bare` means the pole never left `a == 1` (check
`configure()` recomputes it, and that `hf_fc` reached the condition); `band+hf` near 0 means the reject is
attenuating the sine itself, which would be the Nyquist clamp clamping 50 Hz.
- **`trigger delay:`** -- the reject's group delay, measured on **`square`**, which carries no impulses: a
one-pole turns a -1 -> +1 step into a zero crossing at exactly `tau*ln2`, so `delay` must read
`want = 0.002206 s` and **`ratio` between 0.7 and 1.4**. A `ratio` near 0 is a pass-through; a large one
means `a` is far too small, i.e. `dt` never arrived at the detector.
- `state` is `2` (Done) or `4` (Capped) on every leg. `3` (Truncated) on more than one leg means the pump loop
is being overtaken -- nothing to fix in the feature, but say so.
+24
View File
@@ -522,3 +522,27 @@ hold ports.
The probes are the regression net — when changing UI or plot-host behaviour, extend
the probe/dump in `src/ui.cpp` rather than adding a separate harness.
## The noise probe needs a spiked mock
Every other probe runs against the plain mock. The noise probe reads what a *band* and an *HF reject* do to
impulsive interference, so its mock must have some: an alternating-sign impulse train of 2 samples at
amplitude 0.5 every 5 ms, added to `sine` alone (so a spiked channel can be read against a clean one in the
same run).
```bash
cd /home/martino/Projects/tools/udpscope
python3 tools/mock_udpstreamer.py --port 8108 --rate 1300000 --batch 1000 --duration 16 \
--spike-every 0.005 --spike-amp 0.5 --spike-width 2 > /tmp/noise-mock.log 2>&1 &
sleep 1.5
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=14000 UDPSCOPE_ENABLE_ALL_MS=1000 \
UDPSCOPE_PROBE_NOISE_MS=6000 \
./build/udpscope --server 127.0.0.1 --port 8108 2>&1 | grep -E 'noise-probe|CRITICAL'
```
The probe arms at 6 s rather than the 8 s a single-phase probe would use: its first phase enables the signals
it measures and waits 2.5 s for the ring to refill after that structural change. `--spike-every`/`--spike-amp`/
`--spike-width` are the mock's own; `--batch` must stay at or below 1000 (2000 samples x 5 signals x f64 is
80 kB, past UDP's ~65507-byte payload, and the mock dies on its first `sendto` while the run still exits 0).
The three lines and what each must read are in `probe-readings.md`.