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
@@ -208,6 +208,23 @@ to reject noise with, and counting both edges halves the period.
|
||||
The split is a share of the **window's own** min..max, not volts: a measurement is about this
|
||||
signal in this window, and a level in volts would have to be typed for every signal.
|
||||
|
||||
Two more options ride on it, both at their old behaviour by default:
|
||||
|
||||
| option | what it is | unit | off |
|
||||
|---|---|---|---|
|
||||
| `noise` | the hysteresis band's half-width, as the next section describes | % of the window's peak-to-peak | `0` disables the band; the default `10` is the band the app has always counted with |
|
||||
| `hf` | a one-pole low-pass over the window, before *any* statistic | Hz corner | `0` is off |
|
||||
|
||||
The band is clamped to half the room between the split and the nearer extreme, so a 50 % band cannot invert
|
||||
the thresholds. `hf` is the **one** place where a readout does not quote a stored sample: with `hf > 0` the
|
||||
extremes, the mean and the levels come from a filtered copy of the window, so a ruler can sit at a level the
|
||||
drawn trace -- which is raw -- never reaches. That is what filtering *is*; it must not be "fixed" by snapping
|
||||
the value back to the nearest sample, and with `hf = 0` the old rule holds exactly.
|
||||
|
||||
Both cost: a filtered window disables the two bucket fast paths, because a bucket is an aggregate of *raw*
|
||||
samples and cannot be mixed into a filtered walk. A filtered window is still walked once and shared by every
|
||||
measurement that asks for it, so a pane of five measurements at two corners costs two walks, not five.
|
||||
|
||||
### Why a crossing is counted with hysteresis
|
||||
|
||||
A crossing is not read off a single level. It is read off a **band**: two thresholds a tenth of
|
||||
@@ -993,6 +1010,24 @@ modes are unchanged, and only the question "did an event happen at this sample"
|
||||
- **dropout** — the *absence* of a crossing for longer than a timeout. One event per outage:
|
||||
the condition re-arms only when a crossing returns.
|
||||
|
||||
**Noise rejection on the trigger's own input.** A *band* and a *reject* are offered beside the level.
|
||||
`band` is a share of the channel's own span either side of the level -- a second, undecided threshold, so a
|
||||
sample inside it decides nothing -- and `reject` is a one-pole low-pass corner in Hz, one state per
|
||||
configured condition, recomputed when the corner or the sample interval changes. **Off is `band = 0` and
|
||||
`reject = 0`**, which is bit for bit the trigger that was there before them.
|
||||
|
||||
They apply to **edge** and **window** and *not* to **pulse width** or **dropout**. That is a deliberate
|
||||
deviation from the design spec, which asked for every condition type: a pulse's width is measured from a
|
||||
crossing to its return, so a band there would change the *measured width* rather than reject noise (the
|
||||
documented pulse-width readings move with it), and a dropout is a measured *absence*, which a band cannot
|
||||
describe -- there is no level to compare.
|
||||
|
||||
The band and the reject are verified through `BufferSearch` hit counts rather than through
|
||||
`[condition-probe]` latch times, also deliberately: `probe-readings.md` states that a `t0` is **not** an
|
||||
assertion, being a stream stamp rebased by whichever rebuild last dropped history -- it read `1.7103`,
|
||||
`1.6975` and `1.8719` on three consecutive runs of the same leg. The counts, and what each must read, are in
|
||||
`probe-readings.md`.
|
||||
|
||||
A shape's own rows are the only ones shown; the rest are hidden, so the panel never offers a
|
||||
width to a window trigger. The condition is **not** persisted in the session file — the session
|
||||
has never persisted the trigger — so a restart comes up on an edge trigger.
|
||||
@@ -1112,9 +1147,23 @@ an *ordinary channel*: the model synthesises a signal entry for it, so an enable
|
||||
plotted, measured, exported and listed in the signal panel exactly like a streamed signal — and
|
||||
another math channel can be its source. The operations are `channel` (a source), `gain + offset`,
|
||||
`abs`, `sqrt`, `clamp`, `low pass`, `high pass`, `band pass` (biquads, cutoffs in Hz against the
|
||||
stored rate), `sum`, `difference`, `product` and `ratio`; the arithmetic ops take two inputs, which
|
||||
stored rate), `de-noise` (a running median followed by a one-pole), `sum`, `difference`, `product`
|
||||
and `ratio`; the arithmetic ops take two inputs, which
|
||||
is what makes branching — the sum of a raw and a filtered channel — expressible at all.
|
||||
|
||||
`de-noise` is **last in the dropdown on purpose**: a node's op is stored in the session as its index
|
||||
through `math_op_at()`, so an op inserted among the others would reinterpret every node in every file written
|
||||
before it.
|
||||
|
||||
**`de-noise` is one node, not a median the user chains into a low pass.** Its window is `med_n` samples, odd
|
||||
and 3..9 (the dialog offers only those, and the engine rounds to it anyway, so a hand-edited file loads), and
|
||||
an impulse shorter than half the window cannot move the median at all; the one-pole at `fc` Hz then takes the
|
||||
hash the median leaves behind, and `fc = 0` is the median alone. The order is the design: a pole *before* a
|
||||
median feeds the impulse into the filter's state for its whole settling time, which is the thing the node
|
||||
exists to avoid. It costs a per-sample insertion sort of at most nine doubles into a local, on the producer
|
||||
thread where every other op runs -- `math cost:` reads **103 ns/slot** with a de-noise channel in the probe's
|
||||
three graphs.
|
||||
|
||||
**A gap is a gap in a filtered trace, never the end of it.** A hole the resequencer reserved is
|
||||
NaN in the source, and the filters deliberately do not integrate it: the hole shows as a hole in
|
||||
the filtered trace, the filter's history stays the last real samples it saw, and the trace picks
|
||||
|
||||
Reference in new issue
Block a user