MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
TemporalMeasures.hpp
Go to the documentation of this file.
1#pragma once
2
4
5namespace MayaFlux::Kinesis {
6
7// =============================================================================
8// Ordering and sampling convention
9//
10// Every function here takes a CHRONOLOGICAL span: values[0] earliest,
11// values[n-1] latest. This matches PathShape.hpp and is the opposite of
12// the newest-first convention in Differential.hpp. Nothing computed
13// directly in this file is order-sensitive (Estimate::trend_slope,
14// which this file calls rather than reimplements, is itself order-
15// sensitive in the same way), so the convention is carried for
16// consistency with PathShape.hpp rather than because every function
17// below individually requires it.
18//
19// dt is seconds per sample and is assumed uniform across the window.
20// Channel updates driven by device arrival are not exactly uniform, so
21// every result scaled by dt carries that approximation. Over a window
22// short enough for the measure to mean anything the jitter is small
23// against the quantity being measured, and carrying per-sample
24// timestamps through every signature to remove an error smaller than
25// the measurement is not a trade worth making.
26//
27// Mean, variance, and trend slope over a plain span already exist as
28// Stochastic::Estimate::variance and Estimate::trend_slope and are used
29// here rather than reimplemented; Estimate::trend_slope reports per
30// sample index, and this file's own functions report per second, so a
31// caller mixing the two must apply dt to Estimate's result rather than
32// assume the units already match. Discrete::mean and Discrete::variance
33// in Discrete/Analysis.hpp are a third sibling, parallelized and
34// windowed into a vector over a whole buffer; the right tool for
35// offline analysis of a long recording, not for a scalar computed once
36// per channel update, which is what everything below is shaped for.
37// =============================================================================
38
39/**
40 * @brief Mean absolute difference between consecutive samples, per second
41 * @param values Chronological span, at least two entries
42 * @param dt Seconds per sample
43 * @return Average absolute step size divided by dt
44 *
45 * Agitation independent of direction and of where the value sits. A
46 * quantity oscillating rapidly within a narrow band and one holding
47 * still have similar means and similar ranges and very different
48 * roughness, and a quantity sweeping smoothly across its whole range
49 * has a large variance and a small roughness.
50 *
51 * Distinct from two existing measures that sound related. Not
52 * Discrete::mad, which is median absolute deviation about a centre and
53 * measures spread, not step size. Not Estimate::trend_explained_ratio,
54 * which asks what fraction of a window's variance its own linear trend
55 * accounts for and answers signal against noise around that trend;
56 * roughness answers nothing about a trend and reports the same value
57 * whether the steps are around a fixed point or along a steady climb.
58 * A window can score high on both: a value climbing steadily in small
59 * jittery increments has both a high trend-explained ratio and, if the
60 * increments are large relative to the climb's own rate, high
61 * roughness.
62 */
63[[nodiscard]] inline double roughness(std::span<const double> values, double dt) noexcept
64{
65 const size_t n = values.size();
66 if (n < 2 || dt <= 0.0)
67 return 0.0;
68
69 double acc = 0.0;
70 for (size_t i = 1; i < n; ++i)
71 acc += std::abs(values[i] - values[i - 1]);
72
73 return (acc / static_cast<double>(n - 1)) / dt;
74}
75
76/**
77 * @brief Difference between the largest and smallest value in a window
78 * @param values Chronological span, non-empty
79 * @return Range, or zero for an empty span
80 *
81 * How much ground the value covered, ignoring how many times it covered
82 * it. Pairs with roughness: a large excursion with low roughness is one
83 * slow sweep, a small excursion with high roughness is a tremor, and
84 * both large is a sustained thrashing.
85 */
86[[nodiscard]] inline double excursion(std::span<const double> values) noexcept
87{
88 if (values.empty())
89 return 0.0;
90 const auto [lo, hi] = std::ranges::minmax_element(values);
91 return *hi - *lo;
92}
93
94/**
95 * @brief Seconds within a window spent at or above a threshold
96 * @param values Chronological span
97 * @param threshold Level to test against
98 * @param dt Seconds per sample
99 * @return Count of qualifying samples multiplied by dt
100 *
101 * Occupancy in time rather than in space. The scalar counterpart to
102 * SymbolicTrajectory::dwell_count, which answers the same question for
103 * a point moving through a lattice.
104 */
105[[nodiscard]] inline double time_above(
106 std::span<const double> values, double threshold, double dt) noexcept
107{
108 size_t count = 0;
109 for (const double v : values) {
110 if (v >= threshold)
111 ++count;
112 }
113 return static_cast<double>(count) * dt;
114}
115
116/**
117 * @brief Threshold crossings per second within a window
118 * @param values Chronological span, at least two entries
119 * @param threshold Level whose crossings are counted
120 * @param dt Seconds per sample
121 * @return Crossings in either direction, divided by the window duration
122 *
123 * A rate in seconds against an arbitrary level, where
124 * Discrete::zero_crossing_rate is normalized per sample and windowed
125 * into a vector. Both count the same events; this is the shape a
126 * per-update scalar measure needs.
127 *
128 * Counts crossings in both directions, so a value oscillating about the
129 * threshold reports twice the rate of its underlying cycle. Halve the
130 * result when the intended quantity is cycles rather than transitions.
131 */
132[[nodiscard]] inline double threshold_crossing_rate(
133 std::span<const double> values, double threshold, double dt) noexcept
134{
135 const size_t n = values.size();
136 if (n < 2 || dt <= 0.0)
137 return 0.0;
138
139 size_t crossings = 0;
140 for (size_t i = 1; i < n; ++i) {
141 if ((values[i] >= threshold) != (values[i - 1] >= threshold))
142 ++crossings;
143 }
144
145 const double duration = static_cast<double>(n - 1) * dt;
146 return static_cast<double>(crossings) / duration;
147}
148
149/**
150 * @struct PeriodEstimate
151 * @brief A candidate repetition period and how strongly the window
152 * actually supports it.
153 *
154 * Period alone is not a usable answer. Any window returns some lag at
155 * which its correlation happens to be highest, including a window with
156 * no repetition in it at all, so a bare period silently reports
157 * structure in noise. Strength is the normalized correlation at that
158 * lag and is what separates a quantity that genuinely repeats from one
159 * that merely has a best lag.
160 *
161 * A meaning phrased as a tendency to do something every few seconds
162 * needs both numbers: the period is the few seconds, and the strength
163 * is the tendency.
164 */
166 double period { 0.0 }; ///< Seconds between repetitions, zero when no lag qualified.
167 double strength { 0.0 }; ///< Normalized autocorrelation at that lag, in -1..1. Near zero means no repetition.
168};
169
170/**
171 * @brief Estimate a repetition period by direct autocorrelation over a
172 * bounded lag range
173 * @param values Chronological span
174 * @param dt Seconds per sample
175 * @param min_period Shortest period to consider, in seconds
176 * @param max_period Longest period to consider, in seconds
177 * @return The lag in the range with the highest normalized correlation,
178 * with that correlation as strength
179 *
180 * Direct rather than FFT-backed, and bounded rather than full-lag. The
181 * caller stating the period range they care about is not a limitation
182 * to work around: a meaning about turning every few seconds is not
183 * interested in a correlation peak at forty milliseconds, and searching
184 * for one invites the estimate to lock onto sensor noise or onto a
185 * harmonic of the real period.
186 *
187 * Correlation is normalized by the window's own variance
188 * (Estimate::variance), so strength is scale-free and comparable across
189 * channels carrying different units. A window whose variance is
190 * negligible has nothing to correlate and returns zero strength rather
191 * than a ratio of two small numbers.
192 *
193 * Requires the window to be at least twice the longest period searched,
194 * since a lag longer than half the window correlates too few pairs to
195 * mean anything. Lags failing that are skipped rather than reported
196 * with low confidence.
197 *
198 * Distinct from Discrete::auto_correlate, which is FFT-backed, computes
199 * every lag up to the buffer length, and allocates a full output vector
200 * each call; the right shape for characterizing a recording once, the
201 * wrong shape for a bounded search run on every channel update.
202 */
203[[nodiscard]] inline PeriodEstimate estimate_period(
204 std::span<const double> values,
205 double dt,
206 double min_period,
207 double max_period) noexcept
208{
209 const size_t n = values.size();
210 if (n < 4 || dt <= 0.0 || max_period <= min_period)
211 return {};
212
213 double mean = 0.0;
214 for (const double v : values)
215 mean += v;
216 mean /= static_cast<double>(n);
217
218 const double denom = Stochastic::Estimate::variance(values) * static_cast<double>(n - 1);
219 if (denom < 1e-12)
220 return {};
221
222 const auto min_lag = std::max<size_t>(1, static_cast<size_t>(min_period / dt));
223 const auto max_lag = std::min(n / 2, static_cast<size_t>(max_period / dt));
224 if (min_lag > max_lag)
225 return {};
226
227 PeriodEstimate best;
228 for (size_t lag = min_lag; lag <= max_lag; ++lag) {
229 double acc = 0.0;
230 for (size_t i = 0; i + lag < n; ++i)
231 acc += (values[i] - mean) * (values[i + lag] - mean);
232
233 const double correlation = acc / denom;
234 if (correlation > best.strength) {
235 best.strength = correlation;
236 best.period = static_cast<double>(lag) * dt;
237 }
238 }
239
240 return best;
241}
242
243} // namespace MayaFlux::Kinesis
std::vector< std::byte > values
Definition VDBWriter.cpp:26
size_t count
float lo
float threshold
float hi
static double variance(std::span< const double > samples) noexcept
Sample variance of a span.
Definition Estimate.cpp:210
double time_above(std::span< const double > values, double threshold, double dt) noexcept
Seconds within a window spent at or above a threshold.
double roughness(std::span< const double > values, double dt) noexcept
Mean absolute difference between consecutive samples, per second.
double excursion(std::span< const double > values) noexcept
Difference between the largest and smallest value in a window.
double threshold_crossing_rate(std::span< const double > values, double threshold, double dt) noexcept
Threshold crossings per second within a window.
double correlation(std::span< const double > a, std::span< const double > b) noexcept
Pearson correlation between two equal-length windows.
Definition Relation.hpp:45
PeriodEstimate estimate_period(std::span< const double > values, double dt, double min_period, double max_period) noexcept
Estimate a repetition period by direct autocorrelation over a bounded lag range.
double mean(const std::vector< double > &data)
Calculate mean of single-channel data.
Definition Yantra.cpp:55
double period
Seconds between repetitions, zero when no lag qualified.
double strength
Normalized autocorrelation at that lag, in -1..1. Near zero means no repetition.
A candidate repetition period and how strongly the window actually supports it.