measure: top/bottom/amplitude read the flat levels, not the extremes

A bench scope separates Vtop/Vbase from Vmax/Vmin because a band-limited
edge overshoots and rings: the mock's `square` at 30 harmonics reaches
±1.179 where its plateau is ±1. On the rippled square the old readings were
a property of the edges rather than of the waveform -- `top` 0.986496,
`amplitude` 1.179350 -- and `amplitude` reported the overshoot.

`top`, `bottom` and `amplitude` are now the flat levels and their delta.
A flat level is a density question -- the plateau holds most of the
samples and the ringing a few percent -- so `measure_scan_buckets()` folds
a 64-bin histogram of the whole buckets' midpoints while it has the list in
hand (one observation per bucket, each bin carrying its buckets' exact sum
and finite count), and `measure_flat_levels()` takes, either side of the
split, the densest bin holding at least half the densest one's buckets --
the outermost of those, which is what makes a waveform with no plateau read
its peak -- and the exact mean of the samples of the buckets within 5 % of
the window's peak-to-peak of it. 5 % is deliberately under the ~9 %-of-swing
Gibbs overshoot. `amplitude` is `top - bottom` (a scope's Vamplitude), not
the half-swing it was.

Measured, fast mock, 2 s window, harmonics 0 -> 30: top 1.000000 ->
0.999983, bottom -1.000000 -> -0.999983, amplitude 2.000000 -> 1.999966,
while `peak to peak` keeps the extremes (2.000000 -> 2.358701). On the 1 Hz
sine top is 0.957988 (it was 0.636620, the half-wave mean; a sine has no
plateau, so the densest band is at its peak) with sd 0.0014 over a scrolling
window, and `peak to peak` is still exactly 2.

No raw pass and no new fetch: the histogram is the same fetched bucket list
read a second time, gated on a pane that holds one of the three types.
`raw` is unchanged (0.1% clean, 2.1% with the --noise 0.1 interferer,
against 0.1%/2.1% before) for 0.44 -> 0.50 ms and 0.52 -> 0.65 ms a pass.
A window the pyramid cannot summarize -- an hf-filtered walk, or one shorter
than a bucket -- keeps the old readings: the mean of each side of the split,
and the window's peak to peak for amplitude.

Docs: the flat-level invariant (and the two 2/pi assertions it replaces),
the probe readings, the manual's measurements page and the README.
This commit is contained in:
Martino Ferrari committed 2026-10-15 01:19:33 +02:00
1 parent afd2a6cdc1
commit e83ad86b8f
10 files changed
+495 -80

No files matched your search

+17 -3
View File
@@ -84,8 +84,18 @@ precedent, not an exception.
collecting anything -- that is how the first baseline of the stall below read 0.4 ms).
The values are in the exit dump as `measurements: <type>=<value>`; on the fast mock
(`tools/mock_udpstreamer.py --rate 1300000 --batch 2000`) a unit sine must read
`peak to peak=2`, `amplitude=1`, `average=0` and **`top=0.636620`** -- 2/pi, the half-wave
mean of a sine, which is the check that the threshold types are not merely plausible.
`peak to peak=2` **exactly**, `average=0`, and the two flat-level types as their definition
says: **`top=0.957988`** and `amplitude=1.915983`. `top` is no longer 2/pi -- it is the *flat*
level of the upper side, read from the density of the window's buckets, and a sine has no
plateau so the densest band is at its peak; `amplitude` is `top - bottom` (a scope's
`Vamplitude`), not the half-swing it once was. The check that the flat levels are *levels* and
not extremes is the same probe against the mock's `square` (`--square-harmonics 30`, the
control being `0`): `top`/`bottom` `0.999983`/`-0.999983` rippled against `1.000000`/`-1.000000`
ideal, `amplitude` `1.999966` against `2.000000`, while `peak to peak` reads `2.358701` rippled
against `2.000000` ideal -- the ±1.179 edges'
overshoot, kept by the extremes and excluded by the levels. The per-type values, the reason for
each, and the cost of the histogram are in
[invariants-measurements.md](invariants-measurements.md).
`[measure-trace] view=[..] <type>=<value>... marks=N level:V/time:V...` is a repeating line
(250 ms) from the probe: the pane's numbers beside the *current* window and the marks actually
placed, which is the only way a stale result or a mark that should not be drawn shows up as a
@@ -106,7 +116,11 @@ precedent, not an exception.
(`tools/mock_udpstreamer.py --rate 1300000 --batch 2000`), where a window is ~2 M samples.
`--noise 0.1` there reads **2.0%** at 1.96 ms a recompute against the clean 0.1% at 1.41 ms, and
that is the interferer's own cost rather than a regression -- the figure to compare a change
against is the **clean** one.
against is the **clean** one. `raw` is also the guard on the flat-level types, which are the one
thing that reads a second time: `top`/`bottom`/`amplitude` fold a histogram out of the **same**
fetched bucket list, so they add buckets and not samples (measured: `raw=0.1%` and `2.1%` with
the interferer, against `0.1%`/`2.1%` before those types had a flat level, for 0.44→0.50 ms and
0.52→0.65 ms a pass -- the buckets are the budget).
Note the **final** `measurements:` line and `[measure-trace]`'s last lines may read `--`:
`UDPSCOPE_EXIT_AFTER_MS` arms the trigger before it dumps, so the dump can land in `Waiting`
with nothing bound and the axis at its default. The race-free reading is an earlier