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///
39/// Construct via `ElasticConfig::default()`, then assign the fields you need (e.g. `let mut c = ElasticConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq)]
42pub struct ElasticConfig {
43    /// Number of basis functions for the beta coefficient (for elastic_regression).
44    pub ncomp_beta: usize,
45    /// Roughness penalty weight.
46    pub lambda: f64,
47    /// Maximum iterations for iterative alignment.
48    pub max_iter: usize,
49    /// Convergence tolerance.
50    pub tol: f64,
51}
52
53impl Default for ElasticConfig {
54    fn default() -> Self {
55        Self {
56            ncomp_beta: 10,
57            lambda: 0.0,
58            max_iter: 20,
59            tol: 1e-4,
60        }
61    }
62}
63
64/// Configuration for [`elastic_pcr`].
65///
66/// Construct via `ElasticPcrConfig::default()`, then assign the fields you need (e.g. `let mut c = ElasticPcrConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
67#[non_exhaustive]
68#[derive(Debug, Clone, PartialEq)]
69pub struct ElasticPcrConfig {
70    /// Number of principal components to retain.
71    pub ncomp: usize,
72    /// PCA method (vertical, horizontal, or joint).
73    pub pca_method: PcaMethod,
74    /// Roughness penalty weight.
75    pub lambda: f64,
76    /// Maximum iterations for Karcher mean.
77    pub max_iter: usize,
78    /// Convergence tolerance for Karcher mean.
79    pub tol: f64,
80}
81
82impl Default for ElasticPcrConfig {
83    fn default() -> Self {
84        Self {
85            ncomp: 3,
86            pca_method: PcaMethod::Vertical,
87            lambda: 0.0,
88            max_iter: 20,
89            tol: 1e-4,
90        }
91    }
92}
93
94/// Configuration for [`scalar_on_shape()`].
95///
96/// Construct via `ScalarOnShapeConfig::default()`, then assign the fields you need (e.g. `let mut c = ScalarOnShapeConfig::default(); c.field = …;`). This struct is `#[non_exhaustive]`, so external crates cannot build it with a struct literal — not even functional-update `..Default::default()` form.
97#[non_exhaustive]
98#[derive(Debug, Clone, PartialEq)]
99pub struct ScalarOnShapeConfig {
100    /// Number of Fourier basis functions for the beta representation.
101    pub nbasis: usize,
102    /// Roughness penalty weight for beta.
103    pub lambda: f64,
104    /// Penalty derivative order.
105    pub lfd_order: usize,
106    /// Index function method.
107    pub index_method: IndexMethod,
108    /// Polynomial degree for g (intercept link function).
109    pub g_degree: usize,
110    /// Maximum outer iterations (alternating beta, h, g).
111    pub max_iter_outer: usize,
112    /// Maximum inner iterations (beta estimation with alignment).
113    pub max_iter_inner: usize,
114    /// Convergence tolerance.
115    pub tol: f64,
116    /// DP alignment penalty.
117    pub dp_lambda: f64,
118}
119
120impl Default for ScalarOnShapeConfig {
121    fn default() -> Self {
122        Self {
123            nbasis: 11,
124            lambda: 1e-3,
125            lfd_order: 2,
126            index_method: IndexMethod::Identity,
127            g_degree: 2,
128            max_iter_outer: 10,
129            max_iter_inner: 15,
130            tol: 1e-4,
131            dp_lambda: 0.0,
132        }
133    }
134}
135
136// ─── Types ──────────────────────────────────────────────────────────────────
137
138/// PCA method for elastic PCR.
139#[derive(Debug, Clone, Copy, PartialEq)]
140#[non_exhaustive]
141pub enum PcaMethod {
142    Vertical,
143    Horizontal,
144    Joint,
145}
146
147/// Index function method for scalar-on-shape regression.
148///
149/// Controls the link between the shape score and the response variable.
150#[derive(Debug, Clone, PartialEq)]
151#[non_exhaustive]
152pub enum IndexMethod {
153    /// Identity: h(z) = z (standard ScoSh).
154    Identity,
155    /// Polynomial single-index: h(z) = sum of a_k z^k (SI-ScoSh).
156    Polynomial(usize),
157    /// Nadaraya-Watson kernel estimate with the given bandwidth.
158    NadarayaWatson(f64),
159}
160
161// ─── Shared Helpers ────────────────────────────────────────────────────────
162
163/// Apply warping functions to SRSFs, producing aligned SRSFs with sqrt(γ') factor.
164pub(super) fn apply_warps_to_srsfs(
165    q_all: &FdMatrix,
166    gammas: &FdMatrix,
167    argvals: &[f64],
168) -> FdMatrix {
169    let (n, m) = q_all.shape();
170    let h = (argvals[m - 1] - argvals[0]) / (m - 1) as f64;
171    let mut q_aligned = FdMatrix::zeros(n, m);
172    for i in 0..n {
173        let qi: Vec<f64> = (0..m).map(|j| q_all[(i, j)]).collect();
174        let gam: Vec<f64> = (0..m).map(|j| gammas[(i, j)]).collect();
175        let q_warped = reparameterize_curve(&qi, argvals, &gam);
176        let gam_deriv = crate::helpers::gradient_uniform(&gam, h);
177        for j in 0..m {
178            q_aligned[(i, j)] = q_warped[j] * gam_deriv[j].max(0.0).sqrt();
179        }
180    }
181    q_aligned
182}
183
184/// Initialize warping functions to identity (γ_i(t) = t).
185pub(super) fn init_identity_warps(n: usize, argvals: &[f64]) -> FdMatrix {
186    let m = argvals.len();
187    let mut gammas = FdMatrix::zeros(n, m);
188    for i in 0..n {
189        for j in 0..m {
190            gammas[(i, j)] = argvals[j];
191        }
192    }
193    gammas
194}
195
196/// Compute fitted values: ŷ_i = α + ∫ q_aligned_i · β · w dt.
197pub(super) fn srsf_fitted_values(
198    q_aligned: &FdMatrix,
199    beta: &[f64],
200    weights: &[f64],
201    alpha: f64,
202) -> Vec<f64> {
203    let (n, m) = q_aligned.shape();
204    let mut fitted = vec![0.0; n];
205    for i in 0..n {
206        fitted[i] = alpha;
207        for j in 0..m {
208            fitted[i] += q_aligned[(i, j)] * beta[j] * weights[j];
209        }
210    }
211    fitted
212}
213
214/// Check relative convergence of β.
215pub(super) fn beta_converged(beta_new: &[f64], beta_old: &[f64], tol: f64) -> bool {
216    let diff: f64 = beta_new
217        .iter()
218        .zip(beta_old.iter())
219        .map(|(&a, &b)| (a - b).powi(2))
220        .sum::<f64>()
221        .sqrt();
222    let norm: f64 = beta_old
223        .iter()
224        .map(|&b| b * b)
225        .sum::<f64>()
226        .sqrt()
227        .max(1e-10);
228    diff / norm < tol
229}