fdars_core/alignment/pairwise.rs
1//! Pairwise elastic alignment, distance computation, and distance matrices.
2
3use super::srsf::{reparameterize_curve, srsf_single, srsf_transform};
4use super::{dp_alignment_core, AlignmentResult};
5use crate::helpers::{l2_distance, simpsons_weights};
6use crate::iter_maybe_parallel;
7use crate::matrix::FdMatrix;
8#[cfg(feature = "parallel")]
9use rayon::iter::ParallelIterator;
10
11// ─── Public Alignment Functions ─────────────────────────────────────────────
12
13/// Align curve f2 to curve f1 using the elastic framework.
14///
15/// Computes the optimal warping γ such that f2∘γ is as close as possible
16/// to f1 in the elastic (Fisher-Rao) metric.
17///
18/// # Arguments
19/// * `f1` — Target curve (length m)
20/// * `f2` — Curve to align (length m)
21/// * `argvals` — Evaluation points (length m)
22/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
23///
24/// # Returns
25/// [`AlignmentResult`] with warping function, aligned curve, and elastic distance.
26///
27/// # Examples
28///
29/// ```
30/// use fdars_core::alignment::elastic_align_pair;
31///
32/// let argvals: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
33/// let f1: Vec<f64> = argvals.iter().map(|&t| (t * 6.0).sin()).collect();
34/// let f2: Vec<f64> = argvals.iter().map(|&t| ((t + 0.1) * 6.0).sin()).collect();
35/// let result = elastic_align_pair(&f1, &f2, &argvals, 0.0);
36/// assert_eq!(result.f_aligned.len(), 20);
37/// assert!(result.distance >= 0.0);
38/// ```
39#[must_use = "expensive computation whose result should not be discarded"]
40pub fn elastic_align_pair(f1: &[f64], f2: &[f64], argvals: &[f64], lambda: f64) -> AlignmentResult {
41 let q1 = srsf_single(f1, argvals);
42 let q2 = srsf_single(f2, argvals);
43 elastic_align_pair_from_srsf(f2, &q1, &q2, argvals, lambda)
44}
45
46/// Compute the elastic distance between two curves.
47///
48/// This is shorthand for aligning the pair and returning only the distance.
49///
50/// # Arguments
51/// * `f1` — First curve (length m)
52/// * `f2` — Second curve (length m)
53/// * `argvals` — Evaluation points (length m)
54/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
55///
56/// # Examples
57///
58/// ```
59/// use fdars_core::alignment::elastic_distance;
60///
61/// let argvals: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
62/// let f1: Vec<f64> = argvals.iter().map(|&t| (t * 6.0).sin()).collect();
63/// let f2: Vec<f64> = argvals.iter().map(|&t| ((t + 0.1) * 6.0).sin()).collect();
64/// let d = elastic_distance(&f1, &f2, &argvals, 0.0);
65/// assert!(d >= 0.0);
66/// ```
67#[must_use = "expensive computation whose result should not be discarded"]
68pub fn elastic_distance(f1: &[f64], f2: &[f64], argvals: &[f64], lambda: f64) -> f64 {
69 elastic_align_pair(f1, f2, argvals, lambda).distance
70}
71
72// ─── Internal Helpers with Pre-computed SRSFs ────────────────────────────────
73
74/// Align curve f2 to curve f1 given their pre-computed SRSFs.
75///
76/// This avoids redundant SRSF computation when calling from distance matrix
77/// routines where the same curve's SRSF would otherwise be recomputed for
78/// every pair.
79fn elastic_align_pair_from_srsf(
80 f2: &[f64],
81 q1: &[f64],
82 q2: &[f64],
83 argvals: &[f64],
84 lambda: f64,
85) -> AlignmentResult {
86 // Find optimal warping via DP
87 let gamma = dp_alignment_core(q1, q2, argvals, lambda);
88
89 // Apply warping to f2
90 let f_aligned = reparameterize_curve(f2, argvals, &gamma);
91
92 // Compute elastic distance: L2 distance between q1 and aligned q2 SRSF
93 let q_aligned = srsf_single(&f_aligned, argvals);
94
95 let weights = simpsons_weights(argvals);
96 let distance = l2_distance(q1, &q_aligned, &weights);
97
98 AlignmentResult {
99 gamma,
100 f_aligned,
101 distance,
102 }
103}
104
105/// Compute elastic distance given a raw curve f2, and pre-computed SRSFs q1, q2.
106///
107/// The raw f2 is needed to reparameterize before computing the aligned SRSF.
108fn elastic_distance_from_srsf(
109 f2: &[f64],
110 q1: &[f64],
111 q2: &[f64],
112 argvals: &[f64],
113 lambda: f64,
114) -> f64 {
115 let gamma = dp_alignment_core(q1, q2, argvals, lambda);
116 let f_aligned = reparameterize_curve(f2, argvals, &gamma);
117 let q_aligned = srsf_single(&f_aligned, argvals);
118 let weights = simpsons_weights(argvals);
119 l2_distance(q1, &q_aligned, &weights)
120}
121
122// ─── Distance Matrices ──────────────────────────────────────────────────────
123
124/// Compute the symmetric elastic distance matrix for a set of curves.
125///
126/// Pre-computes SRSF transforms for all curves once (O(n)) instead of
127/// recomputing each curve's SRSF for every pair (O(n²)).
128///
129/// Uses upper-triangle computation with parallelism, following the
130/// `self_distance_matrix` pattern from `metric.rs`.
131///
132/// # Arguments
133/// * `data` — Functional data matrix (n × m)
134/// * `argvals` — Evaluation points (length m)
135/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
136///
137/// # Returns
138/// Symmetric n × n distance matrix.
139pub fn elastic_self_distance_matrix(data: &FdMatrix, argvals: &[f64], lambda: f64) -> FdMatrix {
140 let n = data.nrows();
141
142 // Pre-compute all SRSF transforms once
143 let srsfs = srsf_transform(data, argvals);
144
145 let upper_vals: Vec<f64> = iter_maybe_parallel!(0..n)
146 .flat_map(|i| {
147 let qi = srsfs.row(i);
148 ((i + 1)..n)
149 .map(|j| {
150 let fj = data.row(j);
151 let qj = srsfs.row(j);
152 elastic_distance_from_srsf(&fj, &qi, &qj, argvals, lambda)
153 })
154 .collect::<Vec<_>>()
155 })
156 .collect();
157
158 let mut dist = FdMatrix::zeros(n, n);
159 let mut idx = 0;
160 for i in 0..n {
161 for j in (i + 1)..n {
162 let d = upper_vals[idx];
163 dist[(i, j)] = d;
164 dist[(j, i)] = d;
165 idx += 1;
166 }
167 }
168 dist
169}
170
171/// Compute the elastic distance matrix between two sets of curves.
172///
173/// Pre-computes SRSF transforms for both datasets once instead of
174/// recomputing each curve's SRSF for every pair.
175///
176/// # Arguments
177/// * `data1` — First dataset (n1 × m)
178/// * `data2` — Second dataset (n2 × m)
179/// * `argvals` — Evaluation points (length m)
180/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
181///
182/// # Returns
183/// n1 × n2 distance matrix.
184pub fn elastic_cross_distance_matrix(
185 data1: &FdMatrix,
186 data2: &FdMatrix,
187 argvals: &[f64],
188 lambda: f64,
189) -> FdMatrix {
190 let n1 = data1.nrows();
191 let n2 = data2.nrows();
192
193 // Pre-compute all SRSF transforms once for both datasets
194 let srsfs1 = srsf_transform(data1, argvals);
195 let srsfs2 = srsf_transform(data2, argvals);
196
197 let vals: Vec<f64> = iter_maybe_parallel!(0..n1)
198 .flat_map(|i| {
199 let qi = srsfs1.row(i);
200 (0..n2)
201 .map(|j| {
202 let fj = data2.row(j);
203 let qj = srsfs2.row(j);
204 elastic_distance_from_srsf(&fj, &qi, &qj, argvals, lambda)
205 })
206 .collect::<Vec<_>>()
207 })
208 .collect();
209
210 let mut dist = FdMatrix::zeros(n1, n2);
211 for i in 0..n1 {
212 for j in 0..n2 {
213 dist[(i, j)] = vals[i * n2 + j];
214 }
215 }
216 dist
217}
218
219/// Compute the amplitude distance between two curves (= elastic distance after alignment).
220pub fn amplitude_distance(f1: &[f64], f2: &[f64], argvals: &[f64], lambda: f64) -> f64 {
221 elastic_distance(f1, f2, argvals, lambda)
222}
223
224/// Compute the phase distance between two curves (geodesic distance of optimal warp from identity).
225pub fn phase_distance_pair(f1: &[f64], f2: &[f64], argvals: &[f64], lambda: f64) -> f64 {
226 let alignment = elastic_align_pair(f1, f2, argvals, lambda);
227 crate::warping::phase_distance(&alignment.gamma, argvals)
228}
229
230/// Compute the symmetric phase distance matrix for a set of curves.
231pub fn phase_self_distance_matrix(data: &FdMatrix, argvals: &[f64], lambda: f64) -> FdMatrix {
232 let n = data.nrows();
233
234 let upper_vals: Vec<f64> = iter_maybe_parallel!(0..n)
235 .flat_map(|i| {
236 let fi = data.row(i);
237 ((i + 1)..n)
238 .map(|j| {
239 let fj = data.row(j);
240 phase_distance_pair(&fi, &fj, argvals, lambda)
241 })
242 .collect::<Vec<_>>()
243 })
244 .collect();
245
246 let mut dist = FdMatrix::zeros(n, n);
247 let mut idx = 0;
248 for i in 0..n {
249 for j in (i + 1)..n {
250 let d = upper_vals[idx];
251 dist[(i, j)] = d;
252 dist[(j, i)] = d;
253 idx += 1;
254 }
255 }
256 dist
257}
258
259/// Compute the symmetric amplitude distance matrix (= elastic self distance matrix).
260pub fn amplitude_self_distance_matrix(data: &FdMatrix, argvals: &[f64], lambda: f64) -> FdMatrix {
261 elastic_self_distance_matrix(data, argvals, lambda)
262}