measure: the split's own means are back as above/below/between
`top`, `bottom` and `amplitude` became the *flat levels* in e83ad86 -- the level the
signal dwells at, read from the density of the window's buckets -- and that change kept
the split's own two means as its internal fallback for a window the pyramid cannot
summarize. Those means are now three types of their own, so the two different questions
about one split can be asked side by side: `above %` and `below %` are the plain mean of
the samples on one side of a share -- the reading the old `top`/`bottom` gave -- and
`between % %` is the mean of the band between two shares, which no single split can
answer, because a band's two edges are two thresholds.
Nothing about the arithmetic is new, and nothing is a new pass. `Above`/`Below` are the
`sum[]`/`count[]` split `measure_accumulate()` already folds -- the very figures
`Top`/`Bottom` fall back to -- and `Between` is the **difference of two levels' upper
sides**, both of which come out of one pass two over one window. It rides the bucket
fast path unchanged: a bucket wholly inside the band is strictly above the lower level
and strictly below the upper one, so both accumulators fold it whole, and only the
buckets that straddle either edge are opened. `measure_level_pcts()` is the one place the
shares are read, and it clamps both and puts `between`'s pair **in order**, so "between
80 % and 20 %" is the band between 20 % and 80 % -- normalized rather than refused,
because the shares name two edges rather than a direction. Nothing to measure reads `--`,
as for every other type: a share at 100 % has no sample above it, and a band can hold
none at all.
Measured, `[bandavg-probe]`, fast mock at a 2 s window (`--rate 1300000 --batch 1000
--freq 1`):
sine `above 50` 0.636620 (2/pi), `below 50` -0.636620, `between 50 75` 0.255873
square `above 50` 0.986496 (30 harmonics) / 1.000000 (--square-harmonics 0),
`between 50 75` 0.302744 / --
ramp `between 50 75` 0.250000 and `above 50` 0.500000, `above 100` --
The first three are the figures the old `top`/`bottom` documented; the ramp's are what a
uniform density predicts (a band's mean is its own centre); the ideal square's empty band
is the nothing-to-measure rule. The pane offers the types and edits their percentages
through the dialog's second share -- which only `between` shows -- and a measurement's
pair survives add/edit/delete and the session file (`threshold2`), where a file written
before it existed loads at the default.
No raw pass: with the measurements probe adding one of every type at a 2 s window, `raw`
went from 113966 of 240719031 covered (0.0%) to 134978 of 255372033 (0.1%), 0.8 -> 1.1
ns a covered sample.
Docs: the band-average invariant (and the two assertions the flat-level change moved),
the probe readings for the new probe, the verification recipe, the manual's measurements
page and the README.
This commit is contained in:
1 parent
e83ad86b8f
commit
22e7a8efcc
14 files changed
+769
-98
No files matched your search
+88
-14
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
@@ -21,6 +22,35 @@ plot::u32 measure_color(std::size_t index) {
|
||||
return kPalette[index % (sizeof(kPalette) / sizeof(kPalette[0]))];
|
||||
}
|
||||
|
||||
/// One line naming what a measurement's type measures, with the shares it reads them at. The pane's
|
||||
/// row shows the type alone, as it does for every type, so this is the only place a reader sees
|
||||
/// *which* side of *which* split an `above`/`below` is averaging over, or how wide a `between`
|
||||
/// band is -- and the reading of those three is entirely a function of those two numbers.
|
||||
std::string measure_row_summary(const Measure& m) {
|
||||
const auto pct_text = [](double v) {
|
||||
char text[32];
|
||||
std::snprintf(text, sizeof(text), "%g%% of the range", v);
|
||||
return std::string(text);
|
||||
};
|
||||
switch (m.type) {
|
||||
case MeasureType::Above:
|
||||
return "the mean of the samples above " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::Below:
|
||||
return "the mean of the samples below " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::Between:
|
||||
return "the mean of the samples between " + pct_text(m.options.threshold_pct) + " and " +
|
||||
pct_text(m.options.threshold_pct2);
|
||||
case MeasureType::Top:
|
||||
case MeasureType::Bottom:
|
||||
return "the flat level on one side of the split at " + pct_text(m.options.threshold_pct);
|
||||
case MeasureType::PulseDuration:
|
||||
return std::string(m.options.above ? "the pulse above " : "the pulse below ") +
|
||||
pct_text(m.options.threshold_pct);
|
||||
default:
|
||||
return std::string("the ") + measure_type_name(m.type) + " of this window";
|
||||
}
|
||||
}
|
||||
|
||||
/// The channel a measurement's signal resolved to, or -1 while the stream does not carry it.
|
||||
int measure_channel(App& app, const std::string& signal) {
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
@@ -94,6 +124,8 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
const bool side = measure_uses_side(type);
|
||||
gtk_widget_set_visible(d->threshold_row, threshold);
|
||||
gtk_widget_set_visible(d->side_row, side);
|
||||
// The band's second share: a `Between` is the one type whose two edges are *both* parameters.
|
||||
gtk_widget_set_visible(d->band_row, measure_uses_band(type));
|
||||
// The band moves the edge decision, so it is shown for exactly the crossing types. The HF reject sits
|
||||
// before both walks, so it is shown for all of them.
|
||||
gtk_widget_set_visible(d->noise_row, measure_uses_noise(type));
|
||||
@@ -138,6 +170,24 @@ void measure_dialog_sync(MeasureDialog* d) {
|
||||
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::Above:
|
||||
case MeasureType::Below: {
|
||||
// These *are* a mean over one side of the split -- the reading the old `top`/`bottom` gave --
|
||||
// so the words have to say which side of which share, and they have to say it beside the
|
||||
// field rather than only in the docs: the same spin means a different side for each type.
|
||||
const bool above = type == MeasureType::Above;
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), above ? "above % of the range"
|
||||
: "below % of the range");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||
break;
|
||||
}
|
||||
case MeasureType::Between:
|
||||
// Two shares, so two rows: the band's edges, lower first (the engine orders a reversed pair
|
||||
// rather than refusing it, so either order the user types reads the same band).
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "from % of the range");
|
||||
gtk_label_set_text(GTK_LABEL(d->band_label), "to % of the range");
|
||||
gtk_check_button_set_label(GTK_CHECK_BUTTON(d->show), "show on plot (ruler at the mean)");
|
||||
break;
|
||||
case MeasureType::PulseDuration:
|
||||
gtk_label_set_text(GTK_LABEL(d->threshold_label), "split % of the range");
|
||||
gtk_label_set_text(GTK_LABEL(d->side_label), "pulse above the level");
|
||||
@@ -177,6 +227,7 @@ void on_measure_dialog_apply(GtkButton*, gpointer user) {
|
||||
m.signal = d->signals[sig];
|
||||
m.type = measure_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(d->type_drop)));
|
||||
m.options.threshold_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->threshold));
|
||||
m.options.threshold_pct2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->band));
|
||||
m.options.above = gtk_switch_get_active(GTK_SWITCH(d->above));
|
||||
m.options.show_on_plot = gtk_check_button_get_active(GTK_CHECK_BUTTON(d->show));
|
||||
m.options.noise_pct = gtk_spin_button_get_value(GTK_SPIN_BUTTON(d->noise));
|
||||
@@ -308,6 +359,10 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
std::vector<int> chan(app.measures.size(), -1);
|
||||
std::vector<int> want_window(app.measures.size(), -1);
|
||||
std::vector<int> want_level(app.measures.size(), -1);
|
||||
// `Between` reads two levels, and they are two ordinary entries of the same list: one walk answers
|
||||
// every level in the pane, so a band costs the same pass two as any other measurement plus one
|
||||
// more accumulator in it.
|
||||
std::vector<int> want_level2(app.measures.size(), -1);
|
||||
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
||||
const Measure& m = app.measures[i];
|
||||
const int channel = measure_channel(app, m.signal);
|
||||
@@ -335,20 +390,26 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
m.type == MeasureType::Amplitude) {
|
||||
w.flat_wanted = true;
|
||||
}
|
||||
double pct = 0.0;
|
||||
if (!measure_level_pct(m.type, m.options, pct)) continue;
|
||||
double pct[2] = {0.0, 0.0};
|
||||
const int level_count = measure_level_pcts(m.type, m.options, pct);
|
||||
// A type that does not read the band must not split the accumulator over it: its sums split at the bare
|
||||
// level, so a `Top` is the same walk whatever the band is. Grouping those at the default is what keeps
|
||||
// the common pane at exactly one level.
|
||||
const double noise =
|
||||
measure_uses_noise(m.type) ? m.options.noise_pct : MeasureOptions{}.noise_pct;
|
||||
std::size_t k = 0;
|
||||
while (k < w.pct.size() && (w.pct[k] != pct || w.noise[k] != noise)) ++k;
|
||||
if (k == w.pct.size()) {
|
||||
w.pct.push_back(pct);
|
||||
w.noise.push_back(noise);
|
||||
for (int j = 0; j < level_count; ++j) {
|
||||
std::size_t k = 0;
|
||||
while (k < w.pct.size() && (w.pct[k] != pct[j] || w.noise[k] != noise)) ++k;
|
||||
if (k == w.pct.size()) {
|
||||
w.pct.push_back(pct[j]);
|
||||
w.noise.push_back(noise);
|
||||
}
|
||||
if (j == 0) {
|
||||
want_level[i] = static_cast<int>(k);
|
||||
} else {
|
||||
want_level2[i] = static_cast<int>(k);
|
||||
}
|
||||
}
|
||||
want_level[i] = static_cast<int>(k);
|
||||
}
|
||||
|
||||
// The data work: two walks per channel, whatever the pane holds. The first fixes the window's
|
||||
@@ -483,11 +544,15 @@ void update_measures(App& app, bool force, bool accumulate) {
|
||||
continue;
|
||||
}
|
||||
const App::MeasureWindow& w = *wp;
|
||||
const MeasureLevel* level =
|
||||
want_level[i] >= 0 && static_cast<std::size_t>(want_level[i]) < w.level.size()
|
||||
? &w.level[static_cast<std::size_t>(want_level[i])]
|
||||
: nullptr;
|
||||
const MeasureResult r = compute_measure(w.stats, level, m.type, m.options);
|
||||
// In the order `measure_level_pcts()` asked for them: the band's lower edge first.
|
||||
const MeasureLevel* levels[2] = {nullptr, nullptr};
|
||||
const int want[2] = {want_level[i], want_level2[i]};
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
if (want[j] >= 0 && static_cast<std::size_t>(want[j]) < w.level.size()) {
|
||||
levels[j] = &w.level[static_cast<std::size_t>(want[j])];
|
||||
}
|
||||
}
|
||||
const MeasureResult r = compute_measure(w.stats, levels, m.type, m.options);
|
||||
app.measure_results[i] = r;
|
||||
if (r.valid) app.measure_last_valid[i] = r;
|
||||
// Time-valued types carry seconds and a frequency carries hertz whatever the signal's
|
||||
@@ -646,11 +711,20 @@ void measure_dialog_report(App& a, const char* what) {
|
||||
if (dd != nullptr) {
|
||||
// What the dialog is offering for the type on screen: the option rows' visibility and
|
||||
// the words on them, which is the per-type behaviour a user sees.
|
||||
std::snprintf(options, sizeof(options), "type=%d thr=%d side=%d show=%d '%s'",
|
||||
std::snprintf(options, sizeof(options), "type=%d thr=%d band=%d side=%d show=%d "
|
||||
"shares=%.1f/%.1f '%s'",
|
||||
static_cast<int>(gtk_drop_down_get_selected(GTK_DROP_DOWN(dd->type_drop))),
|
||||
dd->threshold_row != nullptr && gtk_widget_get_visible(dd->threshold_row) ? 1 : 0,
|
||||
dd->band_row != nullptr && gtk_widget_get_visible(dd->band_row) ? 1 : 0,
|
||||
dd->side_row != nullptr && gtk_widget_get_visible(dd->side_row) ? 1 : 0,
|
||||
dd->show != nullptr && gtk_widget_get_visible(dd->show) ? 1 : 0,
|
||||
// What the two share fields are *showing*: the pair a `between` reads, read back
|
||||
// out of the widgets, so a field that is built but not seeded or not read back is
|
||||
// visible in the line rather than only in the number it produces.
|
||||
dd->threshold != nullptr
|
||||
? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->threshold))
|
||||
: 0.0,
|
||||
dd->band != nullptr ? gtk_spin_button_get_value(GTK_SPIN_BUTTON(dd->band)) : 0.0,
|
||||
dd->show != nullptr && gtk_check_button_get_child(GTK_CHECK_BUTTON(dd->show)) !=
|
||||
nullptr
|
||||
? gtk_label_get_text(GTK_LABEL(gtk_check_button_get_child(
|
||||
|
||||
Reference in new issue
Block a user