1use crate::matrix::FdMatrix;
39use std::collections::HashMap;
40
41#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct FdaData {
50 pub curves: Option<FdMatrix>,
53 pub argvals: Option<Vec<f64>>,
55
56 pub grouping: Vec<GroupVar>,
59 pub scalar_vars: Vec<NamedVec>,
61 pub tabular: Option<FdMatrix>,
63 pub column_names: Option<Vec<String>>,
65
66 pub layers: HashMap<LayerKey, Layer>,
69}
70
71#[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#[derive(Debug, Clone)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct GroupVar {
83 pub name: String,
85 pub labels: Vec<String>,
87 pub unique: Vec<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96#[non_exhaustive]
97pub enum LayerKey {
98 Fpca,
100 Pls,
102 Alignment,
104 Distances,
106 Depth,
108 Outliers,
110 Clusters,
112 Regression,
114 FunctionOnScalar,
116 Tolerance,
118 Mean,
120 SpmChart,
122 SpmMonitor,
124 Explain,
126 Custom(String),
128}
129
130#[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#[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 pub eigenfunctions: FdMatrix,
162 pub scores: FdMatrix,
164 pub mean: Vec<f64>,
166 pub weights: Vec<f64>,
168 pub ncomp: usize,
169}
170
171#[derive(Debug, Clone)]
173#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
174pub struct PlsLayer {
175 pub weights: FdMatrix,
177 pub scores: FdMatrix,
179 pub loadings: FdMatrix,
181 pub ncomp: usize,
182}
183
184#[derive(Debug, Clone)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187pub struct AlignmentLayer {
188 pub aligned: FdMatrix,
190 pub warps: FdMatrix,
192 pub mean: Vec<f64>,
194 pub mean_srsf: Vec<f64>,
196 pub n_iter: Option<usize>,
198 pub converged: Option<bool>,
200}
201
202#[derive(Debug, Clone)]
204#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
205pub struct DistancesLayer {
206 pub dist_mat: FdMatrix,
208 pub method: String,
210}
211
212#[derive(Debug, Clone)]
214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
215pub struct DepthLayer {
216 pub scores: Vec<f64>,
218 pub method: String,
220}
221
222#[derive(Debug, Clone)]
224#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
225pub struct OutlierLayer {
226 pub flags: Vec<bool>,
228 pub threshold: f64,
230 pub method: String,
232 pub mei: Option<Vec<f64>>,
234 pub mbd: Option<Vec<f64>>,
236 pub magnitude: Option<Vec<f64>>,
238 pub shape: Option<Vec<f64>>,
240 pub outliergram_a0: Option<f64>,
242 pub outliergram_a1: Option<f64>,
244 pub outliergram_a2: Option<f64>,
246}
247
248#[derive(Debug, Clone)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub struct ClusterLayer {
252 pub labels: Vec<usize>,
254 pub k: usize,
256 pub method: String,
258 pub centers: Option<FdMatrix>,
260 pub medoid_indices: Option<Vec<usize>>,
262 pub silhouette: Option<Vec<f64>>,
264}
265
266#[derive(Debug, Clone)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269pub struct RegressionLayer {
270 pub method: String,
272 pub beta_t: Option<Vec<f64>>,
274 pub fitted_values: Vec<f64>,
276 pub residuals: Vec<f64>,
278 pub observed_y: Vec<f64>,
280 pub r_squared: f64,
282 pub adj_r_squared: Option<f64>,
284 pub intercept: f64,
286 pub ncomp: usize,
288 pub argvals: Option<Vec<f64>>,
290 pub beta_se: Option<Vec<f64>>,
292 pub model_name: Option<String>,
294 pub n_obs: Option<usize>,
296 pub fpca: Option<Box<FpcaLayer>>,
298}
299
300#[derive(Debug, Clone)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub struct FosrLayer {
304 pub coefficients: FdMatrix,
306 pub fitted: FdMatrix,
308 pub r_squared_t: Vec<f64>,
310}
311
312#[derive(Debug, Clone)]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315pub struct ToleranceLayer {
316 pub lower: Vec<f64>,
318 pub upper: Vec<f64>,
320 pub center: Vec<f64>,
322 pub method: String,
324}
325
326#[derive(Debug, Clone)]
328#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
329pub struct MeanLayer {
330 pub mean: Vec<f64>,
332}
333
334#[derive(Debug, Clone)]
336#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
337pub struct SpmChartLayer {
338 pub t2_limit: f64,
340 pub spe_limit: f64,
342 pub t2_stats: Vec<f64>,
344 pub spe_stats: Vec<f64>,
346 pub ncomp: usize,
348 pub alpha: f64,
350 pub eigenvalues: Option<Vec<f64>>,
352 pub fpca_mean: Option<Vec<f64>>,
354 pub fpca_rotation: Option<FdMatrix>,
356 pub fpca_weights: Option<Vec<f64>>,
358}
359
360#[derive(Debug, Clone)]
362#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
363pub struct SpmMonitorLayer {
364 pub t2_stats: Vec<f64>,
366 pub spe_stats: Vec<f64>,
368 pub t2_limit: f64,
370 pub spe_limit: f64,
372 pub t2_alarms: Vec<bool>,
374 pub spe_alarms: Vec<bool>,
376}
377
378#[cfg(feature = "serde")]
383pub type ExplainExtra = serde_json::Value;
384#[cfg(not(feature = "serde"))]
386pub type ExplainExtra = HashMap<String, Vec<f64>>;
387
388#[derive(Debug, Clone)]
390#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391pub struct ExplainLayer {
392 pub method: String,
394 pub values: Vec<f64>,
396 pub labels: Vec<String>,
398 pub extra: Option<ExplainExtra>,
401}
402
403#[cfg(feature = "serde")]
408pub type CustomData = serde_json::Value;
409#[cfg(not(feature = "serde"))]
411pub type CustomData = HashMap<String, Vec<f64>>;
412
413#[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
421impl FdaData {
424 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 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 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 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 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 pub fn has_layer(&self, key: &LayerKey) -> bool {
484 self.layers.contains_key(key)
485 }
486
487 pub fn get_layer(&self, key: &LayerKey) -> Option<&Layer> {
489 self.layers.get(key)
490 }
491
492 pub fn set_layer(&mut self, key: LayerKey, layer: Layer) {
494 self.layers.insert(key, layer);
495 }
496
497 pub fn remove_layer(&mut self, key: &LayerKey) -> Option<Layer> {
499 self.layers.remove(key)
500 }
501
502 pub fn layer_keys(&self) -> Vec<&LayerKey> {
504 self.layers.keys().collect()
505 }
506
507 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 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 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 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 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 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 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 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 pub fn n_points(&self) -> usize {
580 self.argvals.as_ref().map_or(0, |a| a.len())
581 }
582
583 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 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 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 pub fn get_grouping(&self, name: &str) -> Option<&GroupVar> {
618 self.grouping.iter().find(|g| g.name == name)
619 }
620}
621
622impl 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, }
687 }
688}
689
690#[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 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()); assert!(layer.beta_t.is_some());
850 assert!(layer.beta_se.is_none()); assert_eq!(layer.n_obs, Some(n));
852 assert!((layer.r_squared - fit.r_squared).abs() < 1e-14);
853 }
854}