docs: record the three noise-rejection controls and their invariants
README gains the three controls where each lives -- the trigger's `band`/`reject`, the measurement's `noise`/`hf`, and the math channel's `de-noise` node -- each with its unit, what "off" is, and what it costs, plus the two deliberate deviations from the design spec next to the features they belong to: the band applies to `edge` and `window` only (a pulse's width is measured from a crossing to its return, so a band there changes the measured width rather than rejecting noise; a dropout is an absence with no level to compare), and the band is verified through `BufferSearch` hit counts rather than `[condition-probe]` latch times, because probe-readings.md states that a `t0` is not an assertion. The invariant docs gain the rules a later change must not break: `EventCondition`'s two new fields and the once-per-packet `configure(dt)` that must not reset; the crossing rule that a change is between two *decided* sides; the measurement path's exception to "a readout quotes a stored sample", why the band moves the edge decision only, why `measure_accumulate` loops samples outer, and why a filtered window disables both bucket fast paths; that `MathOp` is append-only (the index *is* the serialisation), that the median window is pooled in the runtime rather than held per node, why the pole reuses `b0`/`z1`, and that median-then-pole is the design; the session's named `noise`/`hf` keys and its thirteen-field node string that still reads twelve; and the two GTK traps -- positionally brace-initialised dialogs, and a control that cannot apply being hidden rather than disabled.
This commit is contained in:
1 parent
c38630643d
commit
749b34a2cb
7 files changed
+114
-1
No files matched your search
@@ -18,6 +18,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
points (`on_trigger_field_changed`, `on_trigger_drop_changed`) onto one
|
||||
`publish_trigger_fields()`: changing the edge or Normal/Single dropdown used to segfault
|
||||
outright. `UDPSCOPE_PROBE_FIELDS_MS` is the guard.
|
||||
- **`MeasureDialog` and `MathDialog` are brace-initialised *positionally* in `src/ui.cpp`**, so a field
|
||||
added in the middle of either silently shifts every argument after it: add at the end of the widget group
|
||||
and read the initialiser in the same commit. A row array widened by a new control is the same trap from
|
||||
the other side -- a `rows[N]` that was not grown shows a control the sync never touches, and the compiler
|
||||
says so as an out-of-bounds warning on `rows`.
|
||||
- **A control that cannot affect the current type is *hidden*, not disabled**: the trigger's band row is
|
||||
hidden for `dropout`, the measurement's for every non-crossing type, and the node editor's median row for
|
||||
every op but `de-noise`. A greyed row is a question the user can see and cannot answer.
|
||||
- **`GtkDropDown`'s `notify::selected` arrives after the `set_selected()` that caused it
|
||||
returns**, so the `updating_panels` flag — set around a programmatic write and cleared when
|
||||
it returns — does *not* suppress it. Where the write is part of a readout the handler reads
|
||||
|
||||
@@ -241,6 +241,24 @@ and reading values off the widgets *inside* the editor cannot see that. It is th
|
||||
* **A `Delete` key removes the selected *wire* and never falls through to the selected node.**
|
||||
A key whose target depends on an invisible selection state is how a graph gets destroyed by
|
||||
accident; the `delete node` button stays the only way to remove a node.
|
||||
- **`MathOp` is append-only, because `math_op_at()` *is* the serialisation.** A session stores a node's op
|
||||
as its index through that accessor, so an op inserted in the middle reinterprets every node in every file
|
||||
written before it; `DeNoise` is therefore index 12, after `Div`, and sits last in the dropdown.
|
||||
`math_op_uses_filter()` keeps its meaning of *a biquad* -- `math_op_uses_fc()` is the wider question
|
||||
("does this op read `fc` at all") and is what the dialog's corner row asks.
|
||||
- **The de-noise node's median window is *pooled* in `MathRuntime::median`, not held in `MathNodeRt`**, so a
|
||||
node's state stays on one cache line; nine doubles per node would push every node in the graph, filter or
|
||||
not, across two. `MathNodeRt::med_at` / `::med_n` / `::med_pos` / `::med_have` are that pool's slice,
|
||||
length, write position and fill, and the pool is sized in `math_prepare()`.
|
||||
- **Median *then* pole, and the order is the design.** The median kills an impulse outright -- one shorter
|
||||
than half its window cannot move it at all -- and the pole takes the broadband hash it leaves behind. The
|
||||
other order feeds the pole the impulse, which smears it over the pole's whole settling time and leaves the
|
||||
median a wide bump it cannot reject.
|
||||
- **The de-noise pole reuses `b0` and `z1`, deliberately.** `math_prepare()` carries `z1` across a
|
||||
re-prepare, so the pole keeps its history through a decimation change exactly as the biquads do -- while the
|
||||
median window starts from rest, the same choice the biquads' `z2` makes. `median_n()` rounds *up* to odd and
|
||||
caps at 9, and it is what enforces the window rather than the dialog: a session file holding 4 or 40 still
|
||||
loads. The cap is what `double sorted[9]` in the step is sized for.
|
||||
- **The math channels' cost is the *producer's*, and the instrument must not be part of it.**
|
||||
`push_math_sample()` runs once per stored slot -- up to the wire rate -- so what a regression
|
||||
here looks like is `gaps`, never a frame time. Three things were taken off that path and must
|
||||
|
||||
@@ -76,6 +76,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
the pyramid for nothing.
|
||||
`measure cost:`'s `covered`/`buckets`/`raw` is the guard, and it needs the fast mock to say
|
||||
anything.
|
||||
- **`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
|
||||
sums still split at the bare level, because `Top` on a unit sine must read 2/pi (0.636620).
|
||||
- **`measure_accumulate` loops samples outer, levels inner, and that is not backwards.** The filter holds one
|
||||
state for the whole walk, so levels-outer would step it once per level *per sample* and hand every level but
|
||||
the last a different filter state.
|
||||
- **A non-zero `hf_fc` disables *both* bucket fast paths**, with the single
|
||||
`if (!filtered && reader.bucket_range(...))`. An empty `buckets` makes `bulk_from == bulk_to == n` *and*
|
||||
leaves `lo_search` / `hi_search` at 0, so `pin()` becomes a no-op -- and that is the right answer, because a
|
||||
bucket is an aggregate of unfiltered samples and cannot be mixed into a filtered walk in either direction.
|
||||
- **A crossing is counted with hysteresis, and the band moves the *edge decision* only.**
|
||||
`MeasureLevel::hysteresis` is a Schmitt trigger about `level`: an edge is counted once the signal
|
||||
has passed `level + h` going up or `level - h` going down, and a sample inside the band decides
|
||||
|
||||
@@ -32,6 +32,15 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`crosshair_interpolated_mode_blends_the_two_samples` and
|
||||
`the_crosshair_mark_is_bigger_than_a_sample_dot`, plus `[cursors-probe]`'s `first mark at x=..
|
||||
y=..` agreeing with the pane's first value.
|
||||
- **A measurement with a non-zero `hf_fc` is the one exception**, and it is inherent rather than a rounding
|
||||
error. Its `min`, `max`, `peak to peak`, `average`, `top` and `bottom` are computed from a one-pole-filtered
|
||||
copy of the window, so the quoted value need not equal any stored sample -- and with `show_on_plot` its
|
||||
ruler sits at a level the drawn trace, which is raw, never reaches. This is what filtering *is*; it must
|
||||
not be "fixed" by snapping the readout to the nearest sample. Every measurement with `hf_fc == 0`, which is
|
||||
the default, still quotes a stored sample exactly as the rule above says. The filter is seeded with the
|
||||
window's **first sample** and never from history before the window, so a measurement never depends on data
|
||||
outside the window it reports on: the left-edge step transient is removed by the seeding, not by priming
|
||||
from earlier samples.
|
||||
- **`format_measure()` is the one formatter for both panes, and its SI prefix does not depend on
|
||||
there being a unit.** The cursors pane's values and the measurements pane's `text=` go through
|
||||
it, so a change reaches both. The prefix ladder used to be inside `if (!unit.empty())`, which
|
||||
|
||||
@@ -23,6 +23,13 @@ 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.
|
||||
`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".
|
||||
- **Measurement options persist under *named* keys -- `noise` and `hf` -- so a file written before them
|
||||
loads at the defaults**, which are exactly the behaviour it was written against: `noise = 10` is the
|
||||
hysteresis band the app has always counted with, and `hf = 0` is no filter.
|
||||
- **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
|
||||
`med_n` stays at the struct's default -- which is what such a file meant. Field 0 is the op as a
|
||||
`math_op_at()` index, which is why `MathOp` may only be *appended* to.
|
||||
- **The export is collected at click time, not in the file-dialog callback.** The live
|
||||
view keeps scrolling while the chooser is up, so a late collection would write a
|
||||
window the user never saw. The writer is picked by the chosen path's extension.
|
||||
|
||||
@@ -45,6 +45,18 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
be **negative** when the new ring starts after the edge. That is a faithful reading, not a
|
||||
defect: `[-pre_s, post_s]` still describes the samples the snapshot holds. The guard is
|
||||
`UDPSCOPE_PROBE_TRIGRENUM_MS`, whose `t0_held=1` is the assertion.
|
||||
- **`EventCondition` carries the trigger's own rejection: `noise_band` and `hf_fc`.** Both are in the
|
||||
struct's `operator==`, so a change to either *is* a change to the condition. `EventDetector::configure()`
|
||||
takes `dt` and early-returns only when the condition **and** the interval are unchanged -- it runs on
|
||||
*every packet*, so a configure that reset unconditionally would restart the machine before any pulse could
|
||||
close. `dt` is `Partition::slot_dt()`, the *stored* interval and not the CONFIG's nominal rate, and comes
|
||||
from exactly three call sites (`acquisition.cpp`, `ui_trigger.cpp`, `ui_probe.cpp`); a zero there leaves
|
||||
the reject's coefficient at 1, which is a pass-through.
|
||||
- **A crossing is a change between two *decided* sides, never a change from the undecided one.** The band
|
||||
makes each sample decide above / below / undecided, and an edge is counted only when the signal has reached
|
||||
the far threshold of the side it is leaving. Seeding the previous side from `0` -- the undecided code --
|
||||
fires on a signal that starts *inside* the band and leaves it without ever reaching the level, an event the
|
||||
trigger is not supposed to see.
|
||||
- **A pinned window is *clamped* into the event every frame, and a zoom in is allowed.**
|
||||
While an event is held, a capture is filling, or the view is paused, `tick()` calls
|
||||
`RowSet::hold_pinned_window()`, which caps the span at `[-pinned_pre_, +pinned_post_]`'s and
|
||||
|
||||
Reference in new issue
Block a user