mock: the square's edges ring, and the one probe that wants a step asks for it

The square was the mock's only ideal step, which is the one shape a real producer
does not put on the wire. It is now the truncated Fourier series of the step --
`--square-harmonics N` odd harmonics, default 30 -- so each edge overshoots by the
Gibbs constant (about 9% of the 2 V swing, y = +/-1.179) and then rings, and
`--square-harmonics 0` is the ideal +/-1 step as before.

Odd harmonics only, so the series keeps the ideal wave's edge instants and its
polarity: the level-0 trigger, the glitch's inverted pulse and the buffer search
land where they did. Re-run in pairs (`--square-harmonics 0` against the default),
search, condition legs 0/1, trigrenum, disarm, trig trace, measures and events all
read exactly what they read before, with only `t0` moving. The reading that moved
is the noise probe's group-delay leg, whose `want` is the analytic `tau*ln2` of a
step: a band-limited edge reads `delay=0.002770 ratio=1.255`, the same figure on
every run. That recipe now asks for the ideal step, so its documented number stays
exact; probe-readings says why.

The series is vectorised: as a per-sample Python sum it cost ~15 sin() calls a
sample, which at the fast mock's 1.3 MS/s made the producer the bottleneck and
halved the rate the app saw (the noise probe's span-dependent counts read 22
against 48). Vectorised it adds ~0.5 ms to a 2000-sample packet, so the fast mock
runs at about nine tenths of its declared pace and those counts read within a few
percent.
This commit is contained in:
Martino Ferrari committed 2026-10-15 00:45:05 +02:00
1 parent 41cd8e5f38
commit afd2a6cdc1
4 files changed
+85 -7

No files matched your search

+13
View File
@@ -106,6 +106,19 @@ belongs with `todo.md` item 3, alongside the relative-time toggle.
`tools/mock_udpstreamer.py` has waveform-injection options used by the probes:
- `--square-harmonics N` shapes `square`: `N` odd harmonics of the ideal step's Fourier
series, so each edge overshoots by the Gibbs constant (about 9 % of the 2 V swing at the
default 30, i.e. `y = ±1.179`) and then rings — a band-limited edge, which is what a
crossing count or a pulse-width test has to survive. The series is deterministic and the
odd harmonics keep the ideal wave's edge instants and its polarity, so the level-0 trigger,
the search and the glitch's inverted pulse still read where they did on the step; the
*extremes* are what move. `0` (or negative) is the ideal ±1 step, which is what a probe
asserting an exact extreme or an exact width asks for — the noise probe's group-delay leg is
the one that does, because its `want` is the analytic `tau*ln2` of a step
(see [verification.md](verification.md)). It costs the producer about 0.5 ms per
2000-sample packet on top of the ideal branch, so at the fast mock's 1.3 MS/s the mock
delivers at roughly nine tenths of its declared pace — a reading that scales with the
retained span reads that much lower, and nothing else does.
- `--glitch-at S` / `--glitch-width W` invert the `square` signal for `W` seconds at the
start of every square period from stream second `S` onwards — a runt pulse of a known
width, which is what a `<` pulse-width trigger and a glitch search are checked against.
+14 -1
View File
@@ -1027,6 +1027,12 @@ must **not** also be set (two arms on the same port race). Run with
`--rate 2000 --batch 64 --freq 5 --glitch-at 0.25 --glitch-width 0.004` and
`UDPSCOPE_EXIT_AFTER_MS=12000`.
**That recipe needs no `--square-harmonics`**: the mock's `square` rings by default (see
[build-and-run.md](build-and-run.md)), and the odd-harmonic series passes through zero at the ideal edge's
own crossings, so the glitch's inverted 0.004 s pulse, the hit count, `agree` and the pulse-width leg's
`measured=0.200000` all read what they read on the step. Only a *level* the ripple reaches and the step does
not — the extremes, `y = ±1.179` — would need `--square-harmonics 0`, and no probe here asserts one.
Measured output (port 8419):
```
@@ -1854,7 +1860,14 @@ nothing itself but *is* rebuilt under when a graph is installed at its instant -
- **`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.
means `a` is far too small, i.e. `dt` never arrived at the detector. **This is the leg the mock's
`--square-harmonics 0` is for**: `want` is a *step* response, and the square rings by default, which moves
the crossing to `delay=0.002770 ratio=1.255` — inside the band, but no longer the figure beside it, and the
same on every run, so it is the shape and not the run. See the recipe in
[verification.md](verification.md). The band/hf legs need no such flag: they count the crossings of `sine`,
which the ripple does not touch at all — their counts read a few percent lower at this recipe's 1.3 MS/s
only because the extra harmonics cost the *producer*, which
[verification.md](verification.md) notes.
- `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.
+12 -1
View File
@@ -631,13 +631,24 @@ same run).
```bash
cd /home/martino/Projects/tools/udpscope
python3 tools/mock_udpstreamer.py --port 8108 --rate 1300000 --batch 1000 --duration 20 \
--spike-every 0.005 --spike-amp 0.5 --spike-width 2 > /tmp/noise-mock.log 2>&1 &
--spike-every 0.005 --spike-amp 0.5 --spike-width 2 --square-harmonics 0 \
> /tmp/noise-mock.log 2>&1 &
sleep 1.5
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=18000 UDPSCOPE_ENABLE_ALL_MS=1000 \
UDPSCOPE_PROBE_NOISE_MS=4000 \
./build/udpscope --server 127.0.0.1 --port 8108 2>&1 | grep -E 'noise-probe|CRITICAL'
```
`--square-harmonics 0` is the one flag in that command that is not about the sine, and it is what keeps the
probe's last reading exact: the group-delay leg measures a one-pole's *step* response on `square`, and the
mock's square rings by default (see [build-and-run.md](build-and-run.md)). A band-limited edge moves that
crossing off `tau*ln2` — measured `delay=0.002770 ratio=1.255` against the ideal step's `0.002206 / 1.000`,
and the same figure on every run, so it is a property of the shape and not of the run. The leg's assertion is
the `0.7..1.4` band, which the ripple keeps, but the ideal step is what keeps the *number* the line prints
beside `want` as well. The other legs measure event counts on the sine, which the ripple does not touch; the
counts do read a few percent lower under the ripple at this rate, for the producer-cost reason
[build-and-run.md](build-and-run.md) gives.
`UDPSCOPE_PROBE_NOISE_MS` is **not the time its readings are taken**. It times the probe's first stage, which
installs a math graph through `apply_math_channels()` -- and that is a *structural rebuild* that replaces the
buffer and drops its history. Every reading the probe takes is over retained history, so the rebuild has to