measure: the split's own means are back as above/below/between

`top`, `bottom` and `amplitude` became the *flat levels* in e83ad86 -- the level the
signal dwells at, read from the density of the window's buckets -- and that change kept
the split's own two means as its internal fallback for a window the pyramid cannot
summarize. Those means are now three types of their own, so the two different questions
about one split can be asked side by side: `above %` and `below %` are the plain mean of
the samples on one side of a share -- the reading the old `top`/`bottom` gave -- and
`between % %` is the mean of the band between two shares, which no single split can
answer, because a band's two edges are two thresholds.

Nothing about the arithmetic is new, and nothing is a new pass. `Above`/`Below` are the
`sum[]`/`count[]` split `measure_accumulate()` already folds -- the very figures
`Top`/`Bottom` fall back to -- and `Between` is the **difference of two levels' upper
sides**, both of which come out of one pass two over one window. It rides the bucket
fast path unchanged: a bucket wholly inside the band is strictly above the lower level
and strictly below the upper one, so both accumulators fold it whole, and only the
buckets that straddle either edge are opened. `measure_level_pcts()` is the one place the
shares are read, and it clamps both and puts `between`'s pair **in order**, so "between
80 % and 20 %" is the band between 20 % and 80 % -- normalized rather than refused,
because the shares name two edges rather than a direction. Nothing to measure reads `--`,
as for every other type: a share at 100 % has no sample above it, and a band can hold
none at all.

Measured, `[bandavg-probe]`, fast mock at a 2 s window (`--rate 1300000 --batch 1000
--freq 1`):

  sine   `above 50` 0.636620 (2/pi), `below 50` -0.636620, `between 50 75` 0.255873
  square `above 50` 0.986496 (30 harmonics) / 1.000000 (--square-harmonics 0),
         `between 50 75` 0.302744 / --
  ramp   `between 50 75` 0.250000 and `above 50` 0.500000, `above 100` --

The first three are the figures the old `top`/`bottom` documented; the ramp's are what a
uniform density predicts (a band's mean is its own centre); the ideal square's empty band
is the nothing-to-measure rule. The pane offers the types and edits their percentages
through the dialog's second share -- which only `between` shows -- and a measurement's
pair survives add/edit/delete and the session file (`threshold2`), where a file written
before it existed loads at the default.

No raw pass: with the measurements probe adding one of every type at a 2 s window, `raw`
went from 113966 of 240719031 covered (0.0%) to 134978 of 255372033 (0.1%), 0.8 -> 1.1
ns a covered sample.

Docs: the band-average invariant (and the two assertions the flat-level change moved),
the probe readings for the new probe, the verification recipe, the manual's measurements
page and the README.
This commit is contained in:
Martino Ferrari committed 2026-10-15 07:56:37 +02:00
1 parent e83ad86b8f
commit 22e7a8efcc
14 files changed
+769 -98

No files matched your search

+37 -3
View File
@@ -120,13 +120,45 @@ precedent, not an exception.
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).
0.52→0.65 ms a pass -- the buckets are the budget). It is the guard on the band averages for the
same reason: `between` is a *second level in the same walk*, not a second pass, and measured on
the fast mock at a 2 s window with the measurements probe adding one of **every** type, `raw`
stayed at a fraction of a percent of `covered` —
`covered=240719031 buckets=3760828 raw=113966 (0.0%)` for the ten types before, against
`covered=255372033 buckets=3989978 raw=134978 (0.1%)` for the thirteen after, at 0.8 → 1.1
ns/covered sample (`data` 156.85 → 178.38 ms: the pane holds three more measurements and one more
level, and both walk the same fetched bucket list).
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
`[measure-trace]` line, which carries its own `view=`; those lines are also where the
level-mark fallback is visible (an invalid pass must still print its `level:` marks).
- `[bandavg-probe]` (stderr, `UDPSCOPE_PROBE_BANDAVG_MS`) — the **band averages**:
`above`/`below`/`between`, the plain means over one side of a share and over a band, which are
*different numbers* from `top`/`bottom`'s flat levels (see the band-average invariant in
[invariants-measurements.md](invariants-measurements.md)). It adds seven measurements, on the three
shapes whose band means can be computed by hand, and the four figures to read are:
* the 1 Hz unit sine at a **2 s window**: `above 50` = **`0.636620`** (2/pi) and `below 50` =
**`-0.636620`** — the exact figures the old `top`/`bottom` documented, and the reason this type
exists at all; `between 50 75` = `0.255873` (`3(2-sqrt 3)/pi`), an interior mean no side and no
whole-window statistic reports;
* the mock's band-limited `square` (30 harmonics, its default): `above 50` = **`0.986496`**, the
reading the commit that made `top` a flat level recorded as the *old* `top`;
* the mock's `ramp`, whose density is uniform: `between 50 75` = `0.250000` and `above 50` =
`0.500000` — a band's mean *is* that band's centre, so the shape predicts the number;
* and the two nothing-to-measure readings: `above 100` on the `ramp` and `between 50 75` on
`--square-harmonics 0` (whose samples are all exactly +/-1) must be `invalid`/`--`, never 0.
**Run it at a 2 s window** (the probe waits for a full one before it reads anything — the mock
delivers about 0.5 MS/s of the 1.3 MS/s it declares, so a freshly enabled ring needs seconds of
wall clock), and **not** with `UDPSCOPE_PROBE_MEASURE_MS` or `UDPSCOPE_PROBE_MEASURES_MS`: all
three add measurements to one pane. `UDPSCOPE_EXIT_AFTER_MS=17000` covers its sequence. The
command, both mock flavours and the lines are in [probe-readings.md](probe-readings.md):
`--square-harmonics 30` for the `0.986496` and the `0.302744`, `--square-harmonics 0` for the
`1.000000` and the empty band.
- `[pan-probe] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
(stderr, `UDPSCOPE_PROBE_PAN_MS`) — **`held` must be 1**: a gesture must not move a window
@@ -691,8 +723,10 @@ have run. `--spike-every`/`--spike-amp`/
The three lines and what each must read are in `probe-readings.md`.
`UDPSCOPE_PROBE_NOISE_MS`, `UDPSCOPE_PROBE_MEASURE_MS` and `UDPSCOPE_PROBE_MATH_MS` must **not** be scheduled
in the same run. The noise and measurement probes both push onto `app.measures`, and the later one would read
`UDPSCOPE_PROBE_NOISE_MS`, `UDPSCOPE_PROBE_MEASURE_MS`, `UDPSCOPE_PROBE_MEASURES_MS`,
`UDPSCOPE_PROBE_BANDAVG_MS` and `UDPSCOPE_PROBE_MATH_MS` must **not** be scheduled
in the same run. The noise, measurement, measurements and band-average probes all push onto
`app.measures`, and the later one would read
the earlier one's rows as its own; the noise and math probes both write `app.math`, so the later one's graph
lands on top of the earlier one's and both sets of readings describe a state neither probe set up. The noise probe carries
its own measurement legs (see `probe-readings.md`), so nothing is lost by keeping them apart.