Skip to main content

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