Skip to main content

fdars_core/elastic_regression/
mod.rs

1//! Elastic regression models (alignment-integrated regression).
2//!
3//! These models from fdasrvf align curves during the regression fitting process,
4//! jointly optimizing alignment and regression coefficients.
5//!
6//! Key capabilities:
7//! - [`elastic_regression`] — Scalar-on-function regression with elastic alignment
8//! - [`elastic_logistic`] — Binary classification with elastic alignment
9//! - [`elastic_pcr`] — Principal component regression after elastic alignment
10//! - [`scalar_on_shape()`] — Scalar-on-shape regression with optional single-index link
11
12pub mod logistic;
13pub mod pcr;
14pub mod regression;
15pub mod scalar_on_shape;
16
17#[cfg(test)]
18mod tests;
19
20// Re-export all public items
21pub use logistic::{
22    elastic_logistic, elastic_logistic_with_config, elastic_multinomial, predict_elastic_logistic,
23    predict_elastic_multinomial, ElasticLogisticResult, ElasticMultinomialResult,
24};
25pub use pcr::{elastic_pcr, elastic_pcr_with_config, ElasticPcrResult};
26pub use regression::{
27    elastic_regression, elastic_regression_with_config, predict_elastic_regression,
28    ElasticRegressionResult,
29};
30pub use scalar_on_shape::{predict_scalar_on_shape, scalar_on_shape, ScalarOnShapeResult};
31
32use crate::alignment::reparameterize_curve;
33use crate::matrix::FdMatrix;
34
35// ─── Config Structs ─────────────────────────────────────────────────────────
36
37/// Configuration for [`elastic_regression`] and [`elastic_logistic`].
38#[derive(Debug, Clone, PartialEq)]
39pub struct ElasticConfig {
40    /// Number of basis functions for the beta coefficient (for elastic_regression).
41    pub ncomp_beta: usize,
42    /// Roughness penalty weight.
43    pub lambda: f64,
44    /// Maximum iterations for iterative alignment.
45    pub max_iter: usize,
46    /// Convergence tolerance.
47    pub tol: f64,
48}
49
50impl Default for ElasticConfig {
51    fn default() -> Self {
52        Self {
53            ncomp_beta: 10,
54            lambda: 0.0,
55            max_iter: 20,
56            tol: 1e-4,
57        }
58    }
59}
60
61/// Configuration for [`elastic_pcr`].
62#[derive(Debug, Clone, PartialEq)]
63pub struct ElasticPcrConfig {
64    /// Number of principal components to retain.
65    pub ncomp: usize,
66    /// PCA method (vertical, horizontal, or joint).
67    pub pca_method: PcaMethod,
68    /// Roughness penalty weight.
69    pub lambda: f64,
70    /// Maximum iterations for Karcher mean.
71    pub max_iter: usize,
72    /// Convergence tolerance for Karcher mean.
73    pub tol: f64,
74}
75
76impl Default for ElasticPcrConfig {
77    fn default() -> Self {
78        Self {
79            ncomp: 3,
80            pca_method: PcaMethod::Vertical,
81            lambda: 0.0,
82            max_iter: 20,
83            tol: 1e-4,
84        }
85    }
86}
87
88/// Configuration for [`scalar_on_shape()`].
89#[derive(Debug, Clone, PartialEq)]
90pub struct ScalarOnShapeConfig {
91    /// Number of Fourier basis functions for the beta representation.
92    pub nbasis: usize,
93    /// Roughness penalty weight for beta.
94    pub lambda: f64,
95    /// Penalty derivative order.
96    pub lfd_order: usize,
97    /// Index function method.
98    pub index_method: IndexMethod,
99    /// Polynomial degree for g (intercept link function).
100    pub g_degree: usize,
101    /// Maximum outer iterations (alternating beta, h, g).
102    pub max_iter_outer: usize,
103    /// Maximum inner iterations (beta estimation with alignment).
104    pub max_iter_inner: usize,
105    /// Convergence tolerance.
106    pub tol: f64,
107    /// DP alignment penalty.
108    pub dp_lambda: f64,
109}
110
111impl Default for ScalarOnShapeConfig {
112    fn default() -> Self {
113        Self {
114            nbasis: 11,
115            lambda: 1e-3,
116            lfd_order: 2,
117            index_method: IndexMethod::Identity,
118            g_degree: 2,
119            max_iter_outer: 10,
120            max_iter_inner: 15,
121            tol: 1e-4,
122            dp_lambda: 0.0,
123        }
124    }
125}
126
127// ─── Types ──────────────────────────────────────────────────────────────────
128
129/// PCA method for elastic PCR.
130#[derive(Debug, Clone, Copy, PartialEq)]
131#[non_exhaustive]
132pub enum PcaMethod {
133    Vertical,
134    Horizontal,
135    Joint,
136}
137
138/// Index function method for scalar-on-shape regression.
139///
140/// Controls the link between the shape score and the response variable.
141#[derive(Debug, Clone, PartialEq)]
142#[non_exhaustive]
143pub enum IndexMethod {
144    /// Identity: h(z) = z (standard ScoSh).
145    Identity,
146    /// Polynomial single-index: h(z) = sum of a_k z^k (SI-ScoSh).
147    Polynomial(usize),
148    /// Nadaraya-Watson kernel estimate with the given bandwidth.
149    NadarayaWatson(f64),
150}
151
152// ─── Shared Helpers ────────────────────────────────────────────────────────
153
154/// Apply warping functions to SRSFs, producing aligned SRSFs with sqrt(γ') factor.
155pub(super) fn apply_warps_to_srsfs(
156    q_all: &FdMatrix,
157    gammas: &FdMatrix,
158    argvals: &[f64],
159) -> FdMatrix {
160    let (n, m) = q_all.shape();
161    let h = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
162    let mut q_aligned = FdMatrix::zeros(n, m);
163    for i in 0..n {
164        let qi: Vec<f64> = (0..m).map(|j| q_all[(i, j)]).collect();
165        let gam: Vec<f64> = (0..m).map(|j| gammas[(i, j)]).collect();
166        let q_warped = reparameterize_curve(&qi, argvals, &gam);
167        let gam_deriv = crate::helpers::gradient_uniform(&gam, h);
168        for j in 0..m {
169            q_aligned[(i, j)] = q_warped[j] * gam_deriv[j].max(0.0).sqrt();
170        }
171    }
172    q_aligned
173}
174
175/// Initialize warping functions to identity (γ_i(t) = t).
176pub(super) fn init_identity_warps(n: usize, argvals: &[f64]) -> FdMatrix {
177    let m = argvals.len();
178    let mut gammas = FdMatrix::zeros(n, m);
179    for i in 0..n {
180        for j in 0..m {
181            gammas[(i, j)] = argvals[j];
182        }
183    }
184    gammas
185}
186
187/// Compute fitted values: ŷ_i = α + ∫ q_aligned_i · β · w dt.
188pub(super) fn srsf_fitted_values(
189    q_aligned: &FdMatrix,
190    beta: &[f64],
191    weights: &[f64],
192    alpha: f64,
193) -> Vec<f64> {
194    let (n, m) = q_aligned.shape();
195    let mut fitted = vec![0.0; n];
196    for i in 0..n {
197        fitted[i] = alpha;
198        for j in 0..m {
199            fitted[i] += q_aligned[(i, j)] * beta[j] * weights[j];
200        }
201    }
202    fitted
203}
204
205/// Check relative convergence of β.
206pub(super) fn beta_converged(beta_new: &[f64], beta_old: &[f64], tol: f64) -> bool {
207    let diff: f64 = beta_new
208        .iter()
209        .zip(beta_old.iter())
210        .map(|(&a, &b)| (a - b).powi(2))
211        .sum::<f64>()
212        .sqrt();
213    let norm: f64 = beta_old
214        .iter()
215        .map(|&b| b * b)
216        .sum::<f64>()
217        .sqrt()
218        .max(1e-10);
219    diff / norm < tol
220}