measure: top/bottom/amplitude read the flat levels, not the extremes
A bench scope separates Vtop/Vbase from Vmax/Vmin because a band-limited edge overshoots and rings: the mock's `square` at 30 harmonics reaches ±1.179 where its plateau is ±1. On the rippled square the old readings were a property of the edges rather than of the waveform -- `top` 0.986496, `amplitude` 1.179350 -- and `amplitude` reported the overshoot. `top`, `bottom` and `amplitude` are now the flat levels and their delta. A flat level is a density question -- the plateau holds most of the samples and the ringing a few percent -- so `measure_scan_buckets()` folds a 64-bin histogram of the whole buckets' midpoints while it has the list in hand (one observation per bucket, each bin carrying its buckets' exact sum and finite count), and `measure_flat_levels()` takes, either side of the split, the densest bin holding at least half the densest one's buckets -- the outermost of those, which is what makes a waveform with no plateau read its peak -- and the exact mean of the samples of the buckets within 5 % of the window's peak-to-peak of it. 5 % is deliberately under the ~9 %-of-swing Gibbs overshoot. `amplitude` is `top - bottom` (a scope's Vamplitude), not the half-swing it was. Measured, fast mock, 2 s window, harmonics 0 -> 30: top 1.000000 -> 0.999983, bottom -1.000000 -> -0.999983, amplitude 2.000000 -> 1.999966, while `peak to peak` keeps the extremes (2.000000 -> 2.358701). On the 1 Hz sine top is 0.957988 (it was 0.636620, the half-wave mean; a sine has no plateau, so the densest band is at its peak) with sd 0.0014 over a scrolling window, and `peak to peak` is still exactly 2. No raw pass and no new fetch: the histogram is the same fetched bucket list read a second time, gated on a pane that holds one of the three types. `raw` is unchanged (0.1% clean, 2.1% with the --noise 0.1 interferer, against 0.1%/2.1% before) for 0.44 -> 0.50 ms and 0.52 -> 0.65 ms a pass. A window the pyramid cannot summarize -- an hf-filtered walk, or one shorter than a bucket -- keeps the old readings: the mean of each side of the split, and the window's peak to peak for amplitude. Docs: the flat-level invariant (and the two 2/pi assertions it replaces), the probe readings, the manual's measurements page and the README.
This commit is contained in:
1 parent
afd2a6cdc1
commit
e83ad86b8f
10 files changed
+495
-80
No files matched your search
+159
-11
@@ -32,6 +32,23 @@ void enter_side(MeasureLevel& acc, std::size_t e, double t) {
|
||||
// setting now, and a second copy of the number here would be a second thing to keep in step.
|
||||
constexpr double kPi = 3.14159265358979323846;
|
||||
|
||||
/// The flat level's band, as a half-width in shares of the window's peak-to-peak. 5 % is *under*
|
||||
/// the overshoot a band-limited edge rings to -- Gibbs, about 9 % of the swing at the harmonic
|
||||
/// counts the mock's `square` uses -- so the ringing's excursion falls outside the densest band
|
||||
/// while a plateau's own ripple, a few percent, stays inside it. Measured on the mock's 30-harmonic
|
||||
/// square at 1.3 MS/s: the flat top reads 1.000000 with this band, 1.003573 at 3 %, 1.003495 at
|
||||
/// 10 % -- the wider bands reach the ringing again. A share and not an absolute width: the band is
|
||||
/// the same quantity `threshold_pct` and the hysteresis are measured in.
|
||||
constexpr double kFlatBandShare = 0.05;
|
||||
|
||||
/// Which bin of a side is the level: the *outermost* one holding at least this share of the
|
||||
/// densest bin's buckets. One rule for both kinds of waveform. Where a plateau exists it is by
|
||||
/// far the densest band and the rule cannot reach past it. Where none exists -- a sine, a ramp --
|
||||
/// the signal dwells in every band it visits, the whole side qualifies, and the outermost is the
|
||||
/// honest answer: the level is then the top of what the window reaches, near the peak, rather
|
||||
/// than a bin of an arbitrary mid-range density that a shifted window would move.
|
||||
constexpr double kFlatTieShare = 0.5;
|
||||
|
||||
} // namespace
|
||||
|
||||
void measure_set_level(MeasureLevel& acc, const MeasureStats& s, double pct, double noise_frac) {
|
||||
@@ -116,14 +133,29 @@ void measure_scan(const MeasureChunk& chunk, MeasureStats& s, MeasureFilter& fil
|
||||
}
|
||||
}
|
||||
|
||||
void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, MeasureStats& s) {
|
||||
void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, MeasureStats& s,
|
||||
bool flat_hist) {
|
||||
double sum = 0.0;
|
||||
std::size_t finite = 0;
|
||||
// The *buckets'* own range, which is not the window's: the window's partial edge buckets are
|
||||
// read raw and are not in this list. Only the non-empty buckets: an all-non-finite bucket is
|
||||
// stored as an empty aggregate and its `lo`/`hi` say nothing.
|
||||
double blo = 0.0;
|
||||
double bhi = 0.0;
|
||||
bool any = false;
|
||||
for (std::size_t b = 0; b < count; ++b) {
|
||||
const MeasureBucket& k = buckets[b];
|
||||
// A bucket of nothing (every sample non-finite) is stored as an empty aggregate, and the raw
|
||||
// walk skips exactly those samples -- so it contributes nothing here either.
|
||||
if (k.count == 0) continue;
|
||||
if (!any) {
|
||||
blo = k.lo;
|
||||
bhi = k.hi;
|
||||
any = true;
|
||||
} else {
|
||||
if (k.lo < blo) blo = k.lo;
|
||||
if (k.hi > bhi) bhi = k.hi;
|
||||
}
|
||||
sum += k.sum;
|
||||
finite += k.count;
|
||||
if (!s.valid) {
|
||||
@@ -147,6 +179,27 @@ void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, Measu
|
||||
}
|
||||
s.sum += sum;
|
||||
s.count += finite;
|
||||
if (!flat_hist || !any || !(bhi > blo)) return;
|
||||
// One observation per bucket, at its midpoint, carrying that bucket's *exact* sum and finite
|
||||
// count -- so the level the caller finally reports is a mean of real samples and not a bin
|
||||
// centre. Weighting by the bucket (and not by its 64 samples) is what keeps the flat regions
|
||||
// and the ringing on the same footing: the question is how much of the *waveform* dwells in a
|
||||
// band, and a bucket is a span of it. A range that is degenerate (every bucket the same value)
|
||||
// has no band to pick and leaves `folded` at 0, which is the caller's "no histogram".
|
||||
s.flat.lo = blo;
|
||||
s.flat.hi = bhi;
|
||||
const double scale = static_cast<double>(MeasureFlatHist::kBins) / (bhi - blo);
|
||||
for (std::size_t b = 0; b < count; ++b) {
|
||||
const MeasureBucket& k = buckets[b];
|
||||
if (k.count == 0) continue;
|
||||
const double mid = 0.5 * (k.lo + k.hi);
|
||||
std::size_t bin = mid <= blo ? 0u : static_cast<std::size_t>((mid - blo) * scale);
|
||||
if (bin >= MeasureFlatHist::kBins) bin = MeasureFlatHist::kBins - 1;
|
||||
s.flat.sum[bin] += k.sum;
|
||||
s.flat.count[bin] += k.count;
|
||||
++s.flat.buckets[bin];
|
||||
++s.flat.folded;
|
||||
}
|
||||
}
|
||||
|
||||
void measure_accumulate(const MeasureChunk& chunk,
|
||||
@@ -219,7 +272,9 @@ bool measure_accumulate_bucket(const MeasureBucket& k,
|
||||
const bool fully_outside = above ? (k.lo > top) : (k.hi < bot);
|
||||
// Nothing may be folded before this point: a refusal hands the bucket to `measure_accumulate()`,
|
||||
// which adds the same samples again. Sums added above the refusal double-counted every opened
|
||||
// bucket, which read as `top` 0.614304 on a unit sine against the 2/pi it must be.
|
||||
// bucket -- which read as `top` 0.614304 on a unit sine against the 2/pi a mean over a half then
|
||||
// gave, and which is now the sums `Top`/`Bottom` fall back to on a window with no histogram, so
|
||||
// a wrong one is a wrong answer waiting for that window.
|
||||
if (change && !fully_outside) return false;
|
||||
acc.sum[e] += k.sum;
|
||||
acc.count[e] += k.count;
|
||||
@@ -231,6 +286,62 @@ bool measure_accumulate_bucket(const MeasureBucket& k,
|
||||
return true;
|
||||
}
|
||||
|
||||
MeasureFlatLevels measure_flat_levels(const MeasureStats& b, double pct) {
|
||||
MeasureFlatLevels out;
|
||||
const MeasureFlatHist& h = b.flat;
|
||||
const double span = b.hi - b.lo;
|
||||
if (h.folded == 0 || !(h.hi > h.lo) || !(span > 0.0)) return out;
|
||||
// The split, sized exactly as `measure_set_level()` sizes the level the crossing types read, so
|
||||
// "the top" and "the level a period is counted about" are placed by one rule.
|
||||
const double level = b.lo + std::clamp(pct, 0.0, 1.0) * span;
|
||||
const double width = (h.hi - h.lo) / static_cast<double>(MeasureFlatHist::kBins);
|
||||
const double band = kFlatBandShare * span;
|
||||
for (int upper = 1; upper >= 0; --upper) {
|
||||
const auto in_side = [&](std::size_t i) {
|
||||
const double centre = h.lo + (static_cast<double>(i) + 0.5) * width;
|
||||
return (centre > level) == (upper != 0);
|
||||
};
|
||||
// One scan for the densest bin of this side, one for the outermost of the equally dense
|
||||
// ones, one to sum the band about it: 192 iterations of an array of 64, per flat level.
|
||||
std::size_t densest = 0;
|
||||
for (std::size_t i = 0; i < MeasureFlatHist::kBins; ++i) {
|
||||
if (in_side(i) && h.buckets[i] > densest) densest = h.buckets[i];
|
||||
}
|
||||
// A side of the split with no bucket in it has no level: `top` on a window that never reaches
|
||||
// above the split reads `--`, which is exactly what it read before.
|
||||
if (densest == 0) continue;
|
||||
const double floor_buckets = kFlatTieShare * static_cast<double>(densest);
|
||||
std::size_t anchor = 0;
|
||||
bool have_anchor = false;
|
||||
for (std::size_t i = 0; i < MeasureFlatHist::kBins; ++i) {
|
||||
if (!in_side(i) || static_cast<double>(h.buckets[i]) < floor_buckets) continue;
|
||||
// The upper side wants the *last* qualifier and the lower one the first, so a bottom-up scan
|
||||
// is "always overwrite" for one and "only ever write once" for the other.
|
||||
if (upper != 0 || !have_anchor) anchor = i;
|
||||
have_anchor = true;
|
||||
}
|
||||
if (!have_anchor) continue;
|
||||
const double centre = h.lo + (static_cast<double>(anchor) + 0.5) * width;
|
||||
double sum = 0.0;
|
||||
std::size_t count = 0;
|
||||
for (std::size_t i = 0; i < MeasureFlatHist::kBins; ++i) {
|
||||
const double offset = h.lo + (static_cast<double>(i) + 0.5) * width - centre;
|
||||
if (offset < -band || offset > band) continue;
|
||||
sum += h.sum[i];
|
||||
count += h.count[i];
|
||||
}
|
||||
if (count == 0) continue;
|
||||
if (upper != 0) {
|
||||
out.top = sum / static_cast<double>(count);
|
||||
out.top_ok = true;
|
||||
} else {
|
||||
out.bottom = sum / static_cast<double>(count);
|
||||
out.bottom_ok = true;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct) {
|
||||
switch (type) {
|
||||
case MeasureType::Top:
|
||||
@@ -323,11 +434,11 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
case MeasureType::PeakToPeak:
|
||||
case MeasureType::Amplitude: {
|
||||
// Both come straight out of the window's own extremes, which every type already needs:
|
||||
// the two rulers are the same pair, and the numbers differ by a factor of two.
|
||||
out.primary = type == MeasureType::PeakToPeak ? b.hi - b.lo : 0.5 * (b.hi - b.lo);
|
||||
case MeasureType::PeakToPeak: {
|
||||
// Straight out of the window's own extremes, which every type already needs: this one *is*
|
||||
// the extremes, ripple and overshoot included -- that is the difference between it and
|
||||
// `Amplitude`, which reads the two levels the signal dwells at instead.
|
||||
out.primary = b.hi - b.lo;
|
||||
// A ruler at each extreme: the *span* is the measurement, and two levels are how a
|
||||
// plot states a span without a band between them.
|
||||
out.marks.push_back({true, b.lo});
|
||||
@@ -335,13 +446,50 @@ MeasureResult compute_measure(const MeasureStats& b, const MeasureLevel* level,
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
case MeasureType::Amplitude: {
|
||||
// The delta between the two flat levels, so a ringing edge's overshoot is not part of it.
|
||||
// The split is the mid level, fixed rather than an option -- the dialog offers no threshold
|
||||
// for this type -- and the reading is insensitive to it anyway: the band a level is read from
|
||||
// is a *value* band, so any split between the two plateaus picks the same two.
|
||||
const MeasureFlatLevels flat = measure_flat_levels(b, 0.5);
|
||||
if (flat.top_ok && flat.bottom_ok) {
|
||||
out.primary = flat.top - flat.bottom;
|
||||
out.marks.push_back({true, flat.top});
|
||||
out.marks.push_back({true, flat.bottom});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
// No density estimate to separate a plateau from its ringing: a window shorter than a
|
||||
// bucket, or an HF-rejected walk, which cannot use the pyramid at all. The delta then
|
||||
// degenerates to the window's own peak to peak -- the same number when there is nothing to
|
||||
// exclude, and the only honest one when the ripple cannot be told from the level.
|
||||
out.primary = b.hi - b.lo;
|
||||
out.marks.push_back({true, b.lo});
|
||||
out.marks.push_back({true, b.hi});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom: {
|
||||
// The mean of one side of the level, straight out of the second pass' running sums. A flat
|
||||
// window has no range at all, so every sample lands on the "below" side -- which is why
|
||||
// `top` on a signal that never moves reads `--` rather than that one value.
|
||||
// The flat level of one side of the split: the densest band there, whose samples' exact mean
|
||||
// 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
|
||||
// whole point, because those are exactly what a mean over a side reports instead.
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(type, options, pct)) return out;
|
||||
const bool want_top = type == MeasureType::Top;
|
||||
const MeasureFlatLevels flat = measure_flat_levels(b, pct);
|
||||
if (want_top ? flat.top_ok : flat.bottom_ok) {
|
||||
out.primary = want_top ? flat.top : flat.bottom;
|
||||
out.marks.push_back({true, out.primary});
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
// 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
|
||||
// an HF-rejected walk, and on those the split's own means are the honest reading.
|
||||
if (level == nullptr) return out;
|
||||
const std::size_t s = type == MeasureType::Top ? 1 : 0;
|
||||
const std::size_t s = want_top ? 1 : 0;
|
||||
if (level->count[s] == 0) return out;
|
||||
out.primary = level->sum[s] / static_cast<double>(level->count[s]);
|
||||
out.marks.push_back({true, out.primary});
|
||||
|
||||
+90
-16
@@ -17,15 +17,24 @@ enum class MeasureType {
|
||||
Min, ///< the smallest visible value
|
||||
Max, ///< the largest visible value
|
||||
/// The largest swing in the window: `max - min`, the two extremes the eye would measure
|
||||
/// with a ruler across the waveform.
|
||||
/// with a ruler across the waveform -- ripple and overshoot included, which is what makes it
|
||||
/// the *extremes* and not a level.
|
||||
PeakToPeak,
|
||||
/// Half the peak-to-peak swing, `(max - min) / 2` -- the amplitude of a symmetric waveform
|
||||
/// about its own mean, which is the figure a datasheet quotes and a signal generator is set
|
||||
/// to. Deliberately not `max(|value|)`: that is a *distance from zero*, which for a signal
|
||||
/// riding on an offset is a different and less useful number.
|
||||
/// The delta between the two **flat** levels, `top - bottom` (a bench scope's `Vamplitude`,
|
||||
/// which is `Vtop - Vbase`). Deliberately *not* the peak-to-peak swing: a band-limited edge
|
||||
/// overshoots and rings, so the extremes of a nominal 2 V square read about 2.36 V, while the
|
||||
/// levels the signal actually dwells at are ±1 and their delta exactly 2. Where the window
|
||||
/// has no density estimate to separate a plateau from its ripple (shorter than one bucket, or
|
||||
/// an HF-rejected walk) it degenerates to the window's own peak to peak, which is the same
|
||||
/// number on a waveform with nothing to exclude. Never `max(|value|)`: that is a *distance
|
||||
/// from zero*, which for a signal riding on an offset is a different and less useful number.
|
||||
Amplitude,
|
||||
Top, ///< the signal's high level (the mean of the samples above the threshold)
|
||||
Bottom, ///< the signal's low level (the mean of the samples below it)
|
||||
/// The signal's high level: the densest band of the window's upper side, whose samples' exact
|
||||
/// mean is the value -- the *flat top*, so a plateau's ripple and an edge's overshoot neither
|
||||
/// define it nor enter it.
|
||||
Top,
|
||||
/// The signal's low level, the same way from the window's lower side.
|
||||
Bottom,
|
||||
PulseDuration, ///< how long the signal spends on one side of the threshold
|
||||
Period, ///< the time between two successive crossings of the threshold
|
||||
Frequency, ///< 1 / period
|
||||
@@ -92,6 +101,35 @@ struct MeasureResult {
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/// The density estimate behind `Top`/`Bottom`/`Amplitude`: one bin per band of the window's
|
||||
/// value range, over the whole LOD buckets pass one walked.
|
||||
///
|
||||
/// A flat level is a *density* question, not an extreme one: on a band-limited square the
|
||||
/// plateau holds most of the samples while the edge's ringing holds a few percent, so the
|
||||
/// densest band is the level the signal dwells at and the ringing is what a mean over a side
|
||||
/// reports instead. One bucket is one observation at its midpoint -- a bucket is 64 samples
|
||||
/// held as one aggregate, so counting its samples would weight a flat region by nothing at all
|
||||
/// and a ringing one by 64 -- and each bin also carries the exact sum and count of the samples
|
||||
/// its buckets hold, which is what makes the reported level an exact mean rather than a bin
|
||||
/// centre. The bucket list is the one pass one already fetches; no pass reads raw samples for
|
||||
/// this, which is the whole reason it is built from buckets and not from a sample histogram
|
||||
/// (that would be a second O(window) walk).
|
||||
struct MeasureFlatHist {
|
||||
/// Bins across the buckets' own range. The band the level is read from is picked at this
|
||||
/// resolution and is a share of the *window's* peak-to-peak, so the bin count only sets how
|
||||
/// finely the densest band can be located.
|
||||
static constexpr std::size_t kBins = 64;
|
||||
double lo = 0.0; ///< the bin range: the whole buckets' own smallest `lo`..
|
||||
double hi = 0.0; ///< ..and largest `hi`. Empty when nothing was folded.
|
||||
double sum[kBins] = {};
|
||||
std::size_t count[kBins] = {};
|
||||
std::size_t buckets[kBins] = {};
|
||||
/// Whole buckets folded in: 0 means this window has no histogram at all (a window shorter
|
||||
/// than a bucket, or an HF-rejected walk, which cannot use the pyramid), and the flat-level
|
||||
/// types then fall back to their split-relative readings.
|
||||
std::size_t folded = 0;
|
||||
};
|
||||
|
||||
/// The window's own extremes, which every threshold-relative type scales from and which
|
||||
/// several types report directly. Public so a caller holding *several* measurements on one
|
||||
/// signal can pay for the scan once: it is O(visible samples) and a live view at 1 MS/s makes
|
||||
@@ -119,6 +157,10 @@ struct MeasureStats {
|
||||
/// `compute_measure()` reads these, so a caller that skips the resolve reports t = 0.
|
||||
double at_lo = 0.0;
|
||||
double at_hi = 0.0;
|
||||
/// The flat levels' density estimate, folded from the same whole buckets pass one walked.
|
||||
/// The *values* are resolved by `measure_flat_levels()`, which needs the split and the
|
||||
/// window's span and so cannot be done until the walk is over.
|
||||
MeasureFlatHist flat;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
@@ -195,7 +237,14 @@ void measure_scan(const MeasureChunk& chunk, MeasureStats& stats, MeasureFilter&
|
||||
/// Pass one from whole buckets: the same figures, exactly, for 1/64 of the reads. The extreme's
|
||||
/// position comes out as a *bucket* (`lo_search`/`hi_search` non-zero), which one raw
|
||||
/// `measure_scan()` of that bucket then pins. Interleave with `measure_scan()` in window order.
|
||||
void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, MeasureStats& stats);
|
||||
///
|
||||
/// `flat_hist` folds the same list into `stats.flat` for the flat-level types, in a second tight
|
||||
/// loop over the array (the bin range is the buckets' own, which the first loop is what
|
||||
/// discovers). That loop is O(buckets) and reads no sample, so the `measure cost:` line's `raw`
|
||||
/// is unchanged by it -- the cost is buckets, which is the budget the pane is on. It is the
|
||||
/// caller's because only the caller knows whether any measurement reads a flat level.
|
||||
void measure_scan_buckets(const MeasureBucket* buckets, std::size_t count, MeasureStats& stats,
|
||||
bool flat_hist);
|
||||
|
||||
/// Everything the threshold-relative types need from a second walk, at one level: the sum and
|
||||
/// count of the samples on either side of it, and its crossings reduced to running figures -- the
|
||||
@@ -215,12 +264,17 @@ struct MeasureLevel {
|
||||
/// `period=0.025622 freq=39.029` for a true 0.2 s / 5 Hz. With the band, `period=0.200188`.
|
||||
///
|
||||
/// It moves the *edge decision* only. The `sum`/`count` split below stays at the bare `level`,
|
||||
/// because that is what `Top`/`Bottom` are -- the mean of the samples above/below the threshold --
|
||||
/// and a band there would change a documented exact reading (`top` on a unit sine is 2/pi).
|
||||
/// `measure_set_level()` is what sizes it; zero reduces the walk exactly to a single level.
|
||||
/// so a band can never change it -- the split is the split, and a sample's side of it is a
|
||||
/// fact rather than a threshold decision. `measure_set_level()` is what sizes it; zero reduces
|
||||
/// the walk exactly to a single level.
|
||||
double hysteresis = 0.0;
|
||||
/// Index 0 is the side below the level (a sample exactly *at* it counts here, which is what
|
||||
/// `bottom` has always reported), index 1 the side above it.
|
||||
/// The samples either side of `level`, exactly: `Top`/`Bottom`'s reading on a window with no
|
||||
/// bucket histogram to read a flat level from (shorter than one bucket, or an HF-rejected
|
||||
/// walk). They are folded on every window -- pass two walks it for the 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
|
||||
/// side above it.
|
||||
double sum[2] = {0.0, 0.0};
|
||||
std::size_t count[2] = {0, 0};
|
||||
double first_enter[2] = {0.0, 0.0};
|
||||
@@ -275,9 +329,29 @@ bool measure_accumulate_bucket(const MeasureBucket& bucket,
|
||||
/// The level a type reads, as a share of the window's own min..max, or false when it reads none
|
||||
/// (the pure statistics, answered from `MeasureStats` alone). `Top`/`Bottom` read the
|
||||
/// measurement's own threshold; the crossing types read the mid level, deliberately -- see
|
||||
/// `measure_uses_threshold()`.
|
||||
/// `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
|
||||
/// fallback it takes from the window's own extremes -- see `compute_measure()`.
|
||||
bool measure_level_pct(MeasureType type, const MeasureOptions& options, double& pct);
|
||||
|
||||
/// 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
|
||||
/// crossing type's mid level that is the two plateaus of a pulse train, and for `Top`/`Bottom`'s
|
||||
/// own threshold it is "which side is the level I want". Each side is false when it holds no
|
||||
/// bucket, and both are false when the window has no histogram at all.
|
||||
///
|
||||
/// Two things this does not do. It does not read a sample: every figure comes from buckets pass
|
||||
/// one already fetched. And it does not read the window raw, which is why the answer is
|
||||
/// independent of how the window happens to be aligned -- the band is a *value* band, so the
|
||||
/// level it reports does not move when the window does.
|
||||
struct MeasureFlatLevels {
|
||||
double top = 0.0;
|
||||
bool top_ok = false;
|
||||
double bottom = 0.0;
|
||||
bool bottom_ok = false;
|
||||
};
|
||||
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
|
||||
/// accumulator for `measure_level_pct(type, options)` and may be null for the types that read
|
||||
/// none. The function reads nothing else, which is what keeps the answer independent of the plot.
|
||||
@@ -298,8 +372,8 @@ bool measure_uses_threshold(MeasureType type);
|
||||
/// the waveform is the pulse).
|
||||
bool measure_uses_side(MeasureType type);
|
||||
/// Whether a type reads the hysteresis band, so the dialog can show the field only then. The band moves
|
||||
/// the *edge* decision and nothing else, so it is exactly the crossing types: `Top`/`Bottom` split their
|
||||
/// sums at the bare level and read the same answer at any band.
|
||||
/// the *edge* decision and nothing else, so it is exactly the crossing types: `Top`/`Bottom` read a flat
|
||||
/// level out of the bucket histogram, which no band touches.
|
||||
bool measure_uses_noise(MeasureType type);
|
||||
/// Whether a type draws its result on the plot when asked.
|
||||
bool measure_can_show_on_plot(MeasureType type);
|
||||
|
||||
@@ -506,6 +506,10 @@ struct App {
|
||||
std::vector<MeasureLevel> level; ///< one accumulator per entry of `pct`
|
||||
MeasureFilter scan_filter; ///< pass one's state
|
||||
MeasureFilter acc_filter; ///< pass two's; a separate walk, so a separate state
|
||||
/// Some measurement on this window reads a flat level, so pass one folds the bucket
|
||||
/// histogram the flat-level types are answered from. Only then: a pane of means and extremes
|
||||
/// should not pay for a density estimate it never reads.
|
||||
bool flat_wanted = false;
|
||||
bool ok = false;
|
||||
};
|
||||
std::vector<MeasureWindow> measure_samples;
|
||||
|
||||
+26
-3
@@ -118,15 +118,24 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
"show on plot (ruler at the value, marker where it is)");
|
||||
break;
|
||||
case MeasureType::PeakToPeak:
|
||||
case MeasureType::Amplitude:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
|
||||
gtk_label_set_text(GTK_LABEL(d->side_label), "");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
|
||||
"show on plot (rulers at both extremes)");
|
||||
break;
|
||||
case MeasureType::Amplitude:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "");
|
||||
gtk_label_set_text(GTK_LABEL(d->side_label), "");
|
||||
// Amplitude is the delta between the two *flat* levels, so its rulers are those two and not
|
||||
// the extremes a peak-to-peak draws: the pair of rulers is the reading.
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show),
|
||||
"show on plot (rulers at both flat levels)");
|
||||
break;
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
||||
// The split picks the *side*: the level is the densest band either above or below it, so any
|
||||
// split between the two plateaus reads the same -- it is not a mean over the side any more.
|
||||
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)");
|
||||
break;
|
||||
case MeasureType::PulseDuration:
|
||||
@@ -320,6 +329,12 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
}
|
||||
want_window[i] = static_cast<int>(wi);
|
||||
App::MeasureWindow& w = app.measure_samples[wi];
|
||||
// The three types whose value is a flat level: pass one folds the bucket histogram for this
|
||||
// window only if one of them is in the pane.
|
||||
if (m.type == MeasureType::Top || m.type == MeasureType::Bottom ||
|
||||
m.type == MeasureType::Amplitude) {
|
||||
w.flat_wanted = true;
|
||||
}
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(m.type, m.options, pct)) continue;
|
||||
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
||||
@@ -348,6 +363,12 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
// for the samples produced since this frame's `acquire_all()`, and for those straddling
|
||||
// buckets; a window shorter than a bucket, or a signal noisy enough to straddle everywhere,
|
||||
// degrades back to the O(window) cost and nothing else changes.
|
||||
//
|
||||
// The flat-level types (`top`/`bottom`/`amplitude`) are the one thing that reads a *third* time
|
||||
// -- and it is the same fetched bucket list, not a third fetch: pass one's bucket walk folds its
|
||||
// own histogram while it has the buckets in hand (see `measure_scan_buckets`). It is O(buckets)
|
||||
// and it reads no sample, so `raw` does not move; a pane with none of those three types does
|
||||
// not pay for it at all.
|
||||
constexpr std::size_t kChunk = 8192;
|
||||
std::vector<MeasureBucket>& buckets = app.measure_bucket_scratch;
|
||||
for (App::MeasureWindow& w : app.measure_samples) {
|
||||
@@ -385,7 +406,9 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
|
||||
const auto scan = [&](const MeasureChunk& c) { measure_scan(c, w.stats, w.scan_filter); };
|
||||
raw(0, bulk_from, scan);
|
||||
if (!buckets.empty()) measure_scan_buckets(buckets.data(), buckets.size(), w.stats);
|
||||
if (!buckets.empty()) {
|
||||
measure_scan_buckets(buckets.data(), buckets.size(), w.stats, w.flat_wanted);
|
||||
}
|
||||
raw(bulk_to, n, scan);
|
||||
// A bucket named the 64 samples that hold an extreme, not the sample. One scan of that bucket
|
||||
// pins it -- two buckets at most, whatever the window's size.
|
||||
|
||||
+2
-1
@@ -1176,7 +1176,8 @@ gboolean measure_probe(gpointer user) {
|
||||
// The signal this probe measures is enabled *here*, for the same reason the noise probe enables its own:
|
||||
// a probe must not depend on another probe's side effects. `enable_all_probe` ends by switching a signal
|
||||
// off (it reads what a disabled signal does to the plots), and with `sine` gone the readings below were
|
||||
// taken on `square` -- where `top` is 1.0, not the 2/pi the documented baseline is.
|
||||
// taken on `square` -- where `top` is its plateau's 1.0 rather than the sine's flat level, and the numbers
|
||||
// the docs quote are the sine's.
|
||||
{
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
|
||||
Reference in new issue
Block a user