fixed toolbar order and included math/measuraments files
This commit is contained in:
1 parent
aa2508e431
commit
714d042809
4 files changed
+677
No files matched your search
+228
@@ -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
|
||||
Reference in new issue
Block a user