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}