fixed toolbar order and included math/measuraments files

This commit is contained in:
Martino Ferrari committed 2026-10-03 17:39:29 +02:00
1 parent aa2508e431
commit 714d042809
4 files changed
+677

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "measure.hpp"
#include <algorithm>
#include <cmath>
namespace udpscope {
MeasureStats measure_stats(const std::vector<double>& time, const std::vector<double>& value) {
MeasureStats s;
const std::size_t n = std::min(time.size(), value.size());
for (std::size_t i = 0; i < n; ++i) {
const double v = value[i];
if (!std::isfinite(v)) continue;
if (!s.valid) {
s.lo = s.hi = v;
s.at_lo = s.at_hi = time[i];
s.valid = true;
} else {
if (v < s.lo) {
s.lo = v;
s.at_lo = time[i];
}
if (v > s.hi) {
s.hi = v;
s.at_hi = time[i];
}
}
s.sum += v;
++s.count;
}
return s;
}
const char* measure_type_name(MeasureType type) {
switch (type) {
case MeasureType::Average: return "average";
case MeasureType::Min: return "min";
case MeasureType::Max: return "max";
case MeasureType::PeakToPeak: return "peak to peak";
case MeasureType::Amplitude: return "amplitude";
case MeasureType::Top: return "top";
case MeasureType::Bottom: return "bottom";
case MeasureType::PulseDuration: return "pulse duration";
case MeasureType::Period: return "period";
case MeasureType::Frequency: return "frequency";
}
return "?";
}
int measure_type_count() { return 10; }
MeasureType measure_type_at(int index) {
switch (index) {
case 0: return MeasureType::Average;
case 1: return MeasureType::Top;
case 2: return MeasureType::Bottom;
case 3: return MeasureType::Max;
case 4: return MeasureType::Min;
case 5: return MeasureType::PeakToPeak;
case 6: return MeasureType::Amplitude;
case 7: return MeasureType::PulseDuration;
case 8: return MeasureType::Frequency;
default: return MeasureType::Period;
}
}
bool measure_uses_threshold(MeasureType type) {
return type == MeasureType::Top || type == MeasureType::Bottom ||
type == MeasureType::PulseDuration;
}
bool measure_uses_side(MeasureType type) { return type == MeasureType::PulseDuration; }
bool measure_can_show_on_plot(MeasureType) { return true; }
MeasureResult compute_measure(const std::vector<double>& time, const std::vector<double>& value,
MeasureType type, const MeasureOptions& options,
const MeasureStats* stats) {
MeasureResult out;
const std::size_t n = std::min(time.size(), value.size());
if (n == 0) return out;
// A threshold as a share of the window's own range, which is what makes one percentage
// travel across signals: a threshold in volts would have to be typed for each. The bounds
// come from the caller when it has them (several measurements on one signal), because the
// scan is O(visible samples) and this runs per frame while a live window scrolls.
const MeasureStats b = stats != nullptr ? *stats : measure_stats(time, value);
if (!b.valid) return out;
// A threshold as a share of the window's own range. A flat window has no range at all, so
// the level is the signal's one value -- every sample is on neither side of it (nothing is
// strictly above or below), and the crossing types then find no edge, which is the honest
// answer for a signal that never moves.
const double pct = std::clamp(options.threshold_pct, 0.0, 100.0) / 100.0;
const double level = b.lo + pct * (b.hi - b.lo);
switch (type) {
case MeasureType::Average: {
if (b.count == 0) return out;
out.primary = b.sum / static_cast<double>(b.count);
// A mean is a level like any other, so it can be shown as one ruler.
out.marks.push_back({true, out.primary});
out.valid = true;
return out;
}
case MeasureType::Min:
case MeasureType::Max: {
// A ruler at the extreme, and a marker at the time it happened: "the minimum" is a level
// and a place. The place comes from the shared scan, which takes the *first* sample to
// reach the extreme -- the one a reader's eye lands on when the extreme is a plateau.
const bool want_max = type == MeasureType::Max;
out.primary = want_max ? b.hi : b.lo;
out.marks.push_back({true, out.primary});
out.marks.push_back({false, want_max ? b.at_hi : b.at_lo});
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);
// 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});
out.marks.push_back({true, b.hi});
out.valid = true;
return out;
}
case MeasureType::Top:
case MeasureType::Bottom: {
const bool want_high = type == MeasureType::Top;
double sum = 0.0;
std::size_t used = 0;
for (std::size_t i = 0; i < n; ++i) {
if (!std::isfinite(value[i])) continue;
if ((value[i] > level) != want_high) continue;
sum += value[i];
++used;
}
if (used == 0) return out;
out.primary = sum / static_cast<double>(used);
out.marks.push_back({true, out.primary});
out.valid = true;
return out;
}
case MeasureType::PulseDuration:
case MeasureType::Period:
case MeasureType::Frequency: {
// No options here on purpose: a period is the spacing of *rising* crossings of the
// *mid* level -- one edge per cycle -- and a threshold or a side to choose from would
// only let the user make the count wrong (a threshold near either extreme counts noise
// as crossings; counting both edges halves the period).
const double period_level = b.lo + 0.5 * (b.hi - b.lo);
// Every strict side change is an edge. "Strict" is what keeps a signal that merely
// touches the level from counting each sample as a crossing: a sample equal to the
// level is on neither side, so the walk holds the last decided side until one is.
struct Edge {
double t;
bool rising;
};
std::vector<Edge> edges;
bool have_side = false;
bool side = false;
for (std::size_t i = 0; i < n; ++i) {
const double v = value[i];
if (!std::isfinite(v) || v == period_level) continue;
const bool cur = v > period_level;
if (have_side && cur != side) edges.push_back({time[i], cur});
side = cur;
have_side = true;
}
if (edges.size() < 2) return out;
// The pulse is the `above` side: its widths run from the edge that enters it to the
// edge that leaves it, and a period is the spacing of the edges that enter it -- one
// per cycle, whatever else the waveform does in between.
std::vector<double> starts;
std::vector<double> widths;
for (std::size_t i = 0; i + 1 < edges.size(); ++i) {
if (edges[i].rising != options.above) continue;
starts.push_back(edges[i].t);
if (edges[i + 1].rising == options.above) continue;
widths.push_back(edges[i + 1].t - edges[i].t);
}
if (type == MeasureType::PulseDuration) {
if (widths.empty()) return out;
double sum = 0.0;
for (const double w : widths) sum += w;
out.primary = sum / static_cast<double>(widths.size());
// Duty as width/period, not as pulse-time over the window: a window that starts or
// ends mid-pulse holds partial cycles, and counting only the complete ones against
// the whole span would report a duty that depends on where the window happened to
// begin. The period is the spacing of the pulse's own enter-edges.
double period = 0.0;
if (starts.size() >= 2) {
period = (starts.back() - starts.front()) / static_cast<double>(starts.size() - 1);
}
// The duty needs a period, which needs two pulses; a window holding one reports the
// width alone rather than a duty computed against a span that is not a cycle.
out.secondary = period > 0.0 ? out.primary / period : 0.0;
out.has_secondary = period > 0.0;
if (!starts.empty()) {
out.marks.push_back({false, starts.front()});
out.marks.push_back({false, starts.front() + out.primary});
}
out.valid = true;
return out;
}
if (starts.size() < 2) return out;
const double span = starts.back() - starts.front();
const double period = span / static_cast<double>(starts.size() - 1);
if (!(period > 0.0)) return out;
out.primary = type == MeasureType::Period ? period : 1.0 / period;
out.marks.push_back({false, starts.front()});
out.marks.push_back({false, starts.front() + period});
out.valid = true;
return out;
}
}
return out;
}
} // namespace udpscope