MayaFlux 0.5.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
Relation.hpp
Go to the documentation of this file.
1#pragma once
2
4
5namespace MayaFlux::Kinesis {
6
7// =============================================================================
8// Scope
9//
10// Everything in FeatureExtent.hpp, PathShape.hpp, and TemporalMeasures.hpp
11// operates on one stream. This file is the missing sibling: how two or
12// more streams move with respect to each other, independent of what
13// either stream measures or where it came from. A tablet's pressure
14// against its own tilt, one dancer's tracked hand against another's,
15// a text's sentence length against its punctuation density: the same
16// functions apply to all three, because none of them look at anything
17// but the numbers.
18//
19// Chronological order, dt-as-uniform-seconds-per-sample, and the
20// non-allocating single-or-double-pass shape all follow the convention
21// established in TemporalMeasures.hpp; see that file's header comment
22// for the reasoning, not repeated here.
23//
24// Discrete::cross_correlate (Discrete/Convolution.hpp) is the offline
25// sibling: FFT-backed, computes every lag, allocates a full output
26// vector, the right tool for characterizing a whole recording once.
27// relation_at_lag below is its bounded, non-allocating, single-lag
28// counterpart, the right shape for a scalar computed on every channel
29// update, in the same relationship estimate_period in
30// TemporalMeasures.hpp has to Discrete::auto_correlate.
31// =============================================================================
32
33/**
34 * @brief Pearson correlation between two equal-length windows
35 * @param a First span
36 * @param b Second span, same length as @p a
37 * @return Correlation in -1..1, or zero if either span has near-zero
38 * variance or the lengths differ
39 *
40 * The base relation measure: do these two streams move together, apart,
41 * or independently, over this window, with no notion of one leading the
42 * other. Scale-free, so a pressure axis in 0..1 and a tilt axis in
43 * degrees compare meaningfully without the caller normalizing first.
44 */
45[[nodiscard]] inline double correlation(
46 std::span<const double> a, std::span<const double> b) noexcept
47{
48 if (a.size() != b.size() || a.size() < 2)
49 return 0.0;
50
51 const size_t n = a.size();
52 double mean_a = 0.0;
53 double mean_b = 0.0;
54 for (size_t i = 0; i < n; ++i) {
55 mean_a += a[i];
56 mean_b += b[i];
57 }
58 mean_a /= static_cast<double>(n);
59 mean_b /= static_cast<double>(n);
60
61 double cov = 0.0;
62 double var_a = 0.0;
63 double var_b = 0.0;
64 for (size_t i = 0; i < n; ++i) {
65 const double da = a[i] - mean_a;
66 const double db = b[i] - mean_b;
67 cov += da * db;
68 var_a += da * da;
69 var_b += db * db;
70 }
71
72 const double denom = std::sqrt(var_a * var_b);
73 if (denom < 1e-12)
74 return 0.0;
75
76 return std::clamp(cov / denom, -1.0, 1.0);
77}
78
79/**
80 * @struct LagRelation
81 * @brief A candidate offset between two streams and how strongly the
82 * windows support it.
83 *
84 * Mirrors PeriodEstimate in TemporalMeasures.hpp for the same reason: a
85 * bare best lag, with no strength attached, silently reports structure
86 * in noise on every call, including calls where the two streams are
87 * genuinely unrelated. lag and strength together separate "these lead
88 * one another by this much" from "no relationship was found".
89 */
91 long lag { 0 }; ///< Samples b is offset from a; positive means b lags a.
92 double strength { 0.0 }; ///< Correlation at that lag, in -1..1. Near zero means no relation.
93};
94
95/**
96 * @brief Correlation between two spans at a single fixed lag
97 * @param a First span
98 * @param b Second span
99 * @param lag Samples to offset @p b relative to @p a; positive tests
100 * whether b's later samples resemble a's earlier ones
101 * @return Correlation over the overlapping region at that lag, or zero
102 * if the overlap is too short to be meaningful
103 *
104 * The bounded, single-lag counterpart to Discrete::cross_correlate; see
105 * this file's header comment for the relationship. Exists as its own
106 * function, distinct from relation_lag below, because a caller who
107 * already knows the lag they care about (a fixed device latency, a
108 * known frame offset between two sensors) should not pay for a search
109 * over a range to confirm what they already know.
110 */
111[[nodiscard]] inline double relation_at_lag(
112 std::span<const double> a, std::span<const double> b, long lag) noexcept
113{
114 const auto na = static_cast<long>(a.size());
115 const auto nb = static_cast<long>(b.size());
116
117 const long start_a = std::max(0L, -lag);
118 const long start_b = std::max(0L, lag);
119 const long overlap = std::min(na - start_a, nb - start_b);
120
121 if (overlap < 2)
122 return 0.0;
123
124 return correlation(
125 a.subspan(static_cast<size_t>(start_a), static_cast<size_t>(overlap)),
126 b.subspan(static_cast<size_t>(start_b), static_cast<size_t>(overlap)));
127}
128
129/**
130 * @brief Search a bounded lag range for the offset at which two streams
131 * correlate most strongly
132 * @param a First span
133 * @param b Second span
134 * @param max_lag Furthest offset to test in either direction, in samples
135 * @return The lag in [-max_lag, max_lag] with the highest absolute
136 * correlation, and that correlation as strength
137 *
138 * Answers which of two streams leads and by how much, without the
139 * caller specifying the offset in advance. Searches both directions
140 * since neither stream is privileged: a positive result lag means b
141 * lags a, a negative one means a lags b.
142 *
143 * Signed strength is kept rather than always reporting the magnitude,
144 * so a caller distinguishes streams moving together (positive) from
145 * streams moving in exact opposition at the same offset (negative);
146 * the best lag is chosen by absolute value since a strong inverse
147 * relationship is as informative as a strong direct one.
148 *
149 * Bounded rather than full-length for the same reason estimate_period
150 * in TemporalMeasures.hpp bounds its period search: the caller stating
151 * the range of offsets that could plausibly matter keeps the search
152 * from locking onto a coincidental alignment far outside any physically
153 * meaningful lag between two related streams.
154 */
155[[nodiscard]] inline LagRelation relation_lag(
156 std::span<const double> a, std::span<const double> b, long max_lag) noexcept
157{
158 LagRelation best;
159 for (long lag = -max_lag; lag <= max_lag; ++lag) {
160 const double r = relation_at_lag(a, b, lag);
161 if (std::abs(r) > std::abs(best.strength)) {
162 best.strength = r;
163 best.lag = lag;
164 }
165 }
166 return best;
167}
168
169/**
170 * @brief Rate at which two streams approach or separate
171 * @param a First span, chronological order
172 * @param b Second span, chronological order, same length as @p a
173 * @param dt Seconds per sample
174 * @return Least-squares slope of |a - b| over the window, per second;
175 * negative means the streams are converging, positive diverging
176 *
177 * Distinct from correlation, which asks whether two streams move in the
178 * same direction as each other, not whether they are getting nearer.
179 * Two streams can correlate strongly while one holds steady and the
180 * other drifts away from it, or diverge while both move in the same
181 * direction at different rates. This is the direct measure of mutual
182 * approach the dancers-and-a-third-point case needs: not whether the
183 * third point's motion resembles either hand's, but whether it is
184 * closing on the gap between them.
185 *
186 * Operates on scalar streams. A caller with vector positions supplies
187 * the scalar separation (glm::length of the difference, or a projected
188 * distance) rather than raw components, since "approaching" is a
189 * statement about a single distance, not about three independent axes
190 * each separately trending.
191 */
192[[nodiscard]] inline double approach_rate(
193 std::span<const double> a, std::span<const double> b, double dt) noexcept
194{
195 const size_t n = a.size();
196 if (n != b.size() || n < 2 || dt <= 0.0)
197 return 0.0;
198
199 std::vector<double> separation(n);
200 for (size_t i = 0; i < n; ++i)
201 separation[i] = std::abs(a[i] - b[i]);
202
203 return Stochastic::Estimate::trend_slope(separation) / dt;
204}
205
206/**
207 * @brief How tightly a set of streams move together
208 * @param streams Each span the same length, at least two spans
209 * @return Mean pairwise correlation across all distinct pairs, in
210 * -1..1, or zero if fewer than two streams are given
211 *
212 * Generalizes correlation from two streams to an arbitrary-size set,
213 * with no assumption about which stream is which or how many there
214 * are: two tracked hands, five gamepad axes, a whole ensemble's worth
215 * of per-voice measurements. A high value means the set is moving as
216 * one; a value near zero means the set's motion is not coordinated;
217 * a strongly negative value means the set is systematically split into
218 * streams moving in opposition to each other.
219 *
220 * Mean pairwise rather than a single eigenvalue-based coherence measure:
221 * cheap, order-independent, and interpretable directly as a correlation
222 * without a caller needing to reason about a covariance matrix's
223 * spectrum. A caller wanting the finer-grained structure (which streams
224 * are actually forming a bloc) computes correlation() over the specific
225 * pairs of interest instead.
226 */
227[[nodiscard]] inline double coherence(
228 std::span<const std::span<const double>> streams) noexcept
229{
230 const size_t n = streams.size();
231 if (n < 2)
232 return 0.0;
233
234 double acc = 0.0;
235 size_t pairs = 0;
236 for (size_t i = 0; i < n; ++i) {
237 for (size_t j = i + 1; j < n; ++j) {
238 acc += correlation(streams[i], streams[j]);
239 ++pairs;
240 }
241 }
242
243 return (pairs > 0) ? (acc / static_cast<double>(pairs)) : 0.0;
244}
245
246} // namespace MayaFlux::Kinesis
size_t a
size_t b
static double trend_slope(std::span< const double > samples) noexcept
Linear trend slope across a span.
Definition Estimate.cpp:301
double relation_at_lag(std::span< const double > a, std::span< const double > b, long lag) noexcept
Correlation between two spans at a single fixed lag.
Definition Relation.hpp:111
double correlation(std::span< const double > a, std::span< const double > b) noexcept
Pearson correlation between two equal-length windows.
Definition Relation.hpp:45
LagRelation relation_lag(std::span< const double > a, std::span< const double > b, long max_lag) noexcept
Search a bounded lag range for the offset at which two streams correlate most strongly.
Definition Relation.hpp:155
double coherence(std::span< const std::span< const double > > streams) noexcept
How tightly a set of streams move together.
Definition Relation.hpp:227
double approach_rate(std::span< const double > a, std::span< const double > b, double dt) noexcept
Rate at which two streams approach or separate.
Definition Relation.hpp:192
double strength
Correlation at that lag, in -1..1. Near zero means no relation.
Definition Relation.hpp:92
long lag
Samples b is offset from a; positive means b lags a.
Definition Relation.hpp:91
A candidate offset between two streams and how strongly the windows support it.
Definition Relation.hpp:90