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:
1 parent
e83ad86b8f
commit
22e7a8efcc
14 files changed
+769
-98
No files matched your search
@@ -57,7 +57,7 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e
|
|||||||
| [invariants-rendering.md](docs/claude/invariants-rendering.md) | How the renderer maps a visible range onto samples (never the `dx()` grid), what `gather()` emits and relative to which origin, why every series needs a real `MinMaxPyramid`, and why a dense trace is drawn one pixel column at a time. |
|
| [invariants-rendering.md](docs/claude/invariants-rendering.md) | How the renderer maps a visible range onto samples (never the `dx()` grid), what `gather()` emits and relative to which origin, why every series needs a real `MinMaxPyramid`, and why a dense trace is drawn one pixel column at a time. |
|
||||||
| [invariants-trigger-and-pinning.md](docs/claude/invariants-trigger-and-pinning.md) | The event window counted in stream seconds end to end, what pins an axis in each mode, and what each pin is measured *from* (the snapshot's own stamps, not the request). |
|
| [invariants-trigger-and-pinning.md](docs/claude/invariants-trigger-and-pinning.md) | The event window counted in stream seconds end to end, what pins an axis in each mode, and what each pin is measured *from* (the snapshot's own stamps, not the request). |
|
||||||
| [invariants-time-axis-and-window.md](docs/claude/invariants-time-axis-and-window.md) | **No plot interaction changes the time scale**; `apply_time_window()` is the only path that does; how the x labels pick their form; what a fit restores. |
|
| [invariants-time-axis-and-window.md](docs/claude/invariants-time-axis-and-window.md) | **No plot interaction changes the time scale**; `apply_time_window()` is the only path that does; how the x labels pick their form; what a fit restores. |
|
||||||
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted with hysteresis. |
|
| [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which. |
|
||||||
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
|
| [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. |
|
||||||
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
|
| [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. |
|
||||||
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened. |
|
| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the six bundled icons and their authoring rules, and how the Mallard manual is opened. |
|
||||||
@@ -236,6 +236,8 @@ measurement.
|
|||||||
| `max` / `min` | the extremes | — | a ruler at the value **and** a marker where it happened |
|
| `max` / `min` | the extremes | — | a ruler at the value **and** a marker where it happened |
|
||||||
| `peak to peak` | the swing, `max - min` — the extremes, ripple and overshoot included | — | a ruler at each extreme |
|
| `peak to peak` | the swing, `max - min` — the extremes, ripple and overshoot included | — | a ruler at each extreme |
|
||||||
| `amplitude` | the two flat levels' delta, `top - bottom` | — | a ruler at each flat level |
|
| `amplitude` | the two flat levels' delta, `top - bottom` | — | a ruler at each flat level |
|
||||||
|
| `above` / `below` | the mean of the samples **above** / **below** a share — a mean *over a side*, not a level (see below) | split % | a ruler at the mean |
|
||||||
|
| `between` | the mean of the samples **between** two shares | from %, to % | a ruler at the mean |
|
||||||
| `pulse duration` | the mean width of the pulse-side runs | split %, which side is the pulse | markers at the pulse's two edges |
|
| `pulse duration` | the mean width of the pulse-side runs | split %, which side is the pulse | markers at the pulse's two edges |
|
||||||
| `period` / `frequency` | the spacing of the threshold crossings / its reciprocal, in Hz | — | markers at two edges of a cycle |
|
| `period` / `frequency` | the spacing of the threshold crossings / its reciprocal, in Hz | — | markers at two edges of a cycle |
|
||||||
|
|
||||||
@@ -263,6 +265,39 @@ for it at all. Where the pyramid cannot summarize the window — an `hf`-filtere
|
|||||||
shorter than a bucket — there is no density estimate, and the types fall back to the old
|
shorter than a bucket — there is no density estimate, and the types fall back to the old
|
||||||
behaviour: the mean of each side of the split, and `max - min` for `amplitude`.
|
behaviour: the mean of each side of the split, and `max - min` for `amplitude`.
|
||||||
|
|
||||||
|
**`above`, `below` and `between` are those side means, exposed.** The flat-level change kept the
|
||||||
|
split's own two means as its *fallback* for a window the pyramid cannot summarize; these three
|
||||||
|
types are that fallback, named, so the two different questions about one split can be asked side by
|
||||||
|
side. On a unit sine `above 50` reads `0.636620` — 2/π, exactly what `top` reported before it became
|
||||||
|
a level — where `top` reads `0.957988`; on the mock's band-limited square `above 50` reads
|
||||||
|
`0.986496`, where the plateau is 1 and `top` is `0.999983`, because the mean *over a side* carries
|
||||||
|
the edges' ring and the level the signal dwells at does not. `between` is new arithmetic in one
|
||||||
|
sense only: it is the samples above the lower edge **and not** above the upper one, so it is a
|
||||||
|
*difference of two levels* — no wider split can answer a band, because a band's two edges are two
|
||||||
|
thresholds — and it rides the same bucket walk (a bucket wholly inside the band folds into both
|
||||||
|
accumulators whole), which is why `measure cost:`'s `raw` is unchanged by it. The readings, on the
|
||||||
|
fast mock at a 2 s window, `[bandavg-probe]`: sine `above 50` `0.636620`, `below 50` `-0.636620`,
|
||||||
|
`between 50 75` `0.255873` (3(2−√3)/π — neither the window's mean nor the band's edge); rippled
|
||||||
|
square `above 50` `0.986496` and `between 50 75` `0.302744` (the two edges' transitions);
|
||||||
|
`ramp` `between 50 75` `0.250000` and `above 50` `0.500000`, because a uniform density makes every
|
||||||
|
band's mean that band's **own centre**; ideal square (`--square-harmonics 0`) `above 50` `1.000000`
|
||||||
|
and `between 50 75` `--`, because every sample is exactly ±1 and the band holds none.
|
||||||
|
|
||||||
|
Three things to know before reading one of those numbers:
|
||||||
|
|
||||||
|
- **The pair is ordered on read, not refused.** "between 80 % and 20 %" is the band between 20 % and
|
||||||
|
80 %: the two shares name two *edges* and the band does not depend on which was typed first, so a
|
||||||
|
reversed pair is normalized rather than turned into a silent `--`.
|
||||||
|
- **Nothing to measure reads `--`**, like every other type: a share at `100 %` has no sample above
|
||||||
|
it (the level is the window's own maximum) and a band whose interior holds none — the ideal
|
||||||
|
square above — is `invalid`/`--`, never `0`.
|
||||||
|
- **The sixth decimal follows the window's boundary.** The live window is closed at both ends, so
|
||||||
|
its oldest and newest sample share a phase, and one extra sample out of ~2.6 M moves the mean by
|
||||||
|
about one unit in the sixth decimal: over one run `above 50` on the sine printed `0.636620` on
|
||||||
|
most `[measure-trace]` lines and `0.636621` on a few, `above 50` on the square `0.986496` on most
|
||||||
|
and `0.986495` on the rest. The old `top`/`bottom` spread exactly the same way — it is a property
|
||||||
|
of a mean over a finite window, not of these types.
|
||||||
|
|
||||||
`period` and `frequency` deliberately take **no options**: the engine counts *rising*
|
`period` and `frequency` deliberately take **no options**: the engine counts *rising*
|
||||||
crossings of the *mid* level, one edge per cycle, and a split or a side to choose from could
|
crossings of the *mid* level, one edge per cycle, and a split or a side to choose from could
|
||||||
only make that count wrong — a split near either extreme leaves the hysteresis band below no room
|
only make that count wrong — a split near either extreme leaves the hysteresis band below no room
|
||||||
@@ -272,7 +307,7 @@ The split is a share of the **window's own** min..max, not volts: a measurement
|
|||||||
signal in this window, and a level in volts would have to be typed for every signal. For
|
signal in this window, and a level in volts would have to be typed for every signal. For
|
||||||
`top`/`bottom` it says **which side** the level is read from, so any split between the two
|
`top`/`bottom` it says **which side** the level is read from, so any split between the two
|
||||||
plateaus reads the same number — it is no longer a mean *over* the side, which is the reading a
|
plateaus reads the same number — it is no longer a mean *over* the side, which is the reading a
|
||||||
ringing edge used to drag.
|
ringing edge used to drag — and for `above`/`below` it is the level that mean is taken about.
|
||||||
|
|
||||||
Two more options ride on it, both at their old behaviour by default:
|
Two more options ride on it, both at their old behaviour by default:
|
||||||
|
|
||||||
@@ -337,7 +372,9 @@ Each measurement's row carries **two badges**: its own colour (the same one its
|
|||||||
the plot) and the signal's chip, in the colour every other view names that signal with —
|
the plot) and the signal's chip, in the colour every other view names that signal with —
|
||||||
through `RowSet::signal_color()`, so a signal the user re-coloured is re-coloured here too and
|
through `RowSet::signal_color()`, so a signal the user re-coloured is re-coloured here too and
|
||||||
not left at the palette entry. The dialog's fields and their wording follow the type — the split row is absent for
|
not left at the palette entry. The dialog's fields and their wording follow the type — the split row is absent for
|
||||||
`average`, `min` and `max`, the side row only for `pulse duration`, and the "show on plot"
|
`average`, `min` and `max`, the side row only for `pulse duration`, the second share ("to % of the
|
||||||
|
range") only for `between`, which is the one type whose two edges are both parameters, and the
|
||||||
|
words on the split row name its side ("above % of the range" for `above`) — and the "show on plot"
|
||||||
line says what would be drawn.
|
line says what would be drawn.
|
||||||
|
|
||||||
The values are **window statistics over exactly the visible range, at the stored rate**: the
|
The values are **window statistics over exactly the visible range, at the stored rate**: the
|
||||||
@@ -382,7 +419,8 @@ Both walks go through it:
|
|||||||
and one raw scan of that bucket alone then pins it to a sample — two buckets at most, whatever
|
and one raw scan of that bucket alone then pins it to a sample — two buckets at most, whatever
|
||||||
the window's size. The two stamps the scan needed are read at the end rather than at every
|
the window's size. The two stamps the scan needed are read at the end rather than at every
|
||||||
chunk that moved an extreme, since a stamp is a ring read and an extreme moves many times.
|
chunk that moved an extreme, since a stamp is a ring read and an extreme moves many times.
|
||||||
- **Pass two** (the crossing types — `pulse duration`, `period`, `frequency` — and, as a fallback
|
- **Pass two** (the crossing types — `pulse duration`, `period`, `frequency` — the band averages,
|
||||||
|
and, as a fallback
|
||||||
for a window with no histogram, `top`/`bottom`)
|
for a window with no histogram, `top`/`bottom`)
|
||||||
folds a bucket's sums whenever it lies *strictly* on one side of its level — then every sample
|
folds a bucket's sums whenever it lies *strictly* on one side of its level — then every sample
|
||||||
in it falls in the same bin and none is *at* the level, so the fold is exact. It then asks
|
in it falls in the same bin and none is *at* the level, so the fold is exact. It then asks
|
||||||
@@ -396,12 +434,17 @@ samples produced since this frame's `acquire_all()` (bounded by one frame of pro
|
|||||||
for those opened buckets. Nothing is decimated or strided: `peak to peak` still reads
|
for those opened buckets. Nothing is decimated or strided: `peak to peak` still reads
|
||||||
exactly `2` on a unit sine, and the shared pass that carries it is untouched — `top` has no
|
exactly `2` on a unit sine, and the shared pass that carries it is untouched — `top` has no
|
||||||
*exact* reading to protect any more, because it is a flat level read from a density rather than
|
*exact* reading to protect any more, because it is a flat level read from a density rather than
|
||||||
the half-wave mean it used to be (`0.636620` on a unit sine; it is `0.957988` now, near the peak,
|
the half-wave mean it used to be (that mean is now `above 50`, which reads `0.636620` on a unit
|
||||||
because a sine has no plateau). A window shorter than a bucket, or a signal
|
sine where `top` reads `0.957988`, near the peak, because a sine has no plateau). A window shorter
|
||||||
|
than a bucket, or a signal
|
||||||
noisy enough to cross a threshold everywhere, degrades back towards the O(window) cost and nothing
|
noisy enough to cross a threshold everywhere, degrades back towards the O(window) cost and nothing
|
||||||
else changes. The dump's `measure cost:` line reports `covered`, `buckets` and `raw` for exactly
|
else changes. The dump's `measure cost:` line reports `covered`, `buckets` and `raw` for exactly
|
||||||
this: `raw` near `covered` means the pyramid bought nothing — and `raw` is unchanged by the flat
|
this: `raw` near `covered` means the pyramid bought nothing — and `raw` is unchanged by the flat
|
||||||
levels, which read the *same* bucket list a second time and never a sample.
|
levels, which read the *same* bucket list a second time and never a sample, and by the band
|
||||||
|
averages for the same reason (`between` is a **second level in the same walk**: with the
|
||||||
|
measurements probe adding one of every type at a 2 s window, `raw` went from `113966` of
|
||||||
|
`240719031` covered — `0.0%` — with ten types to `134978` of `255372033` — `0.1%` — with
|
||||||
|
thirteen).
|
||||||
|
|
||||||
### What the math channels cost
|
### What the math channels cost
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Invariants: the measurements pane
|
# Invariants: the measurements pane
|
||||||
|
|
||||||
Why the pane is refreshed on a cadence, why neither pass may read its window raw, and how a crossing is counted.
|
Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted, and which mean over a split is which.
|
||||||
|
|
||||||
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
||||||
|
|
||||||
@@ -66,9 +66,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
* **Exactness is the point, so nothing may stride or decimate.** `peak to peak` must still read
|
* **Exactness is the point, so nothing may stride or decimate.** `peak to peak` must still read
|
||||||
exactly `2` on a unit sine, and it and `average` still come straight out of the one shared
|
exactly `2` on a unit sine, and it and `average` still come straight out of the one shared
|
||||||
`measure_stats()` pass. A bucket's sum is a sum. (`top` has no *exact* reading to protect any
|
`measure_stats()` pass. A bucket's sum is a sum. (`top` has no *exact* reading to protect any
|
||||||
more: it is a density estimate and not a mean, and the two places that used to assert
|
more: it is a density estimate and not a mean, so the assertion that used to be
|
||||||
`top == 0.636620` on a sine now assert what the flat level actually reads -- see the flat-level
|
`top == 0.636620` on a sine now belongs to `above 50`, which *is* a mean over a side and reads
|
||||||
invariant below. Everything above is about the *bucket* path being a fast path and not an
|
exactly 2/pi again -- see the band-average invariant below, and the flat-level one for what
|
||||||
|
`top` reads instead. Everything above is about the *bucket* path being a fast path and not an
|
||||||
approximation, and that is unchanged.)
|
approximation, and that is unchanged.)
|
||||||
* **The bucket index space must be the window's.** `ChannelBuffer::acquire()` calls
|
* **The bucket index space must be the window's.** `ChannelBuffer::acquire()` calls
|
||||||
`pyramid_.drop_before(values_->base())`, so a channel's pyramid is in the value ring's
|
`pyramid_.drop_before(values_->base())`, so a channel's pyramid is in the value ring's
|
||||||
@@ -139,6 +140,60 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
`top=0.973824 bottom=-0.991731 amplitude=2.354173 peak to peak=2.354173`, `buckets=0
|
`top=0.973824 bottom=-0.991731 amplitude=2.354173 peak to peak=2.354173`, `buckets=0
|
||||||
raw=200.0%` -- two raw walks and no density estimate, which is the honest reading of a window
|
raw=200.0%` -- two raw walks and no density estimate, which is the honest reading of a window
|
||||||
the pyramid cannot summarize.
|
the pyramid cannot summarize.
|
||||||
|
- **`Above`/`Below`/`Between` are the *means over a side of a share*, and they are the readings
|
||||||
|
`Top`/`Bottom` gave before a flat level replaced them.** That change kept the split's two means as
|
||||||
|
its *internal fallback* for a window the pyramid cannot summarize (above); these three types are
|
||||||
|
that fallback, exposed and named, which is what lets the two different questions about one split be
|
||||||
|
asked side by side -- on a unit sine `above 50` reads `0.636620` (2/pi) where `top` reads
|
||||||
|
`0.957988`, and on the mock's band-limited square `above 50` reads `0.986496` where the plateau is
|
||||||
|
`1.000000` and `top` `0.999983`: the edge's ring is *inside* this mean and not inside that level.
|
||||||
|
Five rules, and the first two are what a reader is most likely to assume wrongly:
|
||||||
|
* **A band is not a wider split, and needs no walk of its own.** `Above`/`Below` are
|
||||||
|
`MeasureLevel::sum[1]`/`count[1]` and `sum[0]`/`count[0]` at the measurement's own share.
|
||||||
|
`Between` is the **difference of two levels' upper sides** -- `sum[1](lo) - sum[1](hi)` over
|
||||||
|
`count[1](lo) - count[1](hi)`, i.e. the samples above the lower edge and *not* above the upper
|
||||||
|
one -- and no single level's two sums can bound a band, because a band's two edges are two
|
||||||
|
thresholds. Both accumulators come out of one pass two over one window (the pane registers one
|
||||||
|
level per edge, `w.pct`, and both are deduplicated into it), so the difference is exact rather
|
||||||
|
than approximate. `v == level` is the below side, as everywhere, so the band is `(lo, hi]`.
|
||||||
|
* **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; only the
|
||||||
|
buckets that straddle either edge are opened, exactly as for one level. `measure cost:` is the
|
||||||
|
guard: adding all three types to a pane of every type left `raw` at `0.1%` of `covered`
|
||||||
|
(`113966` of `240719031` with the ten types, `134978` of `255372033` with the thirteen), and a
|
||||||
|
band is *two entries of the level list*, not a second pass.
|
||||||
|
* **`measure_level_pcts()` is the only place the shares are read**, and it clamps both to `[0,100]`
|
||||||
|
and puts `Between`'s pair **in order**: "between 80 % and 20 %" is the band between 20 % and
|
||||||
|
80 %. Normalized rather than refused because the shares name two *edges*, and the band does not
|
||||||
|
depend on which the user typed first; the pane's dedup key and both accumulators are keyed on the
|
||||||
|
ordered pair, so one band is one walk however it was written.
|
||||||
|
* **Nothing to measure reads `--`**, exactly as for every other type: a side holding no sample (a
|
||||||
|
share at `100 %`, whose level is the window's own maximum) and an *interior* holding none are
|
||||||
|
both `invalid`/`--` and never `0`. The ideal `--square-harmonics 0` square is where the second
|
||||||
|
shows -- every sample is exactly +/-1, so the band `(0, 0.5]` holds nothing -- against the same
|
||||||
|
band on the band-limited square, which holds the edges' transitions (`between 50 75` =
|
||||||
|
`0.302744`, `between 25 75` = `0.000000`: the symmetric band's two halves cancel, the transition
|
||||||
|
being odd about its own midpoint).
|
||||||
|
* **The reading is a mean over the window's *samples*, so its sixth decimal follows the window's
|
||||||
|
boundary.** A live window is closed at both ends, so its oldest and newest sample share a phase
|
||||||
|
and the mean carries one sample that is not in an open interval -- about one unit in the sixth
|
||||||
|
decimal out of 2.6 M samples. Measured over one fast-mock run, `[measure-trace]`: `above 50` on
|
||||||
|
the sine printed `0.636620` on 48 lines and `0.636621` on 2, `below 50` `-0.636620` on 37 and
|
||||||
|
`-0.636619` on 18, `above 50` on the square `0.986496` on 32 and `0.986495` on 18, and
|
||||||
|
`between 50 75` on the ramp `0.250000`/`0.250001`/`0.249999`. The old `Top`/`Bottom` spread the
|
||||||
|
same way -- it is a property of a mean over a finite window, not of these types, and a reading
|
||||||
|
that has to be exact to six figures is read off `[bandavg-probe]`'s nine.
|
||||||
|
Measured on the fast mock, 2 s window, the mock's default 30-harmonic square, `[bandavg-probe]`
|
||||||
|
(nine significant digits, then what the pane prints):
|
||||||
|
`above 50` on the sine `0.636620262` -> `636.62 mV`; `below 50` on the sine `-0.636619283`;
|
||||||
|
`between 50 75` on the sine `0.255873597` -- the analytic `3(2-sqrt 3)/pi = 0.255873`, which is
|
||||||
|
neither the window's mean (`0`) nor the band's edge (`0.5`); `above 50` on the square
|
||||||
|
`0.986494746` / `0.986495505` (one or two samples at the window's end); `between 50 75` on the
|
||||||
|
`ramp` `0.250000385` and `above 50` `0.500000` -- a uniform density makes every band's mean that
|
||||||
|
band's own centre, so a quarter and a half of the range are the shape's prediction and not a
|
||||||
|
coincidence; `above 100` on the `ramp` `--`; and the ideal square's `between 50 75` `--`.
|
||||||
|
Editing the sine's band through the dialog to `50/90` takes it to `0.431361697`
|
||||||
|
(`0.8 / (2 asin 0.8)`), which is the check that the second share is the one the engine reads.
|
||||||
- **`MeasureOptions::noise_pct` is a percentage, clamped in `measure_set_level()` to half the room to the
|
- **`MeasureOptions::noise_pct` is a percentage, clamped in `measure_set_level()` to half the room to the
|
||||||
nearer extreme**, so a band can never invert its own thresholds. It moves the **edge decision only**: the
|
nearer extreme**, so a band can never invert its own thresholds. It moves the **edge decision only**: the
|
||||||
sums still split at the bare level, which is the *fallback* `Top`/`Bottom` read on a window with no
|
sums still split at the bare level, which is the *fallback* `Top`/`Bottom` read on a window with no
|
||||||
@@ -170,12 +225,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
decides that side and the measurement reads nothing at all, which is worse than reading
|
decides that side and the measurement reads nothing at all, which is worse than reading
|
||||||
something noisy. On a ringing 3.3 V logic signal at a 90 % split: `enters=0` with a fixed band,
|
something noisy. On a ringing 3.3 V logic signal at a 90 % split: `enters=0` with a fixed band,
|
||||||
`enters=4 period=0.200000` with the clamp.
|
`enters=4 period=0.200000` with the clamp.
|
||||||
* **The `sum`/`count` split stays at the bare `level`.** Two things read it and neither may be
|
* **The `sum`/`count` split stays at the bare `level`.** Four things read it and none may be
|
||||||
moved by a band: a crossing's *side* is decided by the sample's own value against the level
|
moved by a band: a crossing's *side* is decided by the sample's own value against the level
|
||||||
(`v == level` is the below side, as it always was), and `Top`/`Bottom` fall back to the mean of
|
(`v == level` is the below side, as it always was), `Top`/`Bottom` fall back to the mean of
|
||||||
each side on a window with no bucket histogram. The band is about *deciding a crossing*, not
|
each side on a window with no bucket histogram, and `Above`/`Below`/`Between` read it on
|
||||||
about where a sample lives, so folding it in would change a filtered window's `top` for no gain
|
*every* window -- their whole definition is that split, so a band folded into it would move a
|
||||||
to any crossing count.
|
documented exact reading (2/pi on a sine) for no gain to any crossing count. The band is about
|
||||||
|
*deciding a crossing*, not about where a sample lives.
|
||||||
* **`h = 0` must reduce exactly to the single-level walk**, including the `v == level` skip, so the
|
* **`h = 0` must reduce exactly to the single-level walk**, including the `v == level` skip, so the
|
||||||
band is a parameter of the engine rather than a mode of it.
|
band is a parameter of the engine rather than a mode of it.
|
||||||
The cost is the noise's, not the band's: the clean fast mock reads `raw=119412 (0.1%)` of 215 M at
|
The cost is the noise's, not the band's: the clean fast mock reads `raw=119412 (0.1%)` of 215 M at
|
||||||
|
|||||||
@@ -42,9 +42,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
was parsed into the last calibration record and the enable state was lost without a word.
|
was parsed into the last calibration record and the enable state was lost without a word.
|
||||||
`mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never
|
`mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never
|
||||||
made and the *first* save failed silently -- worth knowing when a layout change "does not stick".
|
made and the *first* save failed silently -- worth knowing when a layout change "does not stick".
|
||||||
- **Measurement options persist under *named* keys -- `noise` and `hf` -- so a file written before them
|
- **Measurement options persist under *named* keys -- `threshold`, `threshold2`, `noise` and `hf` -- so a
|
||||||
loads at the defaults**, which are exactly the behaviour it was written against: `noise = 10` is the
|
file written before them loads at the defaults**, which are exactly the behaviour it was written against:
|
||||||
hysteresis band the app has always counted with, and `hf = 0` is no filter.
|
`noise = 10` is the
|
||||||
|
hysteresis band the app has always counted with, `hf = 0` is no filter, and a file with no
|
||||||
|
`threshold2` -- every file written before `between` existed -- leaves that measurement at
|
||||||
|
`threshold2 = 75`, which is the value the dialog offers a new one. The *pair* is stored as written
|
||||||
|
rather than as ordered: `measure_level_pcts()` is what puts a reversed pair in order on read, so a
|
||||||
|
file records what the user typed and a measurement file round-trips unchanged.
|
||||||
- **The math node string is thirteen pipe fields, and the reader accepts twelve *or* thirteen.** `med_n` is
|
- **The math node string is thirteen pipe fields, and the reader accepts twelve *or* thirteen.** `med_n` is
|
||||||
the thirteenth, after `y`; a twelve-field node is a file written before the de-noise node existed, and its
|
the thirteenth, after `y`; a twelve-field node is a file written before the de-noise node existed, and its
|
||||||
`med_n` stays at the struct's default -- which is what such a file meant. Field 0 is the op as a
|
`med_n` stays at the struct's default -- which is what such a file meant. Field 0 is the op as a
|
||||||
|
|||||||
@@ -218,6 +218,9 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
pane_h=H switch_on=X reset_sensitive=Y`
|
pane_h=H switch_on=X reset_sensitive=Y`
|
||||||
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
|
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
|
||||||
plottable signal and lets the tick compute them, so what it prints is what a user reads.
|
plottable signal and lets the tick compute them, so what it prints is what a user reads.
|
||||||
|
The dump line carries one more field for the one type that reads *two* shares:
|
||||||
|
`measure N: 'sig' between thr=50.0% thr2=75.0% above=1 show=1 -> ok ...`, where `thr2` appears
|
||||||
|
only for `between` (see the band-average probe below).
|
||||||
On the test stream's 1 Hz ±1 sine the values must be: average ≈ 0, `top` ≈ 0.96 and
|
On the test stream's 1 Hz ±1 sine the values must be: average ≈ 0, `top` ≈ 0.96 and
|
||||||
`bottom` ≈ -0.96, `amplitude` ≈ 1.92, max/min ≈ ±1, `peak to peak` **exactly 2**,
|
`bottom` ≈ -0.96, `amplitude` ≈ 1.92, max/min ≈ ±1, `peak to peak` **exactly 2**,
|
||||||
`pulse duration` ≈ 0.5 s
|
`pulse duration` ≈ 0.5 s
|
||||||
@@ -226,7 +229,9 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
window rather than from a mean over a side, and a sine has no plateau — so the densest band
|
window rather than from a mean over a side, and a sine has no plateau — so the densest band
|
||||||
sits at its peak and the reading is near it, not the 0.636620 a mean over a half would give.
|
sits at its peak and the reading is near it, not the 0.636620 a mean over a half would give.
|
||||||
`amplitude` is that pair's delta (`top - bottom`), which is why it reads about 1.92 rather than
|
`amplitude` is that pair's delta (`top - bottom`), which is why it reads about 1.92 rather than
|
||||||
the half-swing it used to: a bench scope's `Vamplitude` is `Vtop - Vbase`. The measured
|
the half-swing it used to: a bench scope's `Vamplitude` is `Vtop - Vbase`. The mean over a half
|
||||||
|
is not gone, though: it is `above 50` on this stream (and `-above 50` is `below 50`), and its
|
||||||
|
own probe asserts it — see `[bandavg-probe]` below. The measured
|
||||||
figures, and the reason they are what they are, are in
|
figures, and the reason they are what they are, are in
|
||||||
[invariants-measurements.md](invariants-measurements.md) — on the mock's `square` the
|
[invariants-measurements.md](invariants-measurements.md) — on the mock's `square` the
|
||||||
separation is the visible one (`top`/`bottom` `0.999983`/`-0.999983`, `amplitude` `1.999966`,
|
separation is the visible one (`top`/`bottom` `0.999983`/`-0.999983`, `amplitude` `1.999966`,
|
||||||
@@ -275,10 +280,15 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
(`an_unselectable_cursor_is_neither_hovered_nor_grabbed`) is the other half. The probe's last
|
(`an_unselectable_cursor_is_neither_hovered_nor_grabbed`) is the other half. The probe's last
|
||||||
step still drags a mark *programmatically* and requires the next frame to put it back
|
step still drags a mark *programmatically* and requires the next frame to put it back
|
||||||
(`restored=1`), which now checks the placement path rather than the read-only property. The dialog report's `type= thr=
|
(`restored=1`), which now checks the placement path rather than the read-only property. The dialog report's `type= thr=
|
||||||
side= show=` is the per-type behaviour: `period` and `frequency` must show **neither** the
|
band= side= show= shares=A/B` is the per-type behaviour: `period` and `frequency` must show **neither** the
|
||||||
split nor the side row (they compute from the mid level's rising crossings, and an option
|
split nor the side row (they compute from the mid level's rising crossings, and an option
|
||||||
there is one the user can only get wrong), `average`/`min`/`max` no split either, and only
|
there is one the user can only get wrong), `average`/`min`/`max` no split either, and only
|
||||||
`pulse duration` a side. A measurement whose signal is not acquired, or
|
`pulse duration` a side. `band` is the *second* share's row, which only `between` shows -- it is
|
||||||
|
the one type whose two edges are both parameters -- and `shares` is what the two spin fields are
|
||||||
|
*showing*, read back out of the widgets: a share that is stored but not seeded into its field, or
|
||||||
|
a field that is built but not read back on apply, is then visible in this line rather than only in
|
||||||
|
the reading it produces. `above`/`below` show the threshold row and no band row, with their own
|
||||||
|
label (`above % of the range`). A measurement whose signal is not acquired, or
|
||||||
a window with too few crossings, must read `invalid`/`--` rather than a made-up number
|
a window with too few crossings, must read `invalid`/`--` rather than a made-up number
|
||||||
(`frequency` on a window holding one edge is the case: `duty` is reported only when a
|
(`frequency` on a window holding one edge is the case: `duty` is reported only when a
|
||||||
period exists to divide by). The probe then **walks the dialog's lifecycle**: open the edit
|
period exists to divide by). The probe then **walks the dialog's lifecycle**: open the edit
|
||||||
@@ -1040,6 +1050,70 @@ phase=2 … n=0 mean=0 sd=0 lo=0 hi=0 (all four)
|
|||||||
- `phase=2` — after Reset. **`n` must be 0 or 1** (0 if the report ran before the next recompute,
|
- `phase=2` — after Reset. **`n` must be 0 or 1** (0 if the report ran before the next recompute,
|
||||||
1 if after); anything larger means Reset cleared nothing.
|
1 if after); anything larger means Reset cleared nothing.
|
||||||
|
|
||||||
|
## `[bandavg-probe]`
|
||||||
|
|
||||||
|
`UDPSCOPE_PROBE_BANDAVG_MS`. Needs `UDPSCOPE_ENABLE_ALL_MS` ahead of it, and it must **not** be
|
||||||
|
scheduled with `UDPSCOPE_PROBE_MEASURE_MS` or `UDPSCOPE_PROBE_MEASURES_MS`: all three add
|
||||||
|
measurements to the same pane, and the later one would report the earlier one's rows as its own.
|
||||||
|
Run it against the fast mock (`--rate 1300000 --batch 1000 --freq 1`) **at a 2 s window** — the
|
||||||
|
figures below are shares of the range, and a window that holds a fifth of a cycle is a different
|
||||||
|
question, not a worse answer. Its own sequence is about 12 s, so `UDPSCOPE_EXIT_AFTER_MS=17000`.
|
||||||
|
|
||||||
|
It adds seven measurements, one per shape whose band mean can be computed by hand, and every one of
|
||||||
|
them is a *mean over a side or a band* rather than a level. `--square-harmonics 30` and
|
||||||
|
`--square-harmonics 0` are two runs — the second is the ideal step, where the band holds nothing:
|
||||||
|
|
||||||
|
| measurement | shape | reading | why that shape |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `above 50` | 1 Hz unit sine | `0.636620` | 2/pi, the figure the old `top` documented |
|
||||||
|
| `below 50` | 1 Hz unit sine | `-0.636620` | its other side: exactly `-above 50` |
|
||||||
|
| `between 50/75` | 1 Hz unit sine | `0.255873` | `3(2-sqrt 3)/pi`, and neither 0 (the window's mean) nor 0.5 (the band's edge) |
|
||||||
|
| `above 50` | `square`, 30 harmonics / `0` | `0.986496` / `1.000000` | the reading that commit recorded as the *old* `top`; the ideal step has no ring to put inside it |
|
||||||
|
| `between 50/75` | `square`, 30 harmonics / `0` | `0.302744` / `--` | the two edges' transitions the band holds; **every** sample is exactly +/-1 on the ideal step, so it holds nothing |
|
||||||
|
| `between 50/75` | `ramp` | `0.250000` | uniform density: a band's mean *is* that band's centre |
|
||||||
|
| `above 100` | `ramp` | `--` | a share at the top of the range has no sample above it |
|
||||||
|
|
||||||
|
Four line kinds, and the reading is the third:
|
||||||
|
|
||||||
|
```
|
||||||
|
[bandavg-probe] waiting stored=2108000 of 2600000 slots for a 2.000 s window
|
||||||
|
[bandavg-probe] readings window channel=0 lo=-1.000000 hi=1.000000 samples=2600000 levels=2
|
||||||
|
[bandavg-probe] readings above 'sine' shares=50% -> ok 0.636620262 text=[636.62 mV]
|
||||||
|
[bandavg-probe] edit opened band_row=1 shares=50.0/75.0 (as stored)
|
||||||
|
[bandavg-probe] after delete measures=6 -> 5 (want one fewer)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `waiting` — the probe reads nothing until the ring holds a **full window**: a share of a *partial*
|
||||||
|
window is a different number (and one that keeps moving while the ring fills), and the mock
|
||||||
|
delivers about 0.5 MS/s of the 1.3 MS/s it declares, so a ring the probe has just re-enabled needs
|
||||||
|
seconds of wall clock before a 2 s window is there. The wait is read off the store
|
||||||
|
(`stored_slots()` against `window / dt`), not guessed from a stopwatch. **A report taken before it
|
||||||
|
finishes is the one reading this probe can produce that is not the share's** — the settled lines
|
||||||
|
are the ones to quote.
|
||||||
|
- `window` — the window each reading was taken over, per channel: `lo`/`hi` are that window's own
|
||||||
|
extremes (so `lo`/`hi` of the split are visible here), `samples` is what it holds, and `levels`
|
||||||
|
is how many level accumulators the pane registered for that channel. **`between` on the sine makes
|
||||||
|
`levels=2` where `top`/`between` alone would make one**: a band is two entries of the level list,
|
||||||
|
and a second measurement of the same band does not add a third.
|
||||||
|
- the value line — the pane's own numbers, nine significant digits and then the row's formatted text
|
||||||
|
(`text=`), with the shares it was given. `-> invalid` with `text=[--]` is the nothing-to-measure
|
||||||
|
reading (the last two rows of the table). The value carries the measurement's **unit**; the three
|
||||||
|
new types are signal-valued like `top`/`average`, so a sine reads `mV` and a quantity with no unit
|
||||||
|
reads the bare number.
|
||||||
|
- `edit opened` — the dialog's second-share row for a `between`: **`band_row` must be 1** (only
|
||||||
|
`between` offers it) and `shares` must be the pair that was *stored*, read back out of the two
|
||||||
|
spin fields. The probe then sets them to `50/90` and saves, and the next report is the proof the
|
||||||
|
field is wired: the sine's `between` reads `0.431361697` (`0.8 / (2 asin 0.8)`), where neither of
|
||||||
|
its sides would.
|
||||||
|
- `after delete` — the cycle's last leg through the same dialog: **one fewer** measurement, process
|
||||||
|
alive. The two shares are the thing a delete could drop on the floor.
|
||||||
|
- The `[measure-trace]` line the probe starts carries the same five readings every 250 ms, which is
|
||||||
|
where the *sixth* decimal's spread is visible (see the band-average invariant in
|
||||||
|
[invariants-measurements.md](invariants-measurements.md)): `above 50` on the sine reads
|
||||||
|
`0.636620` on almost every line and `0.636621` on a few, `above 50` on the square `0.986496` on
|
||||||
|
most and `0.986495` on the rest. **A line's `--` on a *lagging* trace is the trace's, not the
|
||||||
|
reading's** — the pane is refreshed on a cadence, so quote an earlier line beside its own `view=`.
|
||||||
|
|
||||||
## `[search-probe]`
|
## `[search-probe]`
|
||||||
|
|
||||||
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
|
`UDPSCOPE_PROBE_SEARCH_MS`. The handler calls `condition_probe()` directly — the condition is
|
||||||
|
|||||||
@@ -120,13 +120,45 @@ precedent, not an exception.
|
|||||||
thing that reads a second time: `top`/`bottom`/`amplitude` fold a histogram out of the **same**
|
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
|
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
|
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 `--`:
|
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`
|
`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
|
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
|
`[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).
|
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] asked=[..] was=[..]`, `[pan-probe] after=[..] held=D asked=[..]`, then
|
||||||
`[pan-probe] zoom asked=[..] after=[..] span=S window=W limited=L`
|
`[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
|
(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`.
|
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
|
`UDPSCOPE_PROBE_NOISE_MS`, `UDPSCOPE_PROBE_MEASURE_MS`, `UDPSCOPE_PROBE_MEASURES_MS`,
|
||||||
in the same run. The noise and measurement probes both push onto `app.measures`, and the later one would read
|
`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
|
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
|
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.
|
its own measurement legs (see `probe-readings.md`), so nothing is lost by keeping them apart.
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
|
<p>The pane's <gui>+</gui> adds one; clicking a row opens the dialog where the signal,
|
||||||
the type, the threshold share and the on-plot marks are set, with <gui>Save</gui> and
|
the type, the threshold share or shares and the on-plot marks are set, with
|
||||||
<gui>Delete</gui>.</p>
|
<gui>Save</gui> and <gui>Delete</gui>.</p>
|
||||||
</item>
|
</item>
|
||||||
</steps>
|
</steps>
|
||||||
</section>
|
</section>
|
||||||
@@ -35,14 +35,32 @@
|
|||||||
<section id="knowing">
|
<section id="knowing">
|
||||||
<title>Worth knowing</title>
|
<title>Worth knowing</title>
|
||||||
<list>
|
<list>
|
||||||
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, pulse
|
<item><p>The types are average, top, bottom, min, max, peak to peak, amplitude, above,
|
||||||
duration, period and frequency. <gui>top</gui> and <gui>bottom</gui> are the waveform's
|
below, between, pulse duration, period and frequency. <gui>top</gui> and <gui>bottom</gui> are the waveform's
|
||||||
<em>flat</em> high and low levels: the level the signal dwells at, taken from the densest
|
<em>flat</em> high and low levels: the level the signal dwells at, taken from the densest
|
||||||
band of the window on each side of the split. A band-limited edge overshoots and rings —
|
band of the window on each side of the split. A band-limited edge overshoots and rings —
|
||||||
a nominal 2 V square reaches about ±1.18 at its edges — and a ripple or an overshoot is
|
a nominal 2 V square reaches about ±1.18 at its edges — and a ripple or an overshoot is
|
||||||
therefore <em>not</em> what these read. <gui>amplitude</gui> is the delta between those two
|
therefore <em>not</em> what these read. <gui>amplitude</gui> is the delta between those two
|
||||||
levels, and <gui>peak to peak</gui> is the swing between the extremes (so on a rippled
|
levels, and <gui>peak to peak</gui> is the swing between the extremes (so on a rippled
|
||||||
square <gui>amplitude</gui> reads 2 V where <gui>peak to peak</gui> reads 2.36 V).</p></item>
|
square <gui>amplitude</gui> reads 2 V where <gui>peak to peak</gui> reads 2.36 V).</p></item>
|
||||||
|
<item><p><gui>above</gui>, <gui>below</gui> and <gui>between</gui> are the opposite kind of
|
||||||
|
number: the plain <em>mean of the samples</em> above a share, below it, or between two
|
||||||
|
shares. That is a different question from <gui>top</gui>/<gui>bottom</gui> even about the
|
||||||
|
same split, and it is the older one — on a unit sine <gui>above 50%</gui> reads 0.64 V
|
||||||
|
(2/π, the mean of a half cycle) where <gui>top</gui> reads 0.96 V (near the peak, which is
|
||||||
|
where a sine dwells most), and on a band-limited square <gui>above 50%</gui> reads 0.99 V
|
||||||
|
where <gui>top</gui> reads the plateau, 1.00 V: the edge's ring is inside the first and not
|
||||||
|
inside the second. <gui>between</gui> takes <em>two</em> shares — <gui>from %</gui> and
|
||||||
|
<gui>to %</gui> — and reports the mean of the samples inside that band, which no single
|
||||||
|
split can answer; the order you type them in does not matter, since a band between 80 % and
|
||||||
|
20 % is the band between 20 % and 80 %.</p></item>
|
||||||
|
<item><p>A mean over a side carries whatever is on that side, so these three are the right
|
||||||
|
numbers when you want the <em>energy</em> or the <em>offset</em> of part of the waveform,
|
||||||
|
and the wrong ones when you want the level a plateau sits at. Like every other measurement,
|
||||||
|
one with nothing to measure reads <code>--</code> rather than a made-up number: a share at
|
||||||
|
100 % has no sample above it, and a band can hold none at all — on an ideal
|
||||||
|
(non-ringing) square every sample is exactly ±1, so a band between 50 % and 75 % is
|
||||||
|
empty.</p></item>
|
||||||
<item><p><gui>top</gui> and <gui>bottom</gui> also read <em>near the peak</em> on a
|
<item><p><gui>top</gui> and <gui>bottom</gui> also read <em>near the peak</em> on a
|
||||||
waveform that has no flat part, which is the honest reading rather than a failure: a unit
|
waveform that has no flat part, which is the honest reading rather than a failure: a unit
|
||||||
sine has no plateau, so the densest band is at its peak and these read about ±0.96 — not
|
sine has no plateau, so the densest band is at its peak and these read about ±0.96 — not
|
||||||
@@ -95,7 +113,9 @@
|
|||||||
not the raw window: a bucket's sum is a sum, so the mean, the extremes and a level's own
|
not the raw window: a bucket's sum is a sum, so the mean, the extremes and a level's own
|
||||||
mean are exact. <gui>top</gui>, <gui>bottom</gui> and <gui>amplitude</gui> read the
|
mean are exact. <gui>top</gui>, <gui>bottom</gui> and <gui>amplitude</gui> read the
|
||||||
<em>density</em> of the window out of that same summary — which is what tells a flat level
|
<em>density</em> of the window out of that same summary — which is what tells a flat level
|
||||||
from a ripple — so those three cost a second read of the summary and never of a sample.</p></item>
|
from a ripple — so those three cost a second read of the summary and never of a sample, and
|
||||||
|
<gui>between</gui>'s second share is one more level in the same summary walk rather than a
|
||||||
|
walk of its own.</p></item>
|
||||||
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
|
<item><p><gui>Statistics</gui> adds a second line to every measurement showing
|
||||||
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
|
<code>n</code>, the mean, the standard deviation and the range. Each observation is one
|
||||||
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
|
<em>recompute</em> of the pane, so <code>n</code> counts windows measured, not samples —
|
||||||
@@ -117,6 +137,13 @@
|
|||||||
<code>bottom=-0.999983</code>, <code>amplitude=1.999966</code> against
|
<code>bottom=-0.999983</code>, <code>amplitude=1.999966</code> against
|
||||||
<code>peak to peak=2.358701</code> — the levels are the plateau's and the extremes are the
|
<code>peak to peak=2.358701</code> — the levels are the plateau's and the extremes are the
|
||||||
edges'. Over a 2 M-sample window those passes read about 0.1% of the samples.</p>
|
edges'. Over a 2 M-sample window those passes read about 0.1% of the samples.</p>
|
||||||
|
<p>The mean-over-a-side types on the same two windows: on the sine
|
||||||
|
<code>above 50=0.636620</code>, <code>below 50=-0.636620</code> and
|
||||||
|
<code>between 50 75=0.255873</code>; on the rippled square <code>above 50=0.986496</code> and
|
||||||
|
<code>between 50 75=0.302744</code>, against <code>1.000000</code> for the same
|
||||||
|
<code>above 50</code> on an ideal (non-ringing) square. A <code>ramp</code>, whose density is
|
||||||
|
uniform, is the shape that names what a band mean <em>is</em>: <code>above 50=0.500000</code>
|
||||||
|
and <code>between 50 75=0.250000</code>, each band's own centre.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</page>
|
</page>
|
||||||
@@ -612,6 +612,14 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
|||||||
m.options.threshold_pct = std::stod(value);
|
m.options.threshold_pct = std::stod(value);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
}
|
}
|
||||||
|
} else if (key == "threshold2") {
|
||||||
|
// The band's other edge. A file written before `Between` existed has no such key, and the
|
||||||
|
// struct's own default is what such a measurement then reads -- which is the same value the
|
||||||
|
// dialog offers a new one.
|
||||||
|
try {
|
||||||
|
m.options.threshold_pct2 = std::stod(value);
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
} else if (key == "noise") {
|
} else if (key == "noise") {
|
||||||
try {
|
try {
|
||||||
m.options.noise_pct = std::stod(value);
|
m.options.noise_pct = std::stod(value);
|
||||||
@@ -752,6 +760,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
|||||||
}
|
}
|
||||||
o << "\n[[measure]]\nsignal = " << toml_escape(m.signal) << "\ntype = " << kind
|
o << "\n[[measure]]\nsignal = " << toml_escape(m.signal) << "\ntype = " << kind
|
||||||
<< "\nthreshold = " << m.options.threshold_pct
|
<< "\nthreshold = " << m.options.threshold_pct
|
||||||
|
<< "\nthreshold2 = " << m.options.threshold_pct2
|
||||||
<< "\nabove = " << (m.options.above ? "true" : "false") << "\nshow = "
|
<< "\nabove = " << (m.options.above ? "true" : "false") << "\nshow = "
|
||||||
<< (m.options.show_on_plot ? "true" : "false") << "\nnoise = " << m.options.noise_pct
|
<< (m.options.show_on_plot ? "true" : "false") << "\nnoise = " << m.options.noise_pct
|
||||||
<< "\nhf = " << m.options.hf_fc << "\n";
|
<< "\nhf = " << m.options.hf_fc << "\n";
|
||||||
|
|||||||
+92
-28
@@ -342,21 +342,36 @@ MeasureFlatLevels measure_flat_levels(const MeasureStats& b, double pct) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct) {
|
int measure_level_pcts(MeasureType type, const MeasureOptions& options, double pct[2]) {
|
||||||
|
const auto share = [](double v) { return std::clamp(v, 0.0, 100.0) / 100.0; };
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case MeasureType::Top:
|
case MeasureType::Top:
|
||||||
case MeasureType::Bottom:
|
case MeasureType::Bottom:
|
||||||
pct = std::clamp(options.threshold_pct, 0.0, 100.0) / 100.0;
|
case MeasureType::Above:
|
||||||
return true;
|
case MeasureType::Below:
|
||||||
|
pct[0] = share(options.threshold_pct);
|
||||||
|
return 1;
|
||||||
|
case MeasureType::Between: {
|
||||||
|
// The band's two edges, clamped and *ordered*: the shares name a lower and an upper edge, so
|
||||||
|
// "between 80 % and 20 %" is the band between 20 % and 80 %. Normalizing rather than refusing
|
||||||
|
// is the honest reading of a band -- the set of samples does not depend on which edge the user
|
||||||
|
// typed first -- and the two accumulators the pair is answered from are keyed on these ordered
|
||||||
|
// values, so the same band is one walk however it was written.
|
||||||
|
const double a = share(options.threshold_pct);
|
||||||
|
const double b = share(options.threshold_pct2);
|
||||||
|
pct[0] = std::min(a, b);
|
||||||
|
pct[1] = std::max(a, b);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
case MeasureType::PulseDuration:
|
case MeasureType::PulseDuration:
|
||||||
case MeasureType::Period:
|
case MeasureType::Period:
|
||||||
case MeasureType::Frequency:
|
case MeasureType::Frequency:
|
||||||
// The *mid* level, for all three. No option here on purpose: a period is the spacing of
|
// The *mid* level, for all three. No option here on purpose: a period is the spacing of
|
||||||
// one crossing per cycle, and a threshold near either extreme counts noise as crossings.
|
// one crossing per cycle, and a threshold near either extreme counts noise as crossings.
|
||||||
pct = 0.5;
|
pct[0] = 0.5;
|
||||||
return true;
|
return 1;
|
||||||
default:
|
default:
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,11 +388,14 @@ const char* measure_type_name(MeasureType type) {
|
|||||||
case MeasureType::PulseDuration: return "pulse duration";
|
case MeasureType::PulseDuration: return "pulse duration";
|
||||||
case MeasureType::Period: return "period";
|
case MeasureType::Period: return "period";
|
||||||
case MeasureType::Frequency: return "frequency";
|
case MeasureType::Frequency: return "frequency";
|
||||||
|
case MeasureType::Above: return "above";
|
||||||
|
case MeasureType::Below: return "below";
|
||||||
|
case MeasureType::Between: return "between";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
int measure_type_count() { return 10; }
|
int measure_type_count() { return 13; }
|
||||||
|
|
||||||
MeasureType measure_type_at(int index) {
|
MeasureType measure_type_at(int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
@@ -390,15 +408,24 @@ MeasureType measure_type_at(int index) {
|
|||||||
case 6: return MeasureType::Amplitude;
|
case 6: return MeasureType::Amplitude;
|
||||||
case 7: return MeasureType::PulseDuration;
|
case 7: return MeasureType::PulseDuration;
|
||||||
case 8: return MeasureType::Frequency;
|
case 8: return MeasureType::Frequency;
|
||||||
default: return MeasureType::Period;
|
case 9: return MeasureType::Period;
|
||||||
|
// Appended rather than filed beside `Top`/`Bottom`, where they read best in the dropdown:
|
||||||
|
// `layout_store` writes *this* index into the session file, so renumbering would turn a saved
|
||||||
|
// `max` into an `above` on the next start -- silently, and only for users who had one.
|
||||||
|
case 10: return MeasureType::Above;
|
||||||
|
case 11: return MeasureType::Below;
|
||||||
|
default: return MeasureType::Between;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool measure_uses_threshold(MeasureType type) {
|
bool measure_uses_threshold(MeasureType type) {
|
||||||
return type == MeasureType::Top || type == MeasureType::Bottom ||
|
return type == MeasureType::Top || type == MeasureType::Bottom ||
|
||||||
type == MeasureType::PulseDuration;
|
type == MeasureType::PulseDuration || type == MeasureType::Above ||
|
||||||
|
type == MeasureType::Below || type == MeasureType::Between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool measure_uses_band(MeasureType type) { return type == MeasureType::Between; }
|
||||||
|
|
||||||
bool measure_uses_side(MeasureType type) { return type == MeasureType::PulseDuration; }
|
bool measure_uses_side(MeasureType type) { return type == MeasureType::PulseDuration; }
|
||||||
|
|
||||||
bool measure_uses_noise(MeasureType type) {
|
bool measure_uses_noise(MeasureType type) {
|
||||||
@@ -408,8 +435,8 @@ bool measure_uses_noise(MeasureType type) {
|
|||||||
|
|
||||||
bool measure_can_show_on_plot(MeasureType) { return true; }
|
bool measure_can_show_on_plot(MeasureType) { return true; }
|
||||||
|
|
||||||
MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level, MeasureType type,
|
MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* const levels[2],
|
||||||
const MeasureOptions& options) {
|
MeasureType type, const MeasureOptions& options) {
|
||||||
MeasureResult out;
|
MeasureResult out;
|
||||||
if (!b.valid) return out;
|
if (!b.valid) return out;
|
||||||
|
|
||||||
@@ -475,10 +502,10 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
|||||||
// is the value. The band is a *value* band, so the reading does not follow the window's
|
// is the value. The band is a *value* band, so the reading does not follow the window's
|
||||||
// alignment, and it excludes both a plateau's ripple and an edge's overshoot -- which is the
|
// alignment, and it excludes both a plateau's ripple and an edge's overshoot -- which is the
|
||||||
// whole point, because those are exactly what a mean over a side reports instead.
|
// whole point, because those are exactly what a mean over a side reports instead.
|
||||||
double pct = 0.0;
|
double pct[2] = {0.0, 0.0};
|
||||||
if (!measure_level_pct(type, options, pct)) return out;
|
if (measure_level_pcts(type, options, pct) == 0) return out;
|
||||||
const bool want_top = type == MeasureType::Top;
|
const bool want_top = type == MeasureType::Top;
|
||||||
const MeasureFlatLevels flat = measure_flat_levels(b, pct);
|
const MeasureFlatLevels flat = measure_flat_levels(b, pct[0]);
|
||||||
if (want_top ? flat.top_ok : flat.bottom_ok) {
|
if (want_top ? flat.top_ok : flat.bottom_ok) {
|
||||||
out.primary = want_top ? flat.top : flat.bottom;
|
out.primary = want_top ? flat.top : flat.bottom;
|
||||||
out.marks.push_back({true, out.primary});
|
out.marks.push_back({true, out.primary});
|
||||||
@@ -487,11 +514,47 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
|||||||
}
|
}
|
||||||
// No histogram: the mean of one side of the level, straight out of the second pass' running
|
// No histogram: the mean of one side of the level, straight out of the second pass' running
|
||||||
// sums. A window the pyramid cannot summarize is one bucket or two of a zoomed-in view, or
|
// sums. A window the pyramid cannot summarize is one bucket or two of a zoomed-in view, or
|
||||||
// an HF-rejected walk, and on those the split's own means are the honest reading.
|
// an HF-rejected walk, and on those the split's own means are the honest reading -- and they
|
||||||
if (level == nullptr) return out;
|
// are what `Above`/`Below` report on *every* window.
|
||||||
|
const MeasureLevel* lv = levels != nullptr ? levels[0] : nullptr;
|
||||||
const std::size_t s = want_top ? 1 : 0;
|
const std::size_t s = want_top ? 1 : 0;
|
||||||
if (level->count[s] == 0) return out;
|
if (lv == nullptr || lv->count[s] == 0) return out;
|
||||||
out.primary = level->sum[s] / static_cast<double>(level->count[s]);
|
out.primary = lv->sum[s] / static_cast<double>(lv->count[s]);
|
||||||
|
out.marks.push_back({true, out.primary});
|
||||||
|
out.valid = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case MeasureType::Above:
|
||||||
|
case MeasureType::Below: {
|
||||||
|
// The plain mean of the samples on one side of the share -- what `Top`/`Bottom` reported
|
||||||
|
// before they became flat levels, and a different question from them: a mean over a side
|
||||||
|
// carries the ripple and the edge's overshoot (0.986496 on the rippled square where the
|
||||||
|
// plateau is 1), which is exactly why the flat-level types exist beside these.
|
||||||
|
const MeasureLevel* lv = levels != nullptr ? levels[0] : nullptr;
|
||||||
|
const std::size_t s = type == MeasureType::Above ? 1 : 0;
|
||||||
|
if (lv == nullptr || lv->count[s] == 0) return out;
|
||||||
|
out.primary = lv->sum[s] / static_cast<double>(lv->count[s]);
|
||||||
|
out.marks.push_back({true, out.primary});
|
||||||
|
out.valid = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
case MeasureType::Between: {
|
||||||
|
// The band's interior: what lies above the lower edge and *not* above the upper one. Both
|
||||||
|
// accumulators split the same samples the same way -- pass two walks one bucket list and one
|
||||||
|
// filter for the whole window -- so the band is the *difference* of the two upper sides, and
|
||||||
|
// it is exact rather than approximate. It needs no third walk of its own, and the bucket
|
||||||
|
// fast path serves it unchanged: a bucket wholly inside the band is strictly above the lower
|
||||||
|
// level and strictly below the upper, so both accumulators fold it whole.
|
||||||
|
const MeasureLevel* lower = levels != nullptr ? levels[0] : nullptr;
|
||||||
|
const MeasureLevel* upper = levels != nullptr ? levels[1] : nullptr;
|
||||||
|
if (lower == nullptr || upper == nullptr) return out;
|
||||||
|
// Equal counts mean an empty interior: nothing lies in the band, so there is nothing to
|
||||||
|
// average -- the pane reads `--`, as it does for any other type with no samples on the side
|
||||||
|
// it was asked about. `lower` may not count *fewer*, which would say the two accumulators
|
||||||
|
// saw different samples.
|
||||||
|
if (lower->count[1] <= upper->count[1]) return out;
|
||||||
|
out.primary = (lower->sum[1] - upper->sum[1]) /
|
||||||
|
static_cast<double>(lower->count[1] - upper->count[1]);
|
||||||
out.marks.push_back({true, out.primary});
|
out.marks.push_back({true, out.primary});
|
||||||
out.valid = true;
|
out.valid = true;
|
||||||
return out;
|
return out;
|
||||||
@@ -499,18 +562,19 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
|||||||
case MeasureType::PulseDuration:
|
case MeasureType::PulseDuration:
|
||||||
case MeasureType::Period:
|
case MeasureType::Period:
|
||||||
case MeasureType::Frequency: {
|
case MeasureType::Frequency: {
|
||||||
if (level == nullptr) return out;
|
if (levels == nullptr || levels[0] == nullptr) return out;
|
||||||
|
const MeasureLevel& level = *levels[0];
|
||||||
// The pulse is one side of the mid level: its widths run from the edge that enters it to
|
// The pulse is one side of the mid level: its widths run from the edge that enters it to
|
||||||
// the edge that leaves it, and a period is the spacing of the edges that *enter* it -- one
|
// the edge that leaves it, and a period is the spacing of the edges that *enter* it -- one
|
||||||
// per cycle, whatever else the waveform does in between.
|
// per cycle, whatever else the waveform does in between.
|
||||||
const std::size_t s = options.above ? 1 : 0;
|
const std::size_t s = options.above ? 1 : 0;
|
||||||
const double enter_span = level->last_enter[s] - level->first_enter[s];
|
const double enter_span = level.last_enter[s] - level.first_enter[s];
|
||||||
const double enter_period = level->enters[s] >= 2
|
const double enter_period = level.enters[s] >= 2
|
||||||
? enter_span / static_cast<double>(level->enters[s] - 1)
|
? enter_span / static_cast<double>(level.enters[s] - 1)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
if (type == MeasureType::PulseDuration) {
|
if (type == MeasureType::PulseDuration) {
|
||||||
if (level->widths[s] == 0) return out;
|
if (level.widths[s] == 0) return out;
|
||||||
out.primary = level->width_sum[s] / static_cast<double>(level->widths[s]);
|
out.primary = level.width_sum[s] / static_cast<double>(level.widths[s]);
|
||||||
// Duty as width/period, not as pulse-time over the window: a window that starts or
|
// Duty as width/period, not as pulse-time over the window: a window that starts or
|
||||||
// ends mid-pulse holds partial cycles, and counting only the complete ones against
|
// ends mid-pulse holds partial cycles, and counting only the complete ones against
|
||||||
// the whole span would report a duty that depends on where the window happened to
|
// the whole span would report a duty that depends on where the window happened to
|
||||||
@@ -518,15 +582,15 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
|||||||
// them -- a window holding one pulse reports the width alone.
|
// them -- a window holding one pulse reports the width alone.
|
||||||
out.secondary = enter_period > 0.0 ? out.primary / enter_period : 0.0;
|
out.secondary = enter_period > 0.0 ? out.primary / enter_period : 0.0;
|
||||||
out.has_secondary = enter_period > 0.0;
|
out.has_secondary = enter_period > 0.0;
|
||||||
out.marks.push_back({false, level->first_enter[s]});
|
out.marks.push_back({false, level.first_enter[s]});
|
||||||
out.marks.push_back({false, level->first_enter[s] + out.primary});
|
out.marks.push_back({false, level.first_enter[s] + out.primary});
|
||||||
out.valid = true;
|
out.valid = true;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
if (!(enter_period > 0.0)) return out;
|
if (!(enter_period > 0.0)) return out;
|
||||||
out.primary = type == MeasureType::Period ? enter_period : 1.0 / enter_period;
|
out.primary = type == MeasureType::Period ? enter_period : 1.0 / enter_period;
|
||||||
out.marks.push_back({false, level->first_enter[s]});
|
out.marks.push_back({false, level.first_enter[s]});
|
||||||
out.marks.push_back({false, level->first_enter[s] + enter_period});
|
out.marks.push_back({false, level.first_enter[s] + enter_period});
|
||||||
out.valid = true;
|
out.valid = true;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-18
@@ -38,16 +38,37 @@ enum class MeasureType {
|
|||||||
PulseDuration, ///< how long the signal spends on one side of the threshold
|
PulseDuration, ///< how long the signal spends on one side of the threshold
|
||||||
Period, ///< the time between two successive crossings of the threshold
|
Period, ///< the time between two successive crossings of the threshold
|
||||||
Frequency, ///< 1 / period
|
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
|
/// 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
|
/// others ignore it (an option that does nothing for a type is left out of the dialog
|
||||||
/// rather than stored and ignored).
|
/// rather than stored and ignored).
|
||||||
struct MeasureOptions {
|
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
|
/// 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;
|
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`
|
/// `PulseDuration`: which side of the threshold is "the pulse". `Period`/`Frequency`
|
||||||
/// use it as the edge direction, so one cycle is counted once.
|
/// use it as the edge direction, so one cycle is counted once.
|
||||||
bool above = true;
|
bool above = true;
|
||||||
@@ -268,11 +289,13 @@ struct MeasureLevel {
|
|||||||
/// fact rather than a threshold decision. `measure_set_level()` is what sizes it; zero reduces
|
/// fact rather than a threshold decision. `measure_set_level()` is what sizes it; zero reduces
|
||||||
/// the walk exactly to a single level.
|
/// the walk exactly to a single level.
|
||||||
double hysteresis = 0.0;
|
double hysteresis = 0.0;
|
||||||
/// The samples either side of `level`, exactly: `Top`/`Bottom`'s reading on a window with no
|
/// The samples either side of `level`, exactly: the reading of `Above`/`Below` is one of these
|
||||||
/// bucket histogram to read a flat level from (shorter than one bucket, or an HF-rejected
|
/// two means, `Between`'s is the difference of two *levels'* upper sides, and `Top`/`Bottom` fall
|
||||||
/// walk). They are folded on every window -- pass two walks it for the crossing types anyway
|
/// back to them on a window with no bucket histogram to read a flat level from (shorter than one
|
||||||
/// -- but a window that has a histogram is read from that instead, because a mean over a side
|
/// bucket, or an HF-rejected walk). They are folded on every window -- pass two walks it for the
|
||||||
/// reports the ripple and the overshoot rather than the level the signal dwells at.
|
/// 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
|
/// Index 0 is the side below the level (a sample exactly *at* it counts here), index 1 the
|
||||||
/// side above it.
|
/// side above it.
|
||||||
double sum[2] = {0.0, 0.0};
|
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,
|
const std::function<double(std::size_t)>& time_of,
|
||||||
MeasureLevel& level);
|
MeasureLevel& level);
|
||||||
|
|
||||||
/// The level a type reads, as a share of the window's own min..max, or false when it reads none
|
/// The levels a type reads, as shares of the window's own min..max, **lower first**, or 0 when it
|
||||||
/// (the pure statistics, answered from `MeasureStats` alone). `Top`/`Bottom` read the
|
/// reads none (the pure statistics, answered from `MeasureStats` alone). `pct` must hold two: a
|
||||||
/// measurement's own threshold; the crossing types read the mid level, deliberately -- see
|
/// level type fills `pct[0]` and answers 1, `Between` fills both and answers 2.
|
||||||
/// `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
|
/// `Top`/`Bottom`/`Above`/`Below` read the measurement's own threshold; the crossing types read the
|
||||||
/// fallback it takes from the window's own extremes -- see `compute_measure()`.
|
/// mid level, deliberately -- see `measure_uses_threshold()`. `Amplitude` reads no level either: its
|
||||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct);
|
/// 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
|
/// 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
|
/// 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);
|
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
|
/// The engine: one measurement's answer, O(1), from the two passes' results. `levels` is the
|
||||||
/// accumulator for `measure_level_pct(type, options)` and may be null for the types that read
|
/// accumulators `measure_level_pcts(type, options)` asked for, in the same order -- `nullptr` for
|
||||||
/// none. The function reads nothing else, which is what keeps the answer independent of the plot.
|
/// the types that read none, and either entry may be null when the caller could not resolve it.
|
||||||
MeasureResult compute_measure(const MeasureStats& stats, const MeasureLevel* level,
|
/// 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);
|
MeasureType type, const MeasureOptions& options);
|
||||||
|
|
||||||
/// A measurement type's name, as the pane's row and the dialog's dropdown spell it.
|
/// 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
|
/// 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.
|
/// threshold there only invites a setting that makes the count wrong.
|
||||||
bool measure_uses_threshold(MeasureType type);
|
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
|
/// Whether a type uses the `above` side (only `PulseDuration`, where it says which half of
|
||||||
/// the waveform is the pulse).
|
/// the waveform is the pulse).
|
||||||
bool measure_uses_side(MeasureType type);
|
bool measure_uses_side(MeasureType type);
|
||||||
|
|||||||
+24
-8
@@ -1374,12 +1374,8 @@ GtkWidget* measures_row(App& app, std::size_t index, const Measure& m) {
|
|||||||
gtk_widget_set_halign(kind, GTK_ALIGN_START);
|
gtk_widget_set_halign(kind, GTK_ALIGN_START);
|
||||||
gtk_widget_set_hexpand(kind, true);
|
gtk_widget_set_hexpand(kind, true);
|
||||||
gtk_label_set_ellipsize(GTK_LABEL(kind), PANGO_ELLIPSIZE_END);
|
gtk_label_set_ellipsize(GTK_LABEL(kind), PANGO_ELLIPSIZE_END);
|
||||||
char tip[160];
|
const std::string tip = m.signal + "\n" + measure_row_summary(m);
|
||||||
std::snprintf(tip, sizeof(tip), "%s\n%s", m.signal.c_str(),
|
gtk_widget_set_tooltip_text(kind, tip.c_str());
|
||||||
measure_uses_threshold(m.type)
|
|
||||||
? (m.options.above ? "the above-threshold side" : "the below-threshold side")
|
|
||||||
: "");
|
|
||||||
gtk_widget_set_tooltip_text(kind, tip);
|
|
||||||
gtk_box_append(GTK_BOX(box), kind);
|
gtk_box_append(GTK_BOX(box), kind);
|
||||||
GtkWidget* value = gtk_label_new("--");
|
GtkWidget* value = gtk_label_new("--");
|
||||||
gtk_widget_set_halign(value, GTK_ALIGN_END);
|
gtk_widget_set_halign(value, GTK_ALIGN_END);
|
||||||
@@ -1684,6 +1680,17 @@ void open_measure_dialog(App& app, int edit_index) {
|
|||||||
GtkWidget* threshold_row = labelled_row("threshold %", threshold, &threshold_label);
|
GtkWidget* threshold_row = labelled_row("threshold %", threshold, &threshold_label);
|
||||||
gtk_box_append(GTK_BOX(box), threshold_row);
|
gtk_box_append(GTK_BOX(box), threshold_row);
|
||||||
|
|
||||||
|
// `Between`'s second share: the dialog offers it only for the one type that reads two (see
|
||||||
|
// `measure_uses_band()`), and the label is retitled per type with the field above it. The 75 is
|
||||||
|
// `MeasureOptions::threshold_pct2`'s default -- a default the *reader* sees, so it is seeded here
|
||||||
|
// as well as in the struct.
|
||||||
|
GtkWidget* band = spin(0.0, 100.0, 1.0, 1, 75.0);
|
||||||
|
gtk_widget_set_tooltip_text(band,
|
||||||
|
"the band's other edge, the same share of the visible range");
|
||||||
|
GtkWidget* band_label = nullptr;
|
||||||
|
GtkWidget* band_row = labelled_row("to %", band, &band_label);
|
||||||
|
gtk_box_append(GTK_BOX(box), band_row);
|
||||||
|
|
||||||
GtkWidget* above = gtk_switch_new();
|
GtkWidget* above = gtk_switch_new();
|
||||||
gtk_widget_set_halign(above, GTK_ALIGN_START);
|
gtk_widget_set_halign(above, GTK_ALIGN_START);
|
||||||
gtk_widget_set_tooltip_text(above, "which half of the waveform is the pulse");
|
gtk_widget_set_tooltip_text(above, "which half of the waveform is the pulse");
|
||||||
@@ -1714,8 +1721,9 @@ void open_measure_dialog(App& app, int edit_index) {
|
|||||||
gtk_box_append(GTK_BOX(box), show);
|
gtk_box_append(GTK_BOX(box), show);
|
||||||
|
|
||||||
auto* d = new MeasureDialog{&app, edit_index, signal_drop, type_drop,
|
auto* d = new MeasureDialog{&app, edit_index, signal_drop, type_drop,
|
||||||
threshold, above, show, window,
|
threshold, band, above, show,
|
||||||
threshold_row, threshold_label, side_row, side_label,
|
window, threshold_row, threshold_label, band_row,
|
||||||
|
band_label, side_row, side_label,
|
||||||
noise, noise_row, hf, hf_row,
|
noise, noise_row, hf, hf_row,
|
||||||
sig_names};
|
sig_names};
|
||||||
app.measure_dialog = window;
|
app.measure_dialog = window;
|
||||||
@@ -1739,12 +1747,14 @@ void open_measure_dialog(App& app, int edit_index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), editing->options.threshold_pct);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), editing->options.threshold_pct);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(band), editing->options.threshold_pct2);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(noise), editing->options.noise_pct);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(noise), editing->options.noise_pct);
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(hf), editing->options.hf_fc);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(hf), editing->options.hf_fc);
|
||||||
gtk_switch_set_active(GTK_SWITCH(above), editing->options.above);
|
gtk_switch_set_active(GTK_SWITCH(above), editing->options.above);
|
||||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(show), editing->options.show_on_plot);
|
gtk_check_button_set_active(GTK_CHECK_BUTTON(show), editing->options.show_on_plot);
|
||||||
} else {
|
} else {
|
||||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), 50.0);
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(threshold), 50.0);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(band), MeasureOptions{}.threshold_pct2);
|
||||||
gtk_switch_set_active(GTK_SWITCH(above), true);
|
gtk_switch_set_active(GTK_SWITCH(above), true);
|
||||||
}
|
}
|
||||||
g_signal_connect(type_drop, "notify::selected", G_CALLBACK(on_measure_type_changed), d);
|
g_signal_connect(type_drop, "notify::selected", G_CALLBACK(on_measure_type_changed), d);
|
||||||
@@ -5769,6 +5779,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURES_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_MEASURES_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), measures_probe_step, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), measures_probe_step, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_BANDAVG_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), bandavg_probe_step, &app);
|
||||||
|
}
|
||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIT_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_FIT_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), fit_probe_step, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), fit_probe_step, &app);
|
||||||
}
|
}
|
||||||
@@ -6379,6 +6392,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
i, m.signal.c_str(), measure_type_name(m.type), m.options.threshold_pct,
|
i, m.signal.c_str(), measure_type_name(m.type), m.options.threshold_pct,
|
||||||
m.options.above ? 1 : 0, m.options.show_on_plot ? 1 : 0,
|
m.options.above ? 1 : 0, m.options.show_on_plot ? 1 : 0,
|
||||||
r.valid ? "ok" : "invalid", r.valid ? r.primary : 0.0);
|
r.valid ? "ok" : "invalid", r.valid ? r.primary : 0.0);
|
||||||
|
// The band's other edge, only for the type that reads two shares: the reading of a `between`
|
||||||
|
// is a function of the pair, so a line that printed one of them could not say what it is.
|
||||||
|
if (measure_uses_band(m.type)) std::printf(" thr2=%.1f%%", m.options.threshold_pct2);
|
||||||
// What the row *reads*: the formatted string with its unit (so `frequency` showing Hz
|
// What the row *reads*: the formatted string with its unit (so `frequency` showing Hz
|
||||||
// is asserted here, not inferred), and the row's chip count (two: the measurement's
|
// is asserted here, not inferred), and the row's chip count (two: the measurement's
|
||||||
// colour and the signal's).
|
// colour and the signal's).
|
||||||
|
|||||||
@@ -584,6 +584,10 @@ struct App {
|
|||||||
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
|
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
|
||||||
GtkWidget* measure_dialog = nullptr;
|
GtkWidget* measure_dialog = nullptr;
|
||||||
int dbg_measure_step = 0;
|
int dbg_measure_step = 0;
|
||||||
|
/// The band-average probe's own step counter. Its own rather than the measurement pane's: the two
|
||||||
|
/// probes add measurements, so they must not be scheduled in one run (see verification.md), and a
|
||||||
|
/// shared counter would make that a silent mis-step rather than a documented prohibition.
|
||||||
|
int dbg_bandavg_step = 0;
|
||||||
int cursors_pane_mode = -1;
|
int cursors_pane_mode = -1;
|
||||||
int cursors_pane_rows = -1;
|
int cursors_pane_rows = -1;
|
||||||
unsigned cursors_pane_generation = ~0u;
|
unsigned cursors_pane_generation = ~0u;
|
||||||
@@ -1174,6 +1178,9 @@ struct MeasureDialog {
|
|||||||
GtkWidget* signal_drop = nullptr;
|
GtkWidget* signal_drop = nullptr;
|
||||||
GtkWidget* type_drop = nullptr;
|
GtkWidget* type_drop = nullptr;
|
||||||
GtkWidget* threshold = nullptr;
|
GtkWidget* threshold = nullptr;
|
||||||
|
/// `Between`'s *other* edge: the same unit as `threshold`, shown only for the one type that reads
|
||||||
|
/// two shares (see `measure_uses_band()`).
|
||||||
|
GtkWidget* band = nullptr;
|
||||||
GtkWidget* above = nullptr;
|
GtkWidget* above = nullptr;
|
||||||
GtkWidget* show = nullptr;
|
GtkWidget* show = nullptr;
|
||||||
GtkWidget* window = nullptr;
|
GtkWidget* window = nullptr;
|
||||||
@@ -1182,6 +1189,8 @@ struct MeasureDialog {
|
|||||||
/// and the difference has to be in words, not just in visibility.
|
/// and the difference has to be in words, not just in visibility.
|
||||||
GtkWidget* threshold_row = nullptr;
|
GtkWidget* threshold_row = nullptr;
|
||||||
GtkWidget* threshold_label = nullptr;
|
GtkWidget* threshold_label = nullptr;
|
||||||
|
GtkWidget* band_row = nullptr;
|
||||||
|
GtkWidget* band_label = nullptr;
|
||||||
GtkWidget* side_row = nullptr;
|
GtkWidget* side_row = nullptr;
|
||||||
GtkWidget* side_label = nullptr;
|
GtkWidget* side_label = nullptr;
|
||||||
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
|
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
|
||||||
@@ -1502,6 +1511,10 @@ void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offs
|
|||||||
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||||||
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
|
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
|
||||||
plot::u32 measure_color(std::size_t index);
|
plot::u32 measure_color(std::size_t index);
|
||||||
|
/// One line naming what a measurement's type is measuring, with the shares it reads them at: the
|
||||||
|
/// pane's row tooltip. The row's own label is the type alone (as for every type), so this is where
|
||||||
|
/// a user sees the share an `above`/`below`/`between` row is averaging over.
|
||||||
|
std::string measure_row_summary(const Measure& m);
|
||||||
bool visible_samples(App& app, std::size_t channel, std::vector<double>& time, std::vector<double>& value);
|
bool visible_samples(App& app, std::size_t channel, std::vector<double>& time, std::vector<double>& value);
|
||||||
int measure_channel(App& app, const std::string& signal);
|
int measure_channel(App& app, const std::string& signal);
|
||||||
int measure_row(App& app, const std::string& signal);
|
int measure_row(App& app, const std::string& signal);
|
||||||
@@ -1731,6 +1744,10 @@ void measure_dialog_click(App& a, const char* label);
|
|||||||
void measure_dialog_report(App& a, const char* what);
|
void measure_dialog_report(App& a, const char* what);
|
||||||
void measures_probe_next(App& a, int step, guint delay_ms);
|
void measures_probe_next(App& a, int step, guint delay_ms);
|
||||||
gboolean measures_probe_step(gpointer user);
|
gboolean measures_probe_step(gpointer user);
|
||||||
|
void bandavg_probe_next(App& a, int step, guint delay_ms);
|
||||||
|
void bandavg_report(App& a, const char* what);
|
||||||
|
int bandavg_index(App& a, MeasureType type, const char* signal);
|
||||||
|
gboolean bandavg_probe_step(gpointer user);
|
||||||
gboolean zoomkeys_probe(gpointer user);
|
gboolean zoomkeys_probe(gpointer user);
|
||||||
void menus_probe_next(App& a, int step, guint delay_ms);
|
void menus_probe_next(App& a, int step, guint delay_ms);
|
||||||
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
||||||
|
|||||||
+88
-14
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
@@ -21,6 +22,35 @@ plot::u32 measure_color(std::size_t index) {
|
|||||||
return kPalette[index % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
return kPalette[index % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One line naming what a measurement's type measures, with the shares it reads them at. The pane's
|
||||||
|
/// row shows the type alone, as it does for every type, so this is the only place a reader sees
|
||||||
|
/// *which* side of *which* split an `above`/`below` is averaging over, or how wide a `between`
|
||||||
|
/// band is -- and the reading of those three is entirely a function of those two numbers.
|
||||||
|
std::string measure_row_summary(const Measure& m) {
|
||||||
|
const auto pct_text = [](double v) {
|
||||||
|
char text[32];
|
||||||
|
std::snprintf(text, sizeof(text), "%g%% of the range", v);
|
||||||
|
return std::string(text);
|
||||||
|
};
|
||||||
|
switch (m.type) {
|
||||||
|
case MeasureType::Above:
|
||||||
|
return "the mean of the samples above " + pct_text(m.options.threshold_pct);
|
||||||
|
case MeasureType::Below:
|
||||||
|
return "the mean of the samples below " + pct_text(m.options.threshold_pct);
|
||||||
|
case MeasureType::Between:
|
||||||
|
return "the mean of the samples between " + pct_text(m.options.threshold_pct) + " and " +
|
||||||
|
pct_text(m.options.threshold_pct2);
|
||||||
|
case MeasureType::Top:
|
||||||
|
case MeasureType::Bottom:
|
||||||
|
return "the flat level on one side of the split at " + pct_text(m.options.threshold_pct);
|
||||||
|
case MeasureType::PulseDuration:
|
||||||
|
return std::string(m.options.above ? "the pulse above " : "the pulse below ") +
|
||||||
|
pct_text(m.options.threshold_pct);
|
||||||
|
default:
|
||||||
|
return std::string("the ") + measure_type_name(m.type) + " of this window";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
|
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
|
||||||
int measure_channel(App& app, const std::string& signal) {
|
int measure_channel(App& app, const std::string& signal) {
|
||||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||||
@@ -94,6 +124,8 @@ void measure_dialog_sync(MeasureDialog* d) {
|
|||||||
const bool side = measure_uses_side(type);
|
const bool side = measure_uses_side(type);
|
||||||
gtk_widget_set_visible(d->threshold_row, threshold);
|
gtk_widget_set_visible(d->threshold_row, threshold);
|
||||||
gtk_widget_set_visible(d->side_row, side);
|
gtk_widget_set_visible(d->side_row, side);
|
||||||
|
// The band's second share: a `Between` is the one type whose two edges are *both* parameters.
|
||||||
|
gtk_widget_set_visible(d->band_row, measure_uses_band(type));
|
||||||
// The band moves the edge decision, so it is shown for exactly the crossing types. The HF reject sits
|
// The band moves the edge decision, so it is shown for exactly the crossing types. The HF reject sits
|
||||||
// before both walks, so it is shown for all of them.
|
// before both walks, so it is shown for all of them.
|
||||||
gtk_widget_set_visible(d->noise_row, measure_uses_noise(type));
|
gtk_widget_set_visible(d->noise_row, measure_uses_noise(type));
|
||||||
@@ -138,6 +170,24 @@ void measure_dialog_sync(MeasureDialog* d) {
|
|||||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range (picks the side)");
|
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range (picks the side)");
|
||||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the level)");
|
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the level)");
|
||||||
break;
|
break;
|
||||||
|
case MeasureType::Above:
|
||||||
|
case MeasureType::Below: {
|
||||||
|
// These *are* a mean over one side of the split -- the reading the old `top`/`bottom` gave --
|
||||||
|
// so the words have to say which side of which share, and they have to say it beside the
|
||||||
|
// field rather than only in the docs: the same spin means a different side for each type.
|
||||||
|
const bool above = type == MeasureType::Above;
|
||||||
|
gtk_label_set_text(GTK_LABEL(d->threshold_label), above ? "above % of the range"
|
||||||
|
: "below % of the range");
|
||||||
|
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MeasureType::Between:
|
||||||
|
// Two shares, so two rows: the band's edges, lower first (the engine orders a reversed pair
|
||||||
|
// rather than refusing it, so either order the user types reads the same band).
|
||||||
|
gtk_label_set_text(GTK_LABEL(d->threshold_label), "from % of the range");
|
||||||
|
gtk_label_set_text(GTK_LABEL(d->band_label), "to % of the range");
|
||||||
|
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||||
|
break;
|
||||||
case MeasureType::PulseDuration:
|
case MeasureType::PulseDuration:
|
||||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
||||||
gtk_label_set_text(GTK_LABEL(d->side_label), "pulse above the level");
|
gtk_label_set_text(GTK_LABEL(d->side_label), "pulse above the level");
|
||||||
@@ -177,6 +227,7 @@ void on_measure_dialog_apply(GtkButton*, gpointer user) {
|
|||||||
m.signal = d->signals[sig];
|
m.signal = d->signals[sig];
|
||||||
m.type = measure_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop)));
|
m.type = measure_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop)));
|
||||||
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
|
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
|
||||||
|
m.options.threshold_pct2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->band));
|
||||||
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
|
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
|
||||||
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
|
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
|
||||||
m.options.noise_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->noise));
|
m.options.noise_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->noise));
|
||||||
@@ -308,6 +359,10 @@ void update_measures(App& app, bool force, bool accumulate) {
|
|||||||
std::vector<int> chan(app.measures.size(), -1);
|
std::vector<int> chan(app.measures.size(), -1);
|
||||||
std::vector<int> want_window(app.measures.size(), -1);
|
std::vector<int> want_window(app.measures.size(), -1);
|
||||||
std::vector<int> want_level(app.measures.size(), -1);
|
std::vector<int> want_level(app.measures.size(), -1);
|
||||||
|
// `Between` reads two levels, and they are two ordinary entries of the same list: one walk answers
|
||||||
|
// every level in the pane, so a band costs the same pass two as any other measurement plus one
|
||||||
|
// more accumulator in it.
|
||||||
|
std::vector<int> want_level2(app.measures.size(), -1);
|
||||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||||
const Measure& m = app.measures[i];
|
const Measure& m = app.measures[i];
|
||||||
const int channel = measure_channel(app, m.signal);
|
const int channel = measure_channel(app, m.signal);
|
||||||
@@ -335,20 +390,26 @@ void update_measures(App& app, bool force, bool accumulate) {
|
|||||||
m.type == MeasureType::Amplitude) {
|
m.type == MeasureType::Amplitude) {
|
||||||
w.flat_wanted = true;
|
w.flat_wanted = true;
|
||||||
}
|
}
|
||||||
double pct = 0.0;
|
double pct[2] = {0.0, 0.0};
|
||||||
if (!measure_level_pct(m.type, m.options, pct)) continue;
|
const int level_count = measure_level_pcts(m.type, m.options, pct);
|
||||||
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
||||||
// level, so a `Top` is the same walk whatever the band is. Grouping those at the default is what keeps
|
// level, so a `Top` is the same walk whatever the band is. Grouping those at the default is what keeps
|
||||||
// the common pane at exactly one level.
|
// the common pane at exactly one level.
|
||||||
const double noise =
|
const double noise =
|
||||||
measure_uses_noise(m.type) ? m.options.noise_pct : MeasureOptions{}.noise_pct;
|
measure_uses_noise(m.type) ? m.options.noise_pct : MeasureOptions{}.noise_pct;
|
||||||
std::size_t k = 0;
|
for (int j = 0; j < level_count; ++j) {
|
||||||
while (k < w.pct.size() && (w.pct[k] != pct || w.noise[k] != noise)) ++k;
|
std::size_t k = 0;
|
||||||
if (k == w.pct.size()) {
|
while (k < w.pct.size() && (w.pct[k] != pct[j] || w.noise[k] != noise)) ++k;
|
||||||
w.pct.push_back(pct);
|
if (k == w.pct.size()) {
|
||||||
w.noise.push_back(noise);
|
w.pct.push_back(pct[j]);
|
||||||
|
w.noise.push_back(noise);
|
||||||
|
}
|
||||||
|
if (j == 0) {
|
||||||
|
want_level[i] = static_cast<int>(k);
|
||||||
|
} else {
|
||||||
|
want_level2[i] = static_cast<int>(k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
want_level[i] = static_cast<int>(k);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The data work: two walks per channel, whatever the pane holds. The first fixes the window's
|
// The data work: two walks per channel, whatever the pane holds. The first fixes the window's
|
||||||
@@ -483,11 +544,15 @@ void update_measures(App& app, bool force, bool accumulate) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const App::MeasureWindow& w = *wp;
|
const App::MeasureWindow& w = *wp;
|
||||||
const MeasureLevel* level =
|
// In the order `measure_level_pcts()` asked for them: the band's lower edge first.
|
||||||
want_level[i] >= 0 && static_cast<std::size_t>(want_level[i]) < w.level.size()
|
const MeasureLevel* levels[2] = {nullptr, nullptr};
|
||||||
? &w.level[static_cast<std::size_t>(want_level[i])]
|
const int want[2] = {want_level[i], want_level2[i]};
|
||||||
: nullptr;
|
for (int j = 0; j < 2; ++j) {
|
||||||
const MeasureResult r = compute_measure(w.stats, level, m.type, m.options);
|
if (want[j] >= 0 && static_cast<std::size_t>(want[j]) < w.level.size()) {
|
||||||
|
levels[j] = &w.level[static_cast<std::size_t>(want[j])];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const MeasureResult r = compute_measure(w.stats, levels, m.type, m.options);
|
||||||
app.measure_results[i] = r;
|
app.measure_results[i] = r;
|
||||||
if (r.valid) app.measure_last_valid[i] = r;
|
if (r.valid) app.measure_last_valid[i] = r;
|
||||||
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
|
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
|
||||||
@@ -646,11 +711,20 @@ void measure_dialog_report(App& a, const char* what) {
|
|||||||
if (dd != nullptr) {
|
if (dd != nullptr) {
|
||||||
// What the dialog is offering for the type on screen: the option rows' visibility and
|
// What the dialog is offering for the type on screen: the option rows' visibility and
|
||||||
// the words on them, which is the per-type behaviour a user sees.
|
// the words on them, which is the per-type behaviour a user sees.
|
||||||
std::snprintf(options, sizeof(options), "type=%d thr=%d side=%d show=%d '%s'",
|
std::snprintf(options, sizeof(options), "type=%d thr=%d band=%d side=%d show=%d "
|
||||||
|
"shares=%.1f/%.1f '%s'",
|
||||||
static_cast<int>(gtk_drop_down_get_selected(GTK_DROP_DOWN(dd->type_drop))),
|
static_cast<int>(gtk_drop_down_get_selected(GTK_DROP_DOWN(dd->type_drop))),
|
||||||
dd->threshold_row != nullptr && gtk_widget_get_visible(dd->threshold_row) ? 1 : 0,
|
dd->threshold_row != nullptr && gtk_widget_get_visible(dd->threshold_row) ? 1 : 0,
|
||||||
|
dd->band_row != nullptr && gtk_widget_get_visible(dd->band_row) ? 1 : 0,
|
||||||
dd->side_row != nullptr && gtk_widget_get_visible(dd->side_row) ? 1 : 0,
|
dd->side_row != nullptr && gtk_widget_get_visible(dd->side_row) ? 1 : 0,
|
||||||
dd->show != nullptr && gtk_widget_get_visible(dd->show) ? 1 : 0,
|
dd->show != nullptr && gtk_widget_get_visible(dd->show) ? 1 : 0,
|
||||||
|
// What the two share fields are *showing*: the pair a `between` reads, read back
|
||||||
|
// out of the widgets, so a field that is built but not seeded or not read back is
|
||||||
|
// visible in the line rather than only in the number it produces.
|
||||||
|
dd->threshold != nullptr
|
||||||
|
? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->threshold))
|
||||||
|
: 0.0,
|
||||||
|
dd->band != nullptr ? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->band)) : 0.0,
|
||||||
dd->show != nullptr && gtk_check_button_get_child(GTK_CHECK_BUTTON(dd->show)) !=
|
dd->show != nullptr && gtk_check_button_get_child(GTK_CHECK_BUTTON(dd->show)) !=
|
||||||
nullptr
|
nullptr
|
||||||
? gtk_label_get_text(GTK_LABEL(gtk_check_button_get_child(
|
? gtk_label_get_text(GTK_LABEL(gtk_check_button_get_child(
|
||||||
|
|||||||
@@ -2842,6 +2842,225 @@ gboolean measures_probe_step(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The band-average readings, on the shapes whose band means can be computed by hand.
|
||||||
|
///
|
||||||
|
/// `above`, `below` and `between` are plain means of the samples on one side of a share -- what
|
||||||
|
/// `top`/`bottom` reported before a flat level replaced them -- and a mean over a side is a
|
||||||
|
/// *different number* from the level the signal dwells at, so each reading is asserted where the
|
||||||
|
/// shape predicts it. Seven measurements are added on the mock's three scalar signals at once,
|
||||||
|
/// because the trio's point is that a *pair* of levels answers a band no single split can:
|
||||||
|
/// * the 1 Hz unit sine: `above 50` = 2/pi = 0.636620 and `below 50` = -2/pi, the figures the old
|
||||||
|
/// `top`/`bottom` documented (and *not* `top`/`bottom`'s 0.957988, the level a sine dwells at
|
||||||
|
/// near its peak), and `between 50 75` = 3(2-sqrt 3)/pi = 0.255873 -- the mean of the samples in
|
||||||
|
/// the band (0, 0.5], which is neither 0 (the whole window's mean) nor 0.5 (the band's edge);
|
||||||
|
/// * 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 edges'
|
||||||
|
/// ring is inside this mean, and the plateau the signal dwells at is not;
|
||||||
|
/// * the mock's `ramp`, whose density is uniform: every band's mean is that band's own centre, so
|
||||||
|
/// `above 50` = 0.5 and `between 50 75` = 0.25 of the range -- a pair of figures no other
|
||||||
|
/// measurement type on this shape reports (`average` is 0 there, `top`/`bottom` its edges).
|
||||||
|
/// The dialog leg then edits the sine's band through the real widgets -- the second share is the
|
||||||
|
/// one field only `between` offers -- and re-reads the value, so a share that is built but not
|
||||||
|
/// seeded or not read back shows up as a reading that did not move.
|
||||||
|
gboolean bandavg_probe_step(gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
switch (a->dbg_bandavg_step) {
|
||||||
|
case 0: {
|
||||||
|
// The three shapes are enabled *here* rather than assumed: `enable_all_probe` switches a
|
||||||
|
// signal off two seconds after it runs, and a probe that read another probe's leftovers
|
||||||
|
// would measure whatever was left. `set_signals_enabled()` is one structural change and a
|
||||||
|
// no-op when the state already holds, so the usual case costs nothing and the ring keeps
|
||||||
|
// its history; when it does rebuild, the report below is far enough out for a full window.
|
||||||
|
std::vector<std::size_t> wanted;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||||
|
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||||
|
for (std::size_t s = 0; s < signals.size(); ++s) {
|
||||||
|
if (signals[s].name == "sine" || signals[s].name == "square" ||
|
||||||
|
signals[s].name == "ramp") {
|
||||||
|
wanted.push_back(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a->model->set_signals_enabled(wanted, true);
|
||||||
|
struct Leg {
|
||||||
|
const char* signal;
|
||||||
|
MeasureType type;
|
||||||
|
double lo;
|
||||||
|
double hi;
|
||||||
|
};
|
||||||
|
const Leg legs[] = {
|
||||||
|
{"sine", MeasureType::Above, 50.0, 0.0},
|
||||||
|
{"sine", MeasureType::Below, 50.0, 0.0},
|
||||||
|
{"sine", MeasureType::Between, 50.0, 75.0},
|
||||||
|
{"square", MeasureType::Above, 50.0, 0.0},
|
||||||
|
// The same band on the square: on the band-limited one it holds the two edges' transitions
|
||||||
|
// -- a real interior whose mean is ~0, because a transition is odd about its own midpoint --
|
||||||
|
// and on the ideal `--square-harmonics 0` step it holds *nothing at all*, because every
|
||||||
|
// sample is exactly +/-1. That pair is the empty-interior rule from both sides.
|
||||||
|
{"square", MeasureType::Between, 50.0, 75.0},
|
||||||
|
{"ramp", MeasureType::Between, 50.0, 75.0},
|
||||||
|
// A share at the top of the range has no sample above it, so this one must read what any
|
||||||
|
// other measurement with nothing to measure reads -- `invalid`/`--`, never 0. The empty
|
||||||
|
// *interior* is the same rule from the other side, and the ideal square below is where it
|
||||||
|
// shows: its samples are all exactly +/-1, so the band (0, 0.5] holds none of them.
|
||||||
|
{"ramp", MeasureType::Above, 100.0, 0.0},
|
||||||
|
};
|
||||||
|
for (const Leg& leg : legs) {
|
||||||
|
Measure m;
|
||||||
|
m.signal = leg.signal;
|
||||||
|
m.type = leg.type;
|
||||||
|
m.options.threshold_pct = leg.lo;
|
||||||
|
if (leg.hi > 0.0) m.options.threshold_pct2 = leg.hi;
|
||||||
|
// The marks are part of the reading for these types too: one ruler at the mean.
|
||||||
|
m.options.show_on_plot = measure_can_show_on_plot(m.type);
|
||||||
|
a->measures.push_back(std::move(m));
|
||||||
|
}
|
||||||
|
select_side_tab(*a, "measure");
|
||||||
|
rebuild_measures_pane(*a);
|
||||||
|
g_timeout_add(250, measure_trace, a);
|
||||||
|
bandavg_probe_next(*a, 1, 1500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
// A *full* window before any reading. The window is what the pane last described -- two seconds
|
||||||
|
// on the documented run -- and the mock delivers about 0.5 MS/s of the 1.3 MS/s it declares, so
|
||||||
|
// a ring the probe has just re-enabled needs seconds of wall clock to hold a whole one. A short
|
||||||
|
// ring reads a *partial* window, which is a different number and keeps moving while it fills,
|
||||||
|
// and the ring is capacity-bound as well, so the wait is read off the store rather than guessed:
|
||||||
|
// `stored_slots` is how much history there is and `window / dt` is what the pane is measuring.
|
||||||
|
double slot_dt = 0.0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||||
|
slot_dt = a->model->partition().slot_dt();
|
||||||
|
}
|
||||||
|
const double window_s = a->measure_window_valid
|
||||||
|
? a->measure_window.max - a->measure_window.min
|
||||||
|
: 0.0;
|
||||||
|
const std::uint64_t want =
|
||||||
|
slot_dt > 0.0 && window_s > 0.0 ? static_cast<std::uint64_t>(window_s / slot_dt) : 0;
|
||||||
|
const std::uint64_t have = a->model->stored_slots();
|
||||||
|
if (have < want) {
|
||||||
|
std::fprintf(stderr, "[bandavg-probe] %-15s stored=%llu of %llu slots for a %.3f s window\n",
|
||||||
|
"waiting", static_cast<unsigned long long>(have),
|
||||||
|
static_cast<unsigned long long>(want), window_s);
|
||||||
|
std::fflush(stderr);
|
||||||
|
bandavg_probe_next(*a, 1, 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bandavg_report(*a, "readings");
|
||||||
|
bandavg_probe_next(*a, 2, 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
// The `between` on the sine, through the ordinary edit path: its dialog is the one that
|
||||||
|
// offers the second share, and this is where the field is read back out of the widgets.
|
||||||
|
const int at = bandavg_index(*a, MeasureType::Between, "sine");
|
||||||
|
if (at < 0) {
|
||||||
|
std::fprintf(stderr, "[bandavg-probe] no sine/between measurement to edit\n");
|
||||||
|
std::fflush(stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
open_measure_dialog(*a, at);
|
||||||
|
bandavg_probe_next(*a, 3, 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
bandavg_report(*a, "edit opened");
|
||||||
|
auto* d = static_cast<MeasureDialog*>(
|
||||||
|
g_object_get_data(G_OBJECT(a->measure_dialog), "udpscope-measure-ctx"));
|
||||||
|
if (d != nullptr) {
|
||||||
|
// The two fields as the *stored* parameters came back into them, and the band row's
|
||||||
|
// visibility: a share that is stored but not seeded, or offered for a type that does not
|
||||||
|
// read it, is a field the user cannot edit back to what the pane is showing.
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[bandavg-probe] %-15s band_row=%d shares=%.1f/%.1f (as stored)\n",
|
||||||
|
"edit opened", gtk_widget_get_visible(d->band_row) ? 1 : 0,
|
||||||
|
gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold)),
|
||||||
|
gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->band)));
|
||||||
|
// From the mid split to 90 %: a band whose mean neither of its sides reports (the readings
|
||||||
|
// above 50 and below 90 are 0.636620 and 0.431362), so the value after the save can only
|
||||||
|
// come from the pair the two fields now hold.
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->threshold), 50.0);
|
||||||
|
gtk_spin_button_set_value(GTK_SPIN_BUTTON(d->band), 90.0);
|
||||||
|
}
|
||||||
|
measure_dialog_click(*a, "Save");
|
||||||
|
bandavg_probe_next(*a, 4, 500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
bandavg_report(*a, "after band edit");
|
||||||
|
bandavg_probe_next(*a, 5, 300);
|
||||||
|
break;
|
||||||
|
case 5: {
|
||||||
|
// The delete half of the cycle, through the dialog's own button: a measurement that carries
|
||||||
|
// two shares is shown to survive add, edit and delete like the ones that carry none.
|
||||||
|
const int at = bandavg_index(*a, MeasureType::Between, "sine");
|
||||||
|
if (at >= 0) open_measure_dialog(*a, at);
|
||||||
|
bandavg_probe_next(*a, 6, 400);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const std::size_t before = a->measures.size();
|
||||||
|
// `measure_dialog_click()` falls through to the emit when it cannot find its button, so the
|
||||||
|
// dialog being there is this side's to check, not the helper's.
|
||||||
|
if (a->measure_dialog != nullptr) measure_dialog_click(*a, "Delete");
|
||||||
|
std::fprintf(stderr, "[bandavg-probe] %-15s measures=%zu -> %zu (want one fewer)\n",
|
||||||
|
"after delete", before, a->measures.size());
|
||||||
|
std::fflush(stderr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bandavg_probe_next(App& a, int step, guint delay_ms) {
|
||||||
|
a.dbg_bandavg_step = step;
|
||||||
|
g_timeout_add(delay_ms, bandavg_probe_step, &a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of one of the probe's measurements, by the pair that names it: a `between` on the sine
|
||||||
|
/// and a `between` on the ramp are two measurements of one type.
|
||||||
|
int bandavg_index(App& a, MeasureType type, const char* signal) {
|
||||||
|
for (std::size_t i = 0; i < a.measures.size(); ++i) {
|
||||||
|
if (a.measures[i].type == type && a.measures[i].signal == signal) return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the pane out: one line per measurement the probe added, with the shares it was given and
|
||||||
|
/// the two readings a user could compare (the value and the row's own formatted text). The numbers
|
||||||
|
/// come from a *forced* recompute over the window on screen now, so they are what the pane says at
|
||||||
|
/// this instant rather than whatever the last moving frame left behind.
|
||||||
|
void bandavg_report(App& a, const char* what) {
|
||||||
|
update_measures(a, /*force=*/true, /*accumulate=*/false);
|
||||||
|
// The window each reading was taken over, beside it: a mean over a share of a *partial* window is
|
||||||
|
// a different number from the same share of a full one, and nothing in the value says which it
|
||||||
|
// was. `samples` against the ring's span is what a short ring looks like from here.
|
||||||
|
for (const App::MeasureWindow& w : a.measure_samples) {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[bandavg-probe] %-15s window channel=%d lo=%.6f hi=%.6f samples=%zu levels=%zu\n",
|
||||||
|
what, w.channel, w.stats.lo, w.stats.hi, w.stats.count, w.level.size());
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < a.measures.size(); ++i) {
|
||||||
|
const Measure& m = a.measures[i];
|
||||||
|
const MeasureResult& r = i < a.measure_results.size() ? a.measure_results[i] : MeasureResult{};
|
||||||
|
char shares[64];
|
||||||
|
if (measure_uses_band(m.type)) {
|
||||||
|
std::snprintf(shares, sizeof(shares), "%.0f/%.0f%%", m.options.threshold_pct,
|
||||||
|
m.options.threshold_pct2);
|
||||||
|
} else {
|
||||||
|
std::snprintf(shares, sizeof(shares), "%.0f%%", m.options.threshold_pct);
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[bandavg-probe] %-15s %-7s '%s' shares=%-9s -> %-8s %.9g text=[%s]\n",
|
||||||
|
what, measure_type_name(m.type), m.signal.c_str(), shares,
|
||||||
|
r.valid ? "ok" : "invalid", r.valid ? r.primary : 0.0,
|
||||||
|
i < a.measure_text.size() ? a.measure_text[i].c_str() : "");
|
||||||
|
}
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
gboolean zoomkeys_probe(gpointer user) {
|
gboolean zoomkeys_probe(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->rows->row_count() == 0 || a->rows->row(0).plot == nullptr) return G_SOURCE_REMOVE;
|
if (a->rows->row_count() == 0 || a->rows->row(0).plot == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
|||||||
Reference in new issue
Block a user