Skip to main content

fdars_core/
wire.rs

1//! Unified FDA data container for pipeline interchange.
2//!
3//! [`FdaData`] is a single layered container that flows between pipeline nodes.
4//! Nodes read from existing layers and add new ones — data is additive, never
5//! destructive. This replaces per-type wire enums with a composable structure.
6//!
7//! # Design
8//!
9//! - **Core**: curves (FdMatrix) + argvals + metadata (grouping, scalars)
10//! - **Layers**: optional analysis results keyed by [`LayerKey`]
11//! - Nodes declare what they *require* via `require_*` helpers
12//! - Nodes add results via `set_layer`
13//! - Layers compose: FPCA + Depth + Outliers can all coexist on one `FdaData`
14//!
15//! # Example
16//!
17//! ```
18//! use fdars_core::wire::*;
19//! use fdars_core::matrix::FdMatrix;
20//!
21//! let mut fd = FdaData::from_curves(
22//!     FdMatrix::zeros(10, 50),
23//!     (0..50).map(|i| i as f64 / 49.0).collect(),
24//! );
25//!
26//! // A depth node reads curves, adds a Depth layer
27//! let scores = vec![0.5; 10];
28//! fd.set_layer(LayerKey::Depth, Layer::Depth(DepthLayer {
29//!     scores,
30//!     method: "fraiman_muniz".into(),
31//! }));
32//!
33//! // Downstream node checks what's available
34//! assert!(fd.has_layer(&LayerKey::Depth));
35//! assert!(!fd.has_layer(&LayerKey::Fpca));
36//! ```
37
38use crate::matrix::FdMatrix;
39use std::collections::HashMap;
40
41// ─── Core Container ─────────────────────────────────────────────────────────
42
43/// Unified FDA data object for pipeline interchange.
44///
45/// Carries functional data (curves + domain) plus optional analysis layers.
46/// Nodes read what they need and add their results as new layers.
47#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct FdaData {
50    // ── Core functional data ──
51    /// Functional observations (n × m). `None` for tabular-only data.
52    pub curves: Option<FdMatrix>,
53    /// Evaluation grid (length m).
54    pub argvals: Option<Vec<f64>>,
55
56    // ── Metadata ──
57    /// Named grouping variables (multiple allowed).
58    pub grouping: Vec<GroupVar>,
59    /// Named scalar variables (each length n).
60    pub scalar_vars: Vec<NamedVec>,
61    /// Tabular data for non-functional variables (n × p).
62    pub tabular: Option<FdMatrix>,
63    /// Column names for tabular data.
64    pub column_names: Option<Vec<String>>,
65
66    // ── Analysis layers ──
67    /// Analysis results keyed by layer type.
68    pub layers: HashMap<LayerKey, Layer>,
69}
70
71/// A named vector of f64 values (e.g., a scalar covariate or response).
72#[derive(Debug, Clone)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74pub struct NamedVec {
75    pub name: String,
76    pub values: Vec<f64>,
77}
78
79/// Named grouping variable with string labels.
80#[derive(Debug, Clone)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct GroupVar {
83    /// Variable name (e.g., "treatment", "sex").
84    pub name: String,
85    /// Per-observation labels (length n).
86    pub labels: Vec<String>,
87    /// Unique labels in order of first appearance.
88    pub unique: Vec<String>,
89}
90
91// ─── Layer Keys & Types ─────────────────────────────────────────────────────
92
93/// Key identifying a layer type.
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96#[non_exhaustive]
97pub enum LayerKey {
98    /// Functional PCA decomposition.
99    Fpca,
100    /// PLS decomposition.
101    Pls,
102    /// Elastic alignment (Karcher mean + warps).
103    Alignment,
104    /// Precomputed n×n distance matrix.
105    Distances,
106    /// Functional depth scores.
107    Depth,
108    /// Outlier detection flags.
109    Outliers,
110    /// Cluster assignments.
111    Clusters,
112    /// Scalar-on-function regression fit.
113    Regression,
114    /// Function-on-scalar regression fit.
115    FunctionOnScalar,
116    /// Tolerance / confidence bands.
117    Tolerance,
118    /// Mean curve.
119    Mean,
120    /// SPM Phase I chart.
121    SpmChart,
122    /// SPM Phase II monitoring result.
123    SpmMonitor,
124    /// Explainability result (SHAP, PDP, etc.).
125    Explain,
126    /// User-defined extension.
127    Custom(String),
128}
129
130/// Analysis result attached to an [`FdaData`].
131#[derive(Debug, Clone)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
133#[non_exhaustive]
134pub enum Layer {
135    Fpca(FpcaLayer),
136    Pls(PlsLayer),
137    Alignment(AlignmentLayer),
138    Distances(DistancesLayer),
139    Depth(DepthLayer),
140    Outliers(OutlierLayer),
141    Clusters(ClusterLayer),
142    Regression(RegressionLayer),
143    FunctionOnScalar(FosrLayer),
144    Tolerance(ToleranceLayer),
145    Mean(MeanLayer),
146    SpmChart(SpmChartLayer),
147    SpmMonitor(SpmMonitorLayer),
148    Explain(ExplainLayer),
149    Custom(CustomLayer),
150}
151
152// ─── Layer Structs ──────────────────────────────────────────────────────────
153
154/// FPCA decomposition.
155#[derive(Debug, Clone)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct FpcaLayer {
158    pub eigenvalues: Vec<f64>,
159    pub variance_explained: Vec<f64>,
160    /// Eigenfunctions (m × ncomp), each column is one eigenfunction.
161    pub eigenfunctions: FdMatrix,
162    /// Scores (n × ncomp).
163    pub scores: FdMatrix,
164    /// Mean function (length m).
165    pub mean: Vec<f64>,
166    /// Integration weights (length m).
167    pub weights: Vec<f64>,
168    pub ncomp: usize,
169}
170
171/// PLS decomposition.
172#[derive(Debug, Clone)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct PlsLayer {
175    /// Weight vectors (m × ncomp).
176    pub weights: FdMatrix,
177    /// Scores (n × ncomp).
178    pub scores: FdMatrix,
179    /// Loadings (m × ncomp).
180    pub loadings: FdMatrix,
181    pub ncomp: usize,
182}
183
184/// Elastic alignment result.
185#[derive(Debug, Clone)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187pub struct AlignmentLayer {
188    /// Aligned curves (n × m).
189    pub aligned: FdMatrix,
190    /// Warping functions (n × m).
191    pub warps: FdMatrix,
192    /// Karcher mean (length m).
193    pub mean: Vec<f64>,
194    /// Mean SRSF (length m).
195    pub mean_srsf: Vec<f64>,
196    /// Optional: number of alignment iterations performed.
197    pub n_iter: Option<usize>,
198    /// Optional: whether the alignment converged.
199    pub converged: Option<bool>,
200}
201
202/// Precomputed n×n distance matrix with method metadata.
203#[derive(Debug, Clone)]
204#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
205pub struct DistancesLayer {
206    /// Symmetric n×n distance matrix.
207    pub dist_mat: FdMatrix,
208    /// Distance method used (e.g., "elastic", "l2", "dtw", "amplitude", "phase").
209    pub method: String,
210}
211
212/// Functional depth scores.
213#[derive(Debug, Clone)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub struct DepthLayer {
216    /// Depth score per observation (length n).
217    pub scores: Vec<f64>,
218    /// Method name.
219    pub method: String,
220}
221
222/// Outlier detection result.
223#[derive(Debug, Clone)]
224#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
225pub struct OutlierLayer {
226    /// Outlier flag per observation (length n).
227    pub flags: Vec<bool>,
228    /// Detection threshold.
229    pub threshold: f64,
230    /// Method name.
231    pub method: String,
232    /// Optional: MEI scores (for outliergram).
233    pub mei: Option<Vec<f64>>,
234    /// Optional: MBD scores (for outliergram).
235    pub mbd: Option<Vec<f64>>,
236    /// Optional: magnitude outlyingness.
237    pub magnitude: Option<Vec<f64>>,
238    /// Optional: shape outlyingness.
239    pub shape: Option<Vec<f64>>,
240    /// Optional: outliergram parabola intercept coefficient.
241    pub outliergram_a0: Option<f64>,
242    /// Optional: outliergram parabola linear coefficient.
243    pub outliergram_a1: Option<f64>,
244    /// Optional: outliergram parabola quadratic coefficient.
245    pub outliergram_a2: Option<f64>,
246}
247
248/// Cluster assignments.
249#[derive(Debug, Clone)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub struct ClusterLayer {
252    /// Cluster label per observation (0-indexed, length n).
253    pub labels: Vec<usize>,
254    /// Number of clusters.
255    pub k: usize,
256    /// Method name.
257    pub method: String,
258    /// Optional: cluster centers (k rows × m cols).
259    pub centers: Option<FdMatrix>,
260    /// Optional: medoid indices (length k).
261    pub medoid_indices: Option<Vec<usize>>,
262    /// Optional: silhouette scores (length n).
263    pub silhouette: Option<Vec<f64>>,
264}
265
266/// Scalar-on-function regression fit.
267#[derive(Debug, Clone)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269pub struct RegressionLayer {
270    /// Method name (e.g., "fregre_lm", "fregre_pls", "fregre_np", "elastic").
271    pub method: String,
272    /// Functional coefficient β(t) (length m). `None` for nonparametric.
273    pub beta_t: Option<Vec<f64>>,
274    /// Fitted values (length n).
275    pub fitted_values: Vec<f64>,
276    /// Residuals (length n).
277    pub residuals: Vec<f64>,
278    /// Observed response (length n).
279    pub observed_y: Vec<f64>,
280    /// R².
281    pub r_squared: f64,
282    /// Adjusted R².
283    pub adj_r_squared: Option<f64>,
284    /// Intercept.
285    pub intercept: f64,
286    /// Number of components used (0 for nonparametric).
287    pub ncomp: usize,
288    /// Evaluation grid for β(t).
289    pub argvals: Option<Vec<f64>>,
290    /// Pointwise standard errors of β(t).
291    pub beta_se: Option<Vec<f64>>,
292    /// Optional: human-readable model name.
293    pub model_name: Option<String>,
294    /// Optional: number of training observations.
295    pub n_obs: Option<usize>,
296    /// Optional: FPCA decomposition used by the model (needed for explain functions).
297    pub fpca: Option<Box<FpcaLayer>>,
298}
299
300/// Function-on-scalar regression fit.
301#[derive(Debug, Clone)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub struct FosrLayer {
304    /// Coefficient functions (p × m), one per predictor.
305    pub coefficients: FdMatrix,
306    /// Fitted curves (n × m).
307    pub fitted: FdMatrix,
308    /// R² per grid point (length m).
309    pub r_squared_t: Vec<f64>,
310}
311
312/// Tolerance / confidence band.
313#[derive(Debug, Clone)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315pub struct ToleranceLayer {
316    /// Lower bound (length m).
317    pub lower: Vec<f64>,
318    /// Upper bound (length m).
319    pub upper: Vec<f64>,
320    /// Center (length m).
321    pub center: Vec<f64>,
322    /// Method name.
323    pub method: String,
324}
325
326/// Mean curve.
327#[derive(Debug, Clone)]
328#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
329pub struct MeanLayer {
330    /// Mean function (length m).
331    pub mean: Vec<f64>,
332}
333
334/// SPM Phase I chart.
335#[derive(Debug, Clone)]
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337pub struct SpmChartLayer {
338    /// T² control limit.
339    pub t2_limit: f64,
340    /// SPE control limit.
341    pub spe_limit: f64,
342    /// Phase I T² statistics.
343    pub t2_stats: Vec<f64>,
344    /// Phase I SPE statistics.
345    pub spe_stats: Vec<f64>,
346    /// Number of FPC components.
347    pub ncomp: usize,
348    /// Significance level.
349    pub alpha: f64,
350    /// Optional: eigenvalues from FPCA (length ncomp).
351    pub eigenvalues: Option<Vec<f64>>,
352    /// Optional: FPCA mean function (length m).
353    pub fpca_mean: Option<Vec<f64>>,
354    /// Optional: FPCA rotation/eigenfunctions (m × ncomp).
355    pub fpca_rotation: Option<FdMatrix>,
356    /// Optional: FPCA integration weights (length m).
357    pub fpca_weights: Option<Vec<f64>>,
358}
359
360/// SPM Phase II monitoring result.
361#[derive(Debug, Clone)]
362#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
363pub struct SpmMonitorLayer {
364    /// T² statistics for new observations.
365    pub t2_stats: Vec<f64>,
366    /// SPE statistics for new observations.
367    pub spe_stats: Vec<f64>,
368    /// T² control limit.
369    pub t2_limit: f64,
370    /// SPE control limit.
371    pub spe_limit: f64,
372    /// T² alarm flags.
373    pub t2_alarms: Vec<bool>,
374    /// SPE alarm flags.
375    pub spe_alarms: Vec<bool>,
376}
377
378/// Extra data for explain layers.
379///
380/// When the `serde` feature is enabled this is [`serde_json::Value`] (arbitrary
381/// JSON). Otherwise it is a flat `HashMap<String, Vec<f64>>`.
382#[cfg(feature = "serde")]
383pub type ExplainExtra = serde_json::Value;
384/// Extra data for explain layers (flat map when `serde` is disabled).
385#[cfg(not(feature = "serde"))]
386pub type ExplainExtra = HashMap<String, Vec<f64>>;
387
388/// Explainability result.
389#[derive(Debug, Clone)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub struct ExplainLayer {
392    /// Method name (e.g., "shap", "pdp", "ale", "permutation_importance").
393    pub method: String,
394    /// Values (interpretation depends on method).
395    pub values: Vec<f64>,
396    /// Labels for the values.
397    pub labels: Vec<String>,
398    /// Additional method-specific data (arbitrary JSON when `serde` feature is
399    /// enabled, flat `HashMap<String, Vec<f64>>` otherwise).
400    pub extra: Option<ExplainExtra>,
401}
402
403/// Custom layer data type.
404///
405/// When the `serde` feature is enabled this is [`serde_json::Value`] (arbitrary
406/// JSON). Otherwise it is a flat `HashMap<String, Vec<f64>>`.
407#[cfg(feature = "serde")]
408pub type CustomData = serde_json::Value;
409/// Custom layer data type (flat map when `serde` is disabled).
410#[cfg(not(feature = "serde"))]
411pub type CustomData = HashMap<String, Vec<f64>>;
412
413/// User-defined layer for extensions.
414#[derive(Debug, Clone)]
415#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
416pub struct CustomLayer {
417    pub name: String,
418    pub data: CustomData,
419}
420
421// ─── FdaData Constructors ───────────────────────────────────────────────────
422
423impl FdaData {
424    /// Create from functional curves + grid.
425    pub fn from_curves(curves: FdMatrix, argvals: Vec<f64>) -> Self {
426        Self {
427            curves: Some(curves),
428            argvals: Some(argvals),
429            grouping: Vec::new(),
430            scalar_vars: Vec::new(),
431            tabular: None,
432            column_names: None,
433            layers: HashMap::new(),
434        }
435    }
436
437    /// Create from tabular (non-functional) data.
438    pub fn from_tabular(tabular: FdMatrix, column_names: Vec<String>) -> Self {
439        Self {
440            curves: None,
441            argvals: None,
442            grouping: Vec::new(),
443            scalar_vars: Vec::new(),
444            tabular: Some(tabular),
445            column_names: Some(column_names),
446            layers: HashMap::new(),
447        }
448    }
449
450    /// Create empty container.
451    pub fn empty() -> Self {
452        Self {
453            curves: None,
454            argvals: None,
455            grouping: Vec::new(),
456            scalar_vars: Vec::new(),
457            tabular: None,
458            column_names: None,
459            layers: HashMap::new(),
460        }
461    }
462
463    // ── Requirement checks ──
464
465    /// Require functional curves to be present.
466    pub fn require_curves(&self) -> Result<(&FdMatrix, &[f64]), String> {
467        match (&self.curves, &self.argvals) {
468            (Some(c), Some(a)) => Ok((c, a)),
469            _ => Err("FdaData requires functional curves + argvals".into()),
470        }
471    }
472
473    /// Require a specific layer to be present.
474    pub fn require_layer(&self, key: &LayerKey) -> Result<&Layer, String> {
475        self.layers
476            .get(key)
477            .ok_or_else(|| format!("FdaData missing required layer: {key:?}"))
478    }
479
480    // ── Layer access ──
481
482    /// Check if a layer is present.
483    pub fn has_layer(&self, key: &LayerKey) -> bool {
484        self.layers.contains_key(key)
485    }
486
487    /// Get a layer by key.
488    pub fn get_layer(&self, key: &LayerKey) -> Option<&Layer> {
489        self.layers.get(key)
490    }
491
492    /// Set (add or replace) a layer.
493    pub fn set_layer(&mut self, key: LayerKey, layer: Layer) {
494        self.layers.insert(key, layer);
495    }
496
497    /// Remove a layer.
498    pub fn remove_layer(&mut self, key: &LayerKey) -> Option<Layer> {
499        self.layers.remove(key)
500    }
501
502    /// List all layer keys present.
503    pub fn layer_keys(&self) -> Vec<&LayerKey> {
504        self.layers.keys().collect()
505    }
506
507    // ── Typed layer accessors ──
508
509    /// Get FPCA layer if present.
510    pub fn fpca(&self) -> Option<&FpcaLayer> {
511        match self.layers.get(&LayerKey::Fpca)? {
512            Layer::Fpca(l) => Some(l),
513            _ => None,
514        }
515    }
516
517    /// Get distances layer if present.
518    pub fn distances(&self) -> Option<&DistancesLayer> {
519        match self.layers.get(&LayerKey::Distances)? {
520            Layer::Distances(l) => Some(l),
521            _ => None,
522        }
523    }
524
525    /// Get alignment layer if present.
526    pub fn alignment(&self) -> Option<&AlignmentLayer> {
527        match self.layers.get(&LayerKey::Alignment)? {
528            Layer::Alignment(l) => Some(l),
529            _ => None,
530        }
531    }
532
533    /// Get regression layer if present.
534    pub fn regression(&self) -> Option<&RegressionLayer> {
535        match self.layers.get(&LayerKey::Regression)? {
536            Layer::Regression(l) => Some(l),
537            _ => None,
538        }
539    }
540
541    /// Get cluster layer if present.
542    pub fn clusters(&self) -> Option<&ClusterLayer> {
543        match self.layers.get(&LayerKey::Clusters)? {
544            Layer::Clusters(l) => Some(l),
545            _ => None,
546        }
547    }
548
549    /// Get depth layer if present.
550    pub fn depth(&self) -> Option<&DepthLayer> {
551        match self.layers.get(&LayerKey::Depth)? {
552            Layer::Depth(l) => Some(l),
553            _ => None,
554        }
555    }
556
557    /// Get outlier layer if present.
558    pub fn outliers(&self) -> Option<&OutlierLayer> {
559        match self.layers.get(&LayerKey::Outliers)? {
560            Layer::Outliers(l) => Some(l),
561            _ => None,
562        }
563    }
564
565    // ── Metadata helpers ──
566
567    /// Number of observations (from curves, tabular, or first scalar var).
568    pub fn n_obs(&self) -> usize {
569        if let Some(c) = &self.curves {
570            return c.nrows();
571        }
572        if let Some(t) = &self.tabular {
573            return t.nrows();
574        }
575        self.scalar_vars.first().map_or(0, |v| v.values.len())
576    }
577
578    /// Number of grid points (0 if no functional data).
579    pub fn n_points(&self) -> usize {
580        self.argvals.as_ref().map_or(0, |a| a.len())
581    }
582
583    /// Add a scalar variable.
584    pub fn add_scalar(&mut self, name: impl Into<String>, values: Vec<f64>) {
585        self.scalar_vars.push(NamedVec {
586            name: name.into(),
587            values,
588        });
589    }
590
591    /// Get a scalar variable by name.
592    pub fn get_scalar(&self, name: &str) -> Option<&[f64]> {
593        self.scalar_vars
594            .iter()
595            .find(|v| v.name == name)
596            .map(|v| v.values.as_slice())
597    }
598
599    /// Add a grouping variable with per-observation string labels.
600    ///
601    /// Unique labels are computed automatically in order of first appearance.
602    pub fn add_grouping(&mut self, name: impl Into<String>, labels: Vec<String>) {
603        let mut unique = Vec::new();
604        for lab in &labels {
605            if !unique.contains(lab) {
606                unique.push(lab.clone());
607            }
608        }
609        self.grouping.push(GroupVar {
610            name: name.into(),
611            labels,
612            unique,
613        });
614    }
615
616    /// Look up a grouping variable by name.
617    pub fn get_grouping(&self, name: &str) -> Option<&GroupVar> {
618        self.grouping.iter().find(|g| g.name == name)
619    }
620}
621
622// ─── From conversions ──────────────────────────────────────────────────────
623
624impl From<&crate::scalar_on_function::FregreLmResult> for RegressionLayer {
625    fn from(fit: &crate::scalar_on_function::FregreLmResult) -> Self {
626        let n_tune = fit.fpca.scores.nrows();
627        let eigenvalues: Vec<f64> = fit
628            .fpca
629            .singular_values
630            .iter()
631            .map(|s| s * s / (n_tune as f64 - 1.0).max(1.0))
632            .collect();
633        let total_var: f64 = eigenvalues.iter().sum();
634        let variance_explained = if total_var > 0.0 {
635            eigenvalues.iter().map(|&ev| ev / total_var).collect()
636        } else {
637            vec![0.0; eigenvalues.len()]
638        };
639
640        let fpca_layer = FpcaLayer {
641            eigenvalues,
642            variance_explained,
643            eigenfunctions: fit.fpca.rotation.clone(),
644            scores: fit.fpca.scores.clone(),
645            mean: fit.fpca.mean.clone(),
646            weights: fit.fpca.weights.clone(),
647            ncomp: fit.ncomp,
648        };
649
650        RegressionLayer {
651            method: "fregre_lm".into(),
652            beta_t: Some(fit.beta_t.clone()),
653            fitted_values: fit.fitted_values.clone(),
654            residuals: fit.residuals.clone(),
655            observed_y: Vec::new(),
656            r_squared: fit.r_squared,
657            adj_r_squared: Some(fit.r_squared_adj),
658            intercept: fit.intercept,
659            ncomp: fit.ncomp,
660            argvals: None,
661            beta_se: Some(fit.beta_se.clone()),
662            model_name: None,
663            n_obs: Some(fit.fitted_values.len()),
664            fpca: Some(Box::new(fpca_layer)),
665        }
666    }
667}
668
669impl From<&crate::scalar_on_function::PlsRegressionResult> for RegressionLayer {
670    fn from(fit: &crate::scalar_on_function::PlsRegressionResult) -> Self {
671        RegressionLayer {
672            method: "fregre_pls".into(),
673            beta_t: Some(fit.beta_t.clone()),
674            fitted_values: fit.fitted_values.clone(),
675            residuals: fit.residuals.clone(),
676            observed_y: Vec::new(),
677            r_squared: fit.r_squared,
678            adj_r_squared: Some(fit.r_squared_adj),
679            intercept: fit.intercept,
680            ncomp: fit.ncomp,
681            argvals: None,
682            beta_se: None,
683            model_name: None,
684            n_obs: Some(fit.fitted_values.len()),
685            fpca: None, // PLS uses a different decomposition; no FPCA layer
686        }
687    }
688}
689
690// ─── Tests ──────────────────────────────────────────────────────────────────
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn from_curves_basic() {
698        let fd = FdaData::from_curves(
699            FdMatrix::zeros(10, 50),
700            (0..50).map(|i| i as f64 / 49.0).collect(),
701        );
702        assert_eq!(fd.n_obs(), 10);
703        assert_eq!(fd.n_points(), 50);
704        assert!(fd.require_curves().is_ok());
705        assert!(!fd.has_layer(&LayerKey::Fpca));
706    }
707
708    #[test]
709    fn add_and_retrieve_layers() {
710        let mut fd = FdaData::from_curves(
711            FdMatrix::zeros(5, 20),
712            (0..20).map(|i| i as f64 / 19.0).collect(),
713        );
714
715        fd.set_layer(
716            LayerKey::Depth,
717            Layer::Depth(DepthLayer {
718                scores: vec![0.5; 5],
719                method: "fraiman_muniz".into(),
720            }),
721        );
722
723        assert!(fd.has_layer(&LayerKey::Depth));
724        assert!(!fd.has_layer(&LayerKey::Fpca));
725        assert!(fd.depth().is_some());
726        assert_eq!(fd.depth().unwrap().scores.len(), 5);
727        assert_eq!(fd.layer_keys().len(), 1);
728    }
729
730    #[test]
731    fn require_missing_layer_errors() {
732        let fd = FdaData::from_curves(FdMatrix::zeros(3, 10), vec![0.0; 10]);
733        assert!(fd.require_layer(&LayerKey::Fpca).is_err());
734    }
735
736    #[test]
737    fn scalar_vars() {
738        let mut fd = FdaData::empty();
739        fd.add_scalar("height", vec![170.0, 180.0, 165.0]);
740        assert_eq!(fd.get_scalar("height").unwrap(), &[170.0, 180.0, 165.0]);
741        assert!(fd.get_scalar("weight").is_none());
742        assert_eq!(fd.n_obs(), 3);
743    }
744
745    #[test]
746    fn multiple_layers_compose() {
747        let mut fd = FdaData::from_curves(FdMatrix::zeros(10, 30), vec![0.0; 30]);
748
749        fd.set_layer(
750            LayerKey::Depth,
751            Layer::Depth(DepthLayer {
752                scores: vec![0.5; 10],
753                method: "fm".into(),
754            }),
755        );
756        fd.set_layer(
757            LayerKey::Outliers,
758            Layer::Outliers(OutlierLayer {
759                flags: vec![false; 10],
760                threshold: 0.1,
761                method: "lrt".into(),
762                mei: None,
763                mbd: None,
764                magnitude: None,
765                shape: None,
766                outliergram_a0: None,
767                outliergram_a1: None,
768                outliergram_a2: None,
769            }),
770        );
771        fd.set_layer(
772            LayerKey::Distances,
773            Layer::Distances(DistancesLayer {
774                dist_mat: FdMatrix::zeros(10, 10),
775                method: "elastic".into(),
776            }),
777        );
778
779        assert_eq!(fd.layer_keys().len(), 3);
780        assert!(fd.depth().is_some());
781        assert!(fd.outliers().is_some());
782        assert!(fd.distances().is_some());
783    }
784
785    #[test]
786    fn regression_layer_from_fregre_lm() {
787        let (n, m) = (20, 30);
788        let data = FdMatrix::from_column_major(
789            (0..n * m)
790                .map(|k| {
791                    let i = (k % n) as f64;
792                    let j = (k / n) as f64;
793                    ((i + 1.0) * j * 0.2).sin()
794                })
795                .collect(),
796            n,
797            m,
798        )
799        .unwrap();
800        let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.5).sin()).collect();
801        let fit = crate::scalar_on_function::fregre_lm(&data, &y, None, 3).unwrap();
802
803        let layer = RegressionLayer::from(&fit);
804        assert_eq!(layer.method, "fregre_lm");
805        assert_eq!(layer.ncomp, 3);
806        assert_eq!(layer.fitted_values.len(), n);
807        assert_eq!(layer.residuals.len(), n);
808        assert!(layer.fpca.is_some());
809        let fpca = layer.fpca.as_ref().unwrap();
810        assert_eq!(fpca.ncomp, 3);
811        assert_eq!(fpca.mean.len(), m);
812        assert_eq!(fpca.eigenfunctions.shape(), (m, 3));
813        assert_eq!(fpca.scores.shape(), (n, 3));
814        assert_eq!(fpca.weights.len(), m);
815        assert_eq!(fpca.eigenvalues.len(), 3);
816        // Variance explained should sum to ~1.0
817        let ve_sum: f64 = fpca.variance_explained.iter().sum();
818        assert!(
819            (ve_sum - 1.0).abs() < 1e-10,
820            "variance_explained sum = {ve_sum}"
821        );
822        assert!(layer.beta_t.is_some());
823        assert!(layer.beta_se.is_some());
824        assert_eq!(layer.n_obs, Some(n));
825        assert!((layer.r_squared - fit.r_squared).abs() < 1e-14);
826    }
827
828    #[test]
829    fn regression_layer_from_pls() {
830        let n = 30;
831        let m = 50;
832        let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
833        let vals: Vec<f64> = (0..n)
834            .flat_map(|i| {
835                t.iter()
836                    .map(move |&tj| (2.0 * std::f64::consts::PI * tj).sin() + 0.1 * i as f64)
837            })
838            .collect();
839        let data = FdMatrix::from_column_major(vals, n, m).unwrap();
840        let y: Vec<f64> = (0..n).map(|i| 2.0 + 0.5 * i as f64).collect();
841
842        let fit = crate::scalar_on_function::fregre_pls(&data, &y, &t, 3, None).unwrap();
843
844        let layer = RegressionLayer::from(&fit);
845        assert_eq!(layer.method, "fregre_pls");
846        assert_eq!(layer.ncomp, 3);
847        assert_eq!(layer.fitted_values.len(), n);
848        assert!(layer.fpca.is_none()); // PLS has no FPCA decomposition
849        assert!(layer.beta_t.is_some());
850        assert!(layer.beta_se.is_none()); // PLS has no beta SE
851        assert_eq!(layer.n_obs, Some(n));
852        assert!((layer.r_squared - fit.r_squared).abs() < 1e-14);
853    }
854}