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:
1 parent
fec0a1032e
commit
2730707372
2 files changed
+94
-19
No files matched your search
+33
-16
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user