feat: a measurement's band is a setting and its walks can reject HF

`MeasureOptions` gains `noise_pct` (the hysteresis band, % of the window's peak-to-peak,
defaulting to the 10 % the file-scope constant held) and `hf_fc` (a one-pole HF reject over
the measured window). The band's measured rationale moves with the number rather than
becoming an orphan comment, and `measure_set_level()` takes the fraction as an argument
because the band is a per-measurement setting while the call is once per level.

`MeasureFilter` is that reject as a walk carries it: one pole, one state word, `a == 1` a
pass-through so an unfiltered walk pays a multiply and no branch, and a non-finite sample
passing through without touching the state. Both raw walks step it -- and stepping it is why
`measure_accumulate()` now loops samples-outer/levels-inner: a levels-outer loop would step
one filter state once per level per sample, and every level after the first would read a
signal filtered k times over. The inner `continue` is a `continue` of the *level* loop, which
is what it has to be: a sample inside one level's band may be outside another's.

`measure_uses_noise()` names the three types the band reaches (the crossing types -- a band
moves the edge decision and nothing else), and both bucket functions say in their comments
that a bucket aggregates unfiltered samples, so a walk with a reject must not use them.

This leaves `ui_measure.cpp`'s five call sites failing to compile, deliberately: they are the
next task, and pretending otherwise would mean a shim that the task then deletes.
This commit is contained in:
Martino Ferrari committed 2026-10-13 15:36:16 +02:00
1 parent fec0a1032e
commit 2730707372
2 files changed
+94 -19

No files matched your search

+33 -16
View File
@@ -28,16 +28,13 @@ void enter_side(MeasureLevel& acc, std::size_t e, double t) {
acc.open[e] = true;
}
/// The hysteresis band's half-width, as a share of the window's peak-to-peak. Measured rather than
/// chosen: 5 % reads the mock's 0.1 interferer correctly and fails at 0.2 (`period=0.075` for a
/// true 0.2), while 10 % holds to an interferer amplitude of 0.3 -- a third of the waveform -- and
/// to white noise at sigma=0.05. Above that the band starts eating a real waveform's own extremes
/// on anything less square than a sine.
constexpr double kHysteresisFrac = 0.10;
// The hysteresis band and its rationale moved to `MeasureOptions::noise_pct`: it is a per-measurement
// setting now, and a second copy of the number here would be a second thing to keep in step.
constexpr double kPi = 3.14159265358979323846;
} // namespace
void measure_set_level(MeasureLevel& acc, const MeasureStats& s, double pct) {
void measure_set_level(MeasureLevel& acc, const MeasureStats& s, double pct, double noise_frac) {
const double span = s.hi - s.lo;
acc.level = s.lo + pct * span;
// Half the room to the nearer extreme is the clamp. A threshold at 90 % has a tenth of the span
@@ -46,10 +43,21 @@ void measure_set_level(MeasureLevel& acc, const MeasureStats& s, double pct) {
// a noisy reading. Measured on a 3.3 V logic signal with ringing at thr=90 %: `enters=0` with a
// fixed band, `enters=4 period=0.200000` with the clamp.
const double room = std::min(s.hi - acc.level, acc.level - s.lo);
acc.hysteresis = std::max(0.0, std::min(kHysteresisFrac * span, 0.5 * room));
const double want = noise_frac > 0.0 ? noise_frac * span : 0.0;
acc.hysteresis = std::max(0.0, std::min(want, 0.5 * room));
}
void measure_scan(const MeasureChunk& chunk, MeasureStats& s) {
void measure_filter_set(MeasureFilter& f, double fc, double dt) {
f.reset();
f.a = 1.0;
if (fc > 0.0 && dt > 0.0) {
const double nyquist = 0.5 / dt;
const double use = fc < nyquist * 0.99 ? fc : nyquist * 0.99;
f.a = 1.0 - std::exp(-2.0 * kPi * use * dt);
}
}
void measure_scan(const MeasureChunk& chunk, MeasureStats& s, MeasureFilter& filter) {
// The chunk's own extremes first, in a loop that touches nothing but the values: *where* an
// extreme is costs an index, and the caller turns the two that survive into stamps once at the
// end. The sum is accumulated per chunk and folded in once, which is also kinder numerically
@@ -62,7 +70,7 @@ void measure_scan(const MeasureChunk& chunk, MeasureStats& s) {
std::size_t at_hi = 0;
bool any = false;
for (std::size_t i = 0; i < chunk.count; ++i) {
const double v = static_cast<double>(chunk.value[i]);
const double v = filter.step(static_cast<double>(chunk.value[i]));
if (!std::isfinite(v)) continue;
if (!any) {
lo = hi = v;
@@ -143,12 +151,16 @@ void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, Measu
void measure_accumulate(const MeasureChunk& chunk,
const std::function<double(std::size_t)>& time_of, MeasureLevel* levels,
std::size_t level_count) {
for (std::size_t k = 0; k < level_count; ++k) {
MeasureLevel& acc = levels[k];
for (std::size_t i = 0; i < chunk.count; ++i) {
const double v = static_cast<double>(chunk.value[i]);
if (!std::isfinite(v)) continue;
std::size_t level_count, MeasureFilter& filter) {
// Samples outer, levels inner -- the reverse of what reads more naturally, and deliberately. The filter
// holds one state for the whole walk, so a levels-outer loop would step it once per level per sample and
// every level after the first would read a signal filtered `k` times over. Each level's own state is
// independent of the others, so the swap costs nothing else.
for (std::size_t i = 0; i < chunk.count; ++i) {
const double v = filter.step(static_cast<double>(chunk.value[i]));
if (!std::isfinite(v)) continue;
for (std::size_t k = 0; k < level_count; ++k) {
MeasureLevel& acc = levels[k];
// The *sums* split at the bare level: `Top` and `Bottom` are defined as the mean above and
// below the threshold, and a band here would move a documented exact reading.
const int s = v > acc.level ? 1 : 0;
@@ -278,6 +290,11 @@ bool measure_uses_threshold(MeasureType type) {
bool measure_uses_side(MeasureType type) { return type == MeasureType::PulseDuration; }
bool measure_uses_noise(MeasureType type) {
return type == MeasureType::PulseDuration || type == MeasureType::Period ||
type == MeasureType::Frequency;
}
bool measure_can_show_on_plot(MeasureType) { return true; }
MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level, MeasureType type,