From 2c0e21fd20e6bf1cae485ee7ff7cabd4a6a6f659 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Tue, 13 Oct 2026 15:46:24 +0200 Subject: [PATCH] test: the noise probe reads a measurement's band and HF reject Five measurements on the same spiked `sine`, asking for corners 0, 50, 0, 0, 50 on one channel: two windows under the (channel, fc) key, which is what exercises it. measure pkpk bare noise=10.0 hf=0.0 value=2.987692 the stimulus, impulses included measure pkpk hf noise=10.0 hf=50.0 value=1.990075 the waveform: pass one is filtered measure period noband noise=0.0 hf=0.0 value=0.021290 supposed to be WRONG -- the control measure period band noise=25.0 hf=0.0 value=0.200000 the band alone measure period hf noise=0.0 hf=50.0 value=0.200000 the reject alone measure windows=2 the corner is part of the key `pkpk hf` is the reading that says the filter reached *pass one* and not only pass two; a value near 3 means it did not. `period noband` is deliberately the defect the band exists to fix, and a future reader who "fixes" it has removed the check. The band leg asks for **25 %** rather than the 10 % default, and that is arithmetic rather than a workaround: the band is a share of the *window's* peak-to-peak, which with the impulses in it is about 3, so 10 % is a half-band of 0.3 -- less than the 0.5 an impulse moves the signal by. `MeasureOptions::noise_pct`'s own rationale says 10 % holds to a 0.3 interferer, and this mock's is 0.5, so the default could not clear it. The default band is what the `period noband` and `period hf` legs are about. verification.md now says the noise and measurement probes must not share a run: both push onto `app.measures` and the later one would read the earlier one's rows. --- docs/claude/probe-readings.md | 31 ++++++++++++++++++++++ docs/claude/verification.md | 4 +++ src/ui_probe.cpp | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 1bb6f8f..1c5e9f4 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1313,3 +1313,34 @@ buffer. 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. + +The same probe then puts five measurements on the same spiked `sine` as **two windows** -- three legs at +`hf=0` and two at `hf=50` -- and reads them back: + +``` +[noise-probe] measure pkpk bare noise=10.0 hf=0.0 valid=1 value=2.987692 +[noise-probe] measure pkpk hf noise=10.0 hf=50.0 valid=1 value=1.990075 +[noise-probe] measure period noband noise=0.0 hf=0.0 valid=1 value=0.021290 +[noise-probe] measure period band noise=25.0 hf=0.0 valid=1 value=0.200000 +[noise-probe] measure period hf noise=0.0 hf=50.0 valid=1 value=0.200000 +[noise-probe] measure windows=2 +``` + +- **`pkpk bare`** -- about **3.0**: the sine's 2 plus an impulse either way. The impulses dominate the + window's extremes, so this is the *stimulus*. +- **`pkpk hf`** -- about **1.99** (`2/sqrt(1 + (5/50)^2)`): the *waveform*. This is the reading that says the + filter reached **pass one** and not only pass two; a value near 3 means it did not -- check `measure_scan` + is handed `w.scan_filter`, and that `filtered` disabled the buckets, since a bucket's aggregate is raw. +- **`period noband`** -- about **0.021**, far below the true 0.2. **This one is supposed to read wrong**: it is + the control the other two are compared against, the defect the band exists to fix. "Fixing" it removes the + check. +- **`period band`** -- exactly **0.200000**, the true period, from the band alone. Near `period noband` means + `noise_pct` is not reaching `measure_set_level`. The leg asks for **25 %** and not the 10 % default: the band + is a share of the *window's* peak-to-peak, which with the impulses in it is about 3, so 10 % is a half-band + of 0.3 -- less than the 0.5 an impulse moves the signal by, which is exactly what + `MeasureOptions::noise_pct`'s own rationale says (10 % holds to a 0.3 interferer). +- **`period hf`** -- exactly **0.200000** with the band off: the reject alone. Near `period noband` means the + pole coefficient stayed at 1 -- check `measure_filter_set` was given a non-zero `dt`. +- **`measure windows=2`** -- the five legs ask for corners `0, 50, 0, 0, 50` on one channel, which is two + distinct windows. **`windows=1` means the corner is not part of the key**: the five collapsed onto one walk + and two of them read a signal they did not ask for. diff --git a/docs/claude/verification.md b/docs/claude/verification.md index fe8f479..0c446d0 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -546,3 +546,7 @@ it measures and waits 2.5 s for the ring to refill after that structural change. 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`. + +`UDPSCOPE_PROBE_NOISE_MS` and `UDPSCOPE_PROBE_MEASURE_MS` must **not** be scheduled in the same run: both push +onto `app.measures`, and the later one would read the earlier one's rows as its own. The noise probe carries +its own measurement legs (see `probe-readings.md`), so nothing is lost by keeping them apart. diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index d98adcd..df28aa0 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -3560,6 +3560,55 @@ gboolean noise_probe(gpointer user) { "[noise-probe] trigger delay: clean=%zu filtered=%zu delay=%.6f want=%.6f ratio=%.3f\n", e_leg.hits, f_leg.hits, delay, want, want > 0.0 ? delay / want : 0.0); std::fflush(stderr); + + // Five measurements on the spiked sine, as *two* windows: three legs at `hf=0` and two at `hf=50`. The + // pane keys its walks by (channel, corner), so this is also what checks that a second corner does not + // reuse the first one's walk. + std::string name; + { + std::lock_guard lock(a->model->mutex()); + if (a->model->channels().empty()) return G_SOURCE_REMOVE; + name = a->model->channels().front().name; // `sine`; signal 0 is the timestamp + } + struct Leg { + const char* label; + MeasureType type; + double noise; + double hf; + }; + // The impulses dominate the window's extremes, so `peak to peak` reads the *stimulus* (about 3) + // unfiltered and the *waveform* (about 2) filtered -- the one reading that says the filter reached pass + // one and not only pass two. + const Leg legs[] = { + {"pkpk bare", MeasureType::PeakToPeak, 10.0, 0.0}, + {"pkpk hf", MeasureType::PeakToPeak, 10.0, 50.0}, + {"period noband", MeasureType::Period, 0.0, 0.0}, + // 25 % and not the 10 % default: the band is a share of the *window's* peak-to-peak, which with the + // impulses in it is about 3, so 10 % is a half-band of 0.3 -- less than the 0.5 an impulse moves the + // signal by, and the `MeasureOptions::noise_pct` comment says as much ("10 % holds to an interferer + // amplitude of 0.3"). The default is what `period noband` and `period hf` are about; this leg asks for + // a band that clears the stimulus, which is what makes the reading legible. + {"period band", MeasureType::Period, 25.0, 0.0}, + {"period hf", MeasureType::Period, 0.0, 50.0}, + }; + const std::size_t base = a->measures.size(); + for (const Leg& leg : legs) { + MeasureOptions options; + options.noise_pct = leg.noise; + options.hf_fc = leg.hf; + a->measures.push_back({name, leg.type, options}); + } + rebuild_measures_pane(*a); + update_measures(*a, /*force=*/true, /*accumulate=*/true); + for (std::size_t i = 0; i < std::size(legs); ++i) { + const std::size_t at = base + i; + // A *value*, not a reference: the conditional's other arm is a temporary. + const MeasureResult r = at < a->measure_results.size() ? a->measure_results[at] : MeasureResult{}; + std::fprintf(stderr, "[noise-probe] measure %-14s noise=%.1f hf=%.1f valid=%d value=%.6f\n", + legs[i].label, legs[i].noise, legs[i].hf, int(r.valid), r.primary); + } + std::fprintf(stderr, "[noise-probe] measure windows=%zu\n", a->measure_samples.size()); + std::fflush(stderr); a->search.clear(); update_search_panel(*a); return G_SOURCE_REMOVE;