Skip to main content

fdars_core/alignment/
srsf.rs

1//! SRSF (Square-Root Slope Function) transforms and warping utilities.
2
3use crate::error::FdarError;
4use crate::fdata::{deriv, DerivDomain, DerivResult};
5use crate::helpers::{cumulative_trapz, linear_interp};
6use crate::matrix::FdMatrix;
7
8// ─── SRSF Transform and Inverse ─────────────────────────────────────────────
9
10/// Compute the Square-Root Slope Function (SRSF) transform.
11///
12/// For each curve f, the SRSF is: `q(t) = sign(f'(t)) * sqrt(|f'(t)|)`
13///
14/// # Arguments
15/// * `data` — Functional data matrix (n × m)
16/// * `argvals` — Evaluation points (length m)
17///
18/// # Returns
19/// FdMatrix of SRSFs with the same shape as input.
20///
21/// # Examples
22///
23/// ```
24/// use fdars_core::matrix::FdMatrix;
25/// use fdars_core::alignment::srsf_transform;
26///
27/// let argvals: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
28/// let data = FdMatrix::from_column_major(
29///     argvals.iter().map(|&t| (t * 6.0).sin()).collect(),
30///     1, 20,
31/// ).unwrap();
32/// let srsf = srsf_transform(&data, &argvals);
33/// assert_eq!(srsf.shape(), (1, 20));
34/// ```
35#[must_use = "expensive computation whose result should not be discarded"]
36pub fn srsf_transform(data: &FdMatrix, argvals: &[f64]) -> FdMatrix {
37    let (n, m) = data.shape();
38    if n == 0 || m == 0 || argvals.len() != m {
39        return FdMatrix::zeros(n, m);
40    }
41
42    let DerivResult::OneD(deriv_mat) = deriv(data, DerivDomain::OneD { argvals, nderiv: 1 }) else {
43        unreachable!("1D domain yields a 1D result");
44    };
45
46    let mut result = FdMatrix::zeros(n, m);
47    for i in 0..n {
48        for j in 0..m {
49            let d = deriv_mat[(i, j)];
50            result[(i, j)] = d.signum() * d.abs().sqrt();
51        }
52    }
53    result
54}
55
56/// Reconstruct a curve from its SRSF representation.
57///
58/// Given SRSF q and initial value f0, reconstructs: `f(t) = f0 + ∫₀ᵗ q(s)|q(s)| ds`
59///
60/// # Arguments
61/// * `q` — SRSF values (length m)
62/// * `argvals` — Evaluation points (length m)
63/// * `f0` — Initial value f(argvals\[0\])
64///
65/// # Returns
66/// Reconstructed curve values.
67pub fn srsf_inverse(q: &[f64], argvals: &[f64], f0: f64) -> Vec<f64> {
68    let m = q.len();
69    if m == 0 {
70        return Vec::new();
71    }
72
73    // Integrand: q(s) * |q(s)|
74    let integrand: Vec<f64> = q.iter().map(|&qi| qi * qi.abs()).collect();
75    let integral = cumulative_trapz(&integrand, argvals);
76
77    integral.iter().map(|&v| f0 + v).collect()
78}
79
80// ─── Reparameterization ─────────────────────────────────────────────────────
81
82/// Reparameterize a curve by a warping function.
83///
84/// Computes `f(γ(t))` via linear interpolation.
85///
86/// # Arguments
87/// * `f` — Curve values (length m)
88/// * `argvals` — Evaluation points (length m)
89/// * `gamma` — Warping function values (length m)
90pub fn reparameterize_curve(f: &[f64], argvals: &[f64], gamma: &[f64]) -> Vec<f64> {
91    gamma
92        .iter()
93        .map(|&g| linear_interp(argvals, f, g))
94        .collect()
95}
96
97/// Compose two warping functions: `(γ₁ ∘ γ₂)(t) = γ₁(γ₂(t))`.
98///
99/// # Arguments
100/// * `gamma1` — Outer warping function (length m)
101/// * `gamma2` — Inner warping function (length m)
102/// * `argvals` — Evaluation points (length m)
103pub fn compose_warps(gamma1: &[f64], gamma2: &[f64], argvals: &[f64]) -> Vec<f64> {
104    gamma2
105        .iter()
106        .map(|&g| linear_interp(argvals, gamma1, g))
107        .collect()
108}
109
110/// Compute a single SRSF from a slice (single-row convenience).
111pub(crate) fn srsf_single(f: &[f64], argvals: &[f64]) -> Vec<f64> {
112    let m = f.len();
113    let mat = FdMatrix::from_slice(f, 1, m).expect("dimension invariant: data.len() == n * m");
114    let q_mat = srsf_transform(&mat, argvals);
115    q_mat.row(0)
116}
117
118/// Compute the inverse of a warping function.
119///
120/// Given γ: \[a,b\] → \[a,b\], computes γ⁻¹ such that γ⁻¹(γ(t)) ≈ t.
121/// The inverse is computed by mapping to \[0,1\], calling the sphere-based
122/// inverse from the warping module, and mapping back to the original domain.
123///
124/// # Errors
125/// Returns `FdarError::InvalidDimension` if lengths do not match or m < 2.
126pub fn invert_warp(gamma: &[f64], argvals: &[f64]) -> Result<Vec<f64>, FdarError> {
127    let m = gamma.len();
128    if m != argvals.len() {
129        return Err(FdarError::InvalidDimension {
130            parameter: "gamma",
131            expected: format!("length {}", argvals.len()),
132            actual: format!("length {m}"),
133        });
134    }
135    if m < 2 {
136        return Err(FdarError::InvalidDimension {
137            parameter: "gamma",
138            expected: "length >= 2".to_string(),
139            actual: format!("length {m}"),
140        });
141    }
142    let t0 = argvals[0];
143    let domain = argvals[m - 1] - t0;
144    if domain <= 0.0 {
145        return Err(FdarError::InvalidParameter {
146            parameter: "argvals",
147            message: format!("domain must be positive, got {domain}"),
148        });
149    }
150    // Normalize to [0,1]
151    let gam_01: Vec<f64> = gamma.iter().map(|&g| (g - t0) / domain).collect();
152    let time_01: Vec<f64> = argvals.iter().map(|&t| (t - t0) / domain).collect();
153    // Invert using warping module
154    let inv_01 = crate::warping::invert_gamma(&gam_01, &time_01);
155    // Map back to original domain
156    let mut result: Vec<f64> = inv_01.iter().map(|&g| t0 + g * domain).collect();
157    crate::warping::normalize_warp(&mut result, argvals);
158    Ok(result)
159}
160
161/// Verify roundtrip accuracy: max |γ(γ⁻¹(t)) - t| over the domain.
162///
163/// Returns the maximum absolute deviation. Values near 0 indicate
164/// a high-quality inverse. Typical values for smooth warps: < 1e-10.
165pub fn warp_inverse_error(gamma: &[f64], gamma_inv: &[f64], argvals: &[f64]) -> f64 {
166    // compose gamma with gamma_inv, compare to identity
167    let roundtrip = compose_warps(gamma, gamma_inv, argvals);
168    roundtrip
169        .iter()
170        .zip(argvals.iter())
171        .map(|(&r, &t)| (r - t).abs())
172        .fold(0.0_f64, f64::max)
173}