Skip to main content

fdars_core/alignment/
set.rs

1//! Set-level alignment operations and elastic decomposition.
2
3use super::pairwise::elastic_align_pair;
4use super::srsf::reparameterize_curve;
5use super::{AlignmentResult, AlignmentSetResult};
6use crate::iter_maybe_parallel;
7use crate::matrix::FdMatrix;
8#[cfg(feature = "parallel")]
9use rayon::iter::ParallelIterator;
10
11/// Result of elastic phase-amplitude decomposition.
12#[derive(Debug, Clone, PartialEq)]
13pub struct DecompositionResult {
14    /// Full alignment result.
15    pub alignment: AlignmentResult,
16    /// Amplitude distance: SRSF distance after alignment.
17    pub d_amplitude: f64,
18    /// Phase distance: geodesic distance of warp from identity.
19    pub d_phase: f64,
20}
21
22/// Align all curves in `data` to a single target curve.
23///
24/// # Arguments
25/// * `data` — Functional data matrix (n × m)
26/// * `target` — Target curve to align to (length m)
27/// * `argvals` — Evaluation points (length m)
28/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
29///
30/// # Returns
31/// [`AlignmentSetResult`] with all warping functions, aligned curves, and distances.
32#[must_use = "expensive computation whose result should not be discarded"]
33pub fn align_to_target(
34    data: &FdMatrix,
35    target: &[f64],
36    argvals: &[f64],
37    lambda: f64,
38) -> AlignmentSetResult {
39    let (n, m) = data.shape();
40
41    let results: Vec<AlignmentResult> = iter_maybe_parallel!(0..n)
42        .map(|i| {
43            let fi = data.row(i);
44            elastic_align_pair(target, &fi, argvals, lambda)
45        })
46        .collect();
47
48    let mut gammas = FdMatrix::zeros(n, m);
49    let mut aligned_data = FdMatrix::zeros(n, m);
50    let mut distances = Vec::with_capacity(n);
51
52    for (i, r) in results.into_iter().enumerate() {
53        for j in 0..m {
54            gammas[(i, j)] = r.gamma[j];
55            aligned_data[(i, j)] = r.f_aligned[j];
56        }
57        distances.push(r.distance);
58    }
59
60    AlignmentSetResult {
61        gammas,
62        aligned_data,
63        distances,
64    }
65}
66
67/// Perform elastic phase-amplitude decomposition of two curves.
68///
69/// Returns both the alignment result and the separate amplitude and phase distances.
70///
71/// # Arguments
72/// * `f1` — Target curve (length m)
73/// * `f2` — Curve to decompose against f1 (length m)
74/// * `argvals` — Evaluation points (length m)
75/// * `lambda` — Penalty weight on warp deviation from identity (0.0 = no penalty)
76pub fn elastic_decomposition(
77    f1: &[f64],
78    f2: &[f64],
79    argvals: &[f64],
80    lambda: f64,
81) -> DecompositionResult {
82    let alignment = elastic_align_pair(f1, f2, argvals, lambda);
83    let d_amplitude = alignment.distance;
84    let d_phase = crate::warping::phase_distance(&alignment.gamma, argvals);
85    DecompositionResult {
86        alignment,
87        d_amplitude,
88        d_phase,
89    }
90}
91
92/// Apply stored warps to original curves to produce aligned data.
93pub(super) fn apply_stored_warps(data: &FdMatrix, gammas: &FdMatrix, argvals: &[f64]) -> FdMatrix {
94    let (n, m) = data.shape();
95    let mut aligned = FdMatrix::zeros(n, m);
96    for i in 0..n {
97        let fi = data.row(i);
98        let gamma: Vec<f64> = (0..m).map(|j| gammas[(i, j)]).collect();
99        let f_aligned = reparameterize_curve(&fi, argvals, &gamma);
100        for j in 0..m {
101            aligned[(i, j)] = f_aligned[j];
102        }
103    }
104    aligned
105}