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

+51 -18
View File
@@ -38,16 +38,37 @@ enum class MeasureType {
PulseDuration, ///< how long the signal spends on one side of the threshold
Period, ///< the time between two successive crossings of the threshold
Frequency, ///< 1 / period
/// The mean of the samples **above** `threshold_pct` -- the split's own upper mean, which is
/// what `Top` reported before a flat level replaced it. `Top`/`Bottom` are the level the signal
/// *dwells at* (a density question, so a ringing edge does not drag them); these two are the
/// plain mean of the samples on one side of the share, ripple and overshoot included, which is
/// a *different* question about the same split and the reason the two coexist: on a unit sine
/// this reads `0.636620` (2/pi) where `Top` reads `0.957988`, and on the mock's band-limited
/// square `0.986496` where its plateau is 1.
Above,
/// The same from the split's lower side.
Below,
/// The mean of the samples **between** `threshold_pct` and `threshold_pct2`: the band's
/// interior, which neither of the two means above can answer (one of them gives the two sides
/// and the other the whole window, because the band's two edges are not one split).
Between,
};
/// Type-specific behaviour. Every field is meaningful only for the types named below; the
/// others ignore it (an option that does nothing for a type is left out of the dialog
/// rather than stored and ignored).
struct MeasureOptions {
/// `Top`/`Bottom`/`PulseDuration`/`Period`/`Frequency`: where the split is, as a share of
/// `Top`/`Bottom`/`PulseDuration`/`Above`/`Below`/`Between`: where the split is, as a share of
/// the window's own min..max. The middle by default, which is what a symmetric waveform
/// wants; a 3.3 V logic signal with ringing wants it nearer the top.
/// wants; a 3.3 V logic signal with ringing wants it nearer the top. For `Above`/`Below` it is
/// the level the mean is taken on one side of; for `Between` it is the band's *lower* edge.
double threshold_pct = 50.0;
/// `Between`: the band's other edge, the same unit. The two are read through
/// `measure_level_pcts()`, which clamps both and puts the pair in order, so the band between
/// 80 % and 20 % is the band between 20 % and 80 % -- the shares name two *edges* and the order
/// they were typed in carries no meaning, which is why a reversed pair is normalized rather
/// than refused.
double threshold_pct2 = 75.0;
/// `PulseDuration`: which side of the threshold is "the pulse". `Period`/`Frequency`
/// use it as the edge direction, so one cycle is counted once.
bool above = true;
@@ -268,11 +289,13 @@ struct MeasureLevel {
/// fact rather than a threshold decision. `measure_set_level()` is what sizes it; zero reduces
/// the walk exactly to a single level.
double hysteresis = 0.0;
/// The samples either side of `level`, exactly: `Top`/`Bottom`'s reading on a window with no
/// bucket histogram to read a flat level from (shorter than one bucket, or an HF-rejected
/// walk). They are folded on every window -- pass two walks it for the crossing types anyway
/// -- but a window that has a histogram is read from that instead, because a mean over a side
/// reports the ripple and the overshoot rather than the level the signal dwells at.
/// The samples either side of `level`, exactly: the reading of `Above`/`Below` is one of these
/// two means, `Between`'s is the difference of two *levels'* upper sides, and `Top`/`Bottom` fall
/// back to them on a window with no bucket histogram to read a flat level from (shorter than one
/// bucket, or an HF-rejected walk). They are folded on every window -- pass two walks it for the
/// crossing types anyway -- but a window that has a histogram is read from that instead, because
/// a mean over a side reports the ripple and the overshoot rather than the level the signal
/// dwells at.
/// Index 0 is the side below the level (a sample exactly *at* it counts here), index 1 the
/// side above it.
double sum[2] = {0.0, 0.0};
@@ -326,13 +349,18 @@ bool measure_accumulate_bucket(const MeasureBucket& bucket,
const std::function<double(std::size_t)>& time_of,
MeasureLevel& level);
/// The level a type reads, as a share of the window's own min..max, or false when it reads none
/// (the pure statistics, answered from `MeasureStats` alone). `Top`/`Bottom` read the
/// measurement's own threshold; the crossing types read the mid level, deliberately -- see
/// `measure_uses_threshold()`. `Amplitude` reads no level either: its value is the delta between
/// the two flat levels, which the histogram answers, and the split's two means are only a
/// fallback it takes from the window's own extremes -- see `compute_measure()`.
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct);
/// The levels a type reads, as shares of the window's own min..max, **lower first**, or 0 when it
/// reads none (the pure statistics, answered from `MeasureStats` alone). `pct` must hold two: a
/// level type fills `pct[0]` and answers 1, `Between` fills both and answers 2.
///
/// `Top`/`Bottom`/`Above`/`Below` read the measurement's own threshold; the crossing types read the
/// mid level, deliberately -- see `measure_uses_threshold()`. `Amplitude` reads no level either: its
/// value is the delta between the two flat levels, which the histogram answers, and the split's two
/// means are only a fallback it takes from the window's own extremes -- see `compute_measure()`.
///
/// This is the one place `Between`'s pair is clamped and ordered, so the pane's dedup key, the
/// engine's two accumulators and the dialog all describe the same band whatever was typed.
int measure_level_pcts(MeasureType type, const MeasureOptions& options, double pct[2]);
/// The window's two flat levels, from the bucket histogram in `stats.flat`. `top` is the densest
/// band of the bins *above* `lo + pct * (hi - lo)`, `bottom` the same below it -- for a
@@ -352,10 +380,11 @@ struct MeasureFlatLevels {
};
MeasureFlatLevels measure_flat_levels(const MeasureStats& stats, double pct);
/// The engine: one measurement's answer, O(1), from the two passes' results. `level` is the
/// accumulator for `measure_level_pct(type, options)` and may be null for the types that read
/// none. The function reads nothing else, which is what keeps the answer independent of the plot.
MeasureResult compute_measure(const MeasureStats& stats, const MeasureLevel* level,
/// The engine: one measurement's answer, O(1), from the two passes' results. `levels` is the
/// accumulators `measure_level_pcts(type, options)` asked for, in the same order -- `nullptr` for
/// the types that read none, and either entry may be null when the caller could not resolve it.
/// The function reads nothing else, which is what keeps the answer independent of the plot.
MeasureResult compute_measure(const MeasureStats& stats, const MeasureLevel* const levels[2],
MeasureType type, const MeasureOptions& options);
/// A measurement type's name, as the pane's row and the dialog's dropdown spell it.
@@ -368,6 +397,10 @@ MeasureType measure_type_at(int index);
/// spacing of the crossings of the *mid* level, one rising edge per cycle, and offering a
/// threshold there only invites a setting that makes the count wrong.
bool measure_uses_threshold(MeasureType type);
/// Whether a type reads a *pair* of shares, so the dialog shows its second share only then: the
/// two edges of a band are one measurement's two parameters, and one field of the two is a
/// measurement of a different band than the user asked for.
bool measure_uses_band(MeasureType type);
/// Whether a type uses the `above` side (only `PulseDuration`, where it says which half of
/// the waveform is the pulse).
bool measure_uses_side(MeasureType type);