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
202impl AlignmentLayer {
203 pub fn to_karcher_mean_result(&self) -> crate::alignment::KarcherMeanResult {
209 crate::alignment::KarcherMeanResult {
210 mean: self.mean.clone(),
211 mean_srsf: self.mean_srsf.clone(),
212 gammas: self.warps.clone(),
213 aligned_data: self.aligned.clone(),
214 n_iter: self.n_iter.unwrap_or(0),
215 converged: self.converged.unwrap_or(true),
216 aligned_srsfs: None,
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct DistancesLayer {
225 pub dist_mat: FdMatrix,
227 pub method: String,
229}
230
231#[derive(Debug, Clone)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
234pub struct DepthLayer {
235 pub scores: Vec<f64>,
237 pub method: String,
239}
240
241#[derive(Debug, Clone)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244pub struct OutlierLayer {
245 pub flags: Vec<bool>,
247 pub threshold: f64,
249 pub method: String,
251 pub mei: Option<Vec<f64>>,
253 pub mbd: Option<Vec<f64>>,
255 pub magnitude: Option<Vec<f64>>,
257 pub shape: Option<Vec<f64>>,
259 pub outliergram_a0: Option<f64>,
261 pub outliergram_a1: Option<f64>,
263 pub outliergram_a2: Option<f64>,
265}
266
267#[derive(Debug, Clone)]
269#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
270pub struct ClusterLayer {
271 pub labels: Vec<usize>,
273 pub k: usize,
275 pub method: String,
277 pub centers: Option<FdMatrix>,
279 pub medoid_indices: Option<Vec<usize>>,
281 pub silhouette: Option<Vec<f64>>,
283}
284
285#[derive(Debug, Clone)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288pub struct RegressionLayer {
289 pub method: String,
291 pub beta_t: Option<Vec<f64>>,
293 pub fitted_values: Vec<f64>,
295 pub residuals: Vec<f64>,
297 pub observed_y: Vec<f64>,
299 pub r_squared: f64,
301 pub adj_r_squared: Option<f64>,
303 pub intercept: f64,
305 pub ncomp: usize,
307 pub argvals: Option<Vec<f64>>,
309 pub beta_se: Option<Vec<f64>>,
311 pub model_name: Option<String>,
313 pub n_obs: Option<usize>,
315 pub fpca: Option<Box<FpcaLayer>>,
317 #[cfg(feature = "serde")]
319 pub selection_extra: Option<serde_json::Value>,
320 #[cfg(not(feature = "serde"))]
322 pub selection_extra: Option<HashMap<String, Vec<f64>>>,
323}
324
325#[derive(Debug, Clone)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328pub struct FosrLayer {
329 pub coefficients: FdMatrix,
331 pub fitted: FdMatrix,
333 pub r_squared_t: Vec<f64>,
335}
336
337#[derive(Debug, Clone)]
339#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
340pub struct ToleranceLayer {
341 pub lower: Vec<f64>,
343 pub upper: Vec<f64>,
345 pub center: Vec<f64>,
347 pub method: String,
349}
350
351#[derive(Debug, Clone)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354pub struct MeanLayer {
355 pub mean: Vec<f64>,
357}
358
359#[derive(Debug, Clone)]
361#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
362pub struct SpmChartLayer {
363 pub t2_limit: f64,
365 pub spe_limit: f64,
367 pub t2_stats: Vec<f64>,
369 pub spe_stats: Vec<f64>,
371 pub ncomp: usize,
373 pub alpha: f64,
375 pub eigenvalues: Option<Vec<f64>>,
377 pub fpca_mean: Option<Vec<f64>>,
379 pub fpca_rotation: Option<FdMatrix>,
381 pub fpca_weights: Option<Vec<f64>>,
383}
384
385impl SpmChartLayer {
386 pub fn from_chart(chart: &crate::spm::SpmChart) -> Self {
388 Self {
389 t2_limit: chart.t2_limit.ucl,
390 spe_limit: chart.spe_limit.ucl,
391 t2_stats: chart.t2_phase1.clone(),
392 spe_stats: chart.spe_phase1.clone(),
393 ncomp: chart.eigenvalues.len(),
394 alpha: chart.config.alpha,
395 eigenvalues: Some(chart.eigenvalues.clone()),
396 fpca_mean: Some(chart.fpca.mean.clone()),
397 fpca_rotation: Some(chart.fpca.rotation.clone()),
398 fpca_weights: Some(chart.fpca.weights.clone()),
399 }
400 }
401
402 pub fn can_monitor(&self) -> bool {
404 self.eigenvalues.is_some()
405 && self.fpca_mean.is_some()
406 && self.fpca_rotation.is_some()
407 && self.fpca_weights.is_some()
408 }
409}
410
411#[derive(Debug, Clone)]
413#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
414pub struct SpmMonitorLayer {
415 pub t2_stats: Vec<f64>,
417 pub spe_stats: Vec<f64>,
419 pub t2_limit: f64,
421 pub spe_limit: f64,
423 pub t2_alarms: Vec<bool>,
425 pub spe_alarms: Vec<bool>,
427}
428
429#[cfg(feature = "serde")]
434pub type ExplainExtra = serde_json::Value;
435#[cfg(not(feature = "serde"))]
437pub type ExplainExtra = HashMap<String, Vec<f64>>;
438
439#[derive(Debug, Clone)]
441#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
442pub struct ExplainLayer {
443 pub method: String,
445 pub values: Vec<f64>,
447 pub labels: Vec<String>,
449 pub extra: Option<ExplainExtra>,
452}
453
454#[cfg(feature = "serde")]
459pub type CustomData = serde_json::Value;
460#[cfg(not(feature = "serde"))]
462pub type CustomData = HashMap<String, Vec<f64>>;
463
464#[derive(Debug, Clone)]
466#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
467pub struct CustomLayer {
468 pub name: String,
469 pub data: CustomData,
470}
471
472impl FdaData {
475 pub fn from_curves(curves: FdMatrix, argvals: Vec<f64>) -> Self {
477 Self {
478 curves: Some(curves),
479 argvals: Some(argvals),
480 grouping: Vec::new(),
481 scalar_vars: Vec::new(),
482 tabular: None,
483 column_names: None,
484 layers: HashMap::new(),
485 }
486 }
487
488 pub fn from_tabular(tabular: FdMatrix, column_names: Vec<String>) -> Self {
490 Self {
491 curves: None,
492 argvals: None,
493 grouping: Vec::new(),
494 scalar_vars: Vec::new(),
495 tabular: Some(tabular),
496 column_names: Some(column_names),
497 layers: HashMap::new(),
498 }
499 }
500
501 pub fn empty() -> Self {
503 Self {
504 curves: None,
505 argvals: None,
506 grouping: Vec::new(),
507 scalar_vars: Vec::new(),
508 tabular: None,
509 column_names: None,
510 layers: HashMap::new(),
511 }
512 }
513
514 pub fn require_curves(&self) -> Result<(&FdMatrix, &[f64]), String> {
518 match (&self.curves, &self.argvals) {
519 (Some(c), Some(a)) => Ok((c, a)),
520 _ => Err("FdaData requires functional curves + argvals".into()),
521 }
522 }
523
524 pub fn require_layer(&self, key: &LayerKey) -> Result<&Layer, String> {
526 self.layers
527 .get(key)
528 .ok_or_else(|| format!("FdaData missing required layer: {key:?}"))
529 }
530
531 pub fn has_layer(&self, key: &LayerKey) -> bool {
535 self.layers.contains_key(key)
536 }
537
538 pub fn get_layer(&self, key: &LayerKey) -> Option<&Layer> {
540 self.layers.get(key)
541 }
542
543 pub fn set_layer(&mut self, key: LayerKey, layer: Layer) {
545 self.layers.insert(key, layer);
546 }
547
548 pub fn remove_layer(&mut self, key: &LayerKey) -> Option<Layer> {
550 self.layers.remove(key)
551 }
552
553 pub fn layer_keys(&self) -> Vec<&LayerKey> {
555 self.layers.keys().collect()
556 }
557
558 pub fn fpca(&self) -> Option<&FpcaLayer> {
562 match self.layers.get(&LayerKey::Fpca)? {
563 Layer::Fpca(l) => Some(l),
564 _ => None,
565 }
566 }
567
568 pub fn distances(&self) -> Option<&DistancesLayer> {
570 match self.layers.get(&LayerKey::Distances)? {
571 Layer::Distances(l) => Some(l),
572 _ => None,
573 }
574 }
575
576 pub fn alignment(&self) -> Option<&AlignmentLayer> {
578 match self.layers.get(&LayerKey::Alignment)? {
579 Layer::Alignment(l) => Some(l),
580 _ => None,
581 }
582 }
583
584 pub fn regression(&self) -> Option<&RegressionLayer> {
586 match self.layers.get(&LayerKey::Regression)? {
587 Layer::Regression(l) => Some(l),
588 _ => None,
589 }
590 }
591
592 pub fn clusters(&self) -> Option<&ClusterLayer> {
594 match self.layers.get(&LayerKey::Clusters)? {
595 Layer::Clusters(l) => Some(l),
596 _ => None,
597 }
598 }
599
600 pub fn depth(&self) -> Option<&DepthLayer> {
602 match self.layers.get(&LayerKey::Depth)? {
603 Layer::Depth(l) => Some(l),
604 _ => None,
605 }
606 }
607
608 pub fn outliers(&self) -> Option<&OutlierLayer> {
610 match self.layers.get(&LayerKey::Outliers)? {
611 Layer::Outliers(l) => Some(l),
612 _ => None,
613 }
614 }
615
616 pub fn n_obs(&self) -> usize {
620 if let Some(c) = &self.curves {
621 return c.nrows();
622 }
623 if let Some(t) = &self.tabular {
624 return t.nrows();
625 }
626 self.scalar_vars.first().map_or(0, |v| v.values.len())
627 }
628
629 pub fn n_points(&self) -> usize {
631 self.argvals.as_ref().map_or(0, |a| a.len())
632 }
633
634 pub fn add_scalar(&mut self, name: impl Into<String>, values: Vec<f64>) {
636 self.scalar_vars.push(NamedVec {
637 name: name.into(),
638 values,
639 });
640 }
641
642 pub fn get_scalar(&self, name: &str) -> Option<&[f64]> {
644 self.scalar_vars
645 .iter()
646 .find(|v| v.name == name)
647 .map(|v| v.values.as_slice())
648 }
649
650 pub fn add_grouping(&mut self, name: impl Into<String>, labels: Vec<String>) {
654 let mut unique = Vec::new();
655 for lab in &labels {
656 if !unique.contains(lab) {
657 unique.push(lab.clone());
658 }
659 }
660 self.grouping.push(GroupVar {
661 name: name.into(),
662 labels,
663 unique,
664 });
665 }
666
667 pub fn get_grouping(&self, name: &str) -> Option<&GroupVar> {
669 self.grouping.iter().find(|g| g.name == name)
670 }
671}
672
673impl From<&crate::scalar_on_function::FregreLmResult> for RegressionLayer {
676 fn from(fit: &crate::scalar_on_function::FregreLmResult) -> Self {
677 let n_tune = fit.fpca.scores.nrows();
678 let eigenvalues: Vec<f64> = fit
679 .fpca
680 .singular_values
681 .iter()
682 .map(|s| s * s / (n_tune as f64 - 1.0).max(1.0))
683 .collect();
684 let total_var: f64 = eigenvalues.iter().sum();
685 let variance_explained = if total_var > 0.0 {
686 eigenvalues.iter().map(|&ev| ev / total_var).collect()
687 } else {
688 vec![0.0; eigenvalues.len()]
689 };
690
691 let fpca_layer = FpcaLayer {
692 eigenvalues,
693 variance_explained,
694 eigenfunctions: fit.fpca.rotation.clone(),
695 scores: fit.fpca.scores.clone(),
696 mean: fit.fpca.mean.clone(),
697 weights: fit.fpca.weights.clone(),
698 ncomp: fit.ncomp,
699 };
700
701 RegressionLayer {
702 method: "fregre_lm".into(),
703 beta_t: Some(fit.beta_t.clone()),
704 fitted_values: fit.fitted_values.clone(),
705 residuals: fit.residuals.clone(),
706 observed_y: Vec::new(),
707 r_squared: fit.r_squared,
708 adj_r_squared: Some(fit.r_squared_adj),
709 intercept: fit.intercept,
710 ncomp: fit.ncomp,
711 argvals: None,
712 beta_se: Some(fit.beta_se.clone()),
713 model_name: None,
714 n_obs: Some(fit.fitted_values.len()),
715 fpca: Some(Box::new(fpca_layer)),
716 selection_extra: None,
717 }
718 }
719}
720
721impl From<&crate::scalar_on_function::PlsRegressionResult> for RegressionLayer {
722 fn from(fit: &crate::scalar_on_function::PlsRegressionResult) -> Self {
723 RegressionLayer {
724 method: "fregre_pls".into(),
725 beta_t: Some(fit.beta_t.clone()),
726 fitted_values: fit.fitted_values.clone(),
727 residuals: fit.residuals.clone(),
728 observed_y: Vec::new(),
729 r_squared: fit.r_squared,
730 adj_r_squared: Some(fit.r_squared_adj),
731 intercept: fit.intercept,
732 ncomp: fit.ncomp,
733 argvals: None,
734 beta_se: None,
735 model_name: None,
736 n_obs: Some(fit.fitted_values.len()),
737 fpca: None, selection_extra: None,
739 }
740 }
741}
742
743#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
750 fn from_curves_basic() {
751 let fd = FdaData::from_curves(
752 FdMatrix::zeros(10, 50),
753 (0..50).map(|i| i as f64 / 49.0).collect(),
754 );
755 assert_eq!(fd.n_obs(), 10);
756 assert_eq!(fd.n_points(), 50);
757 assert!(fd.require_curves().is_ok());
758 assert!(!fd.has_layer(&LayerKey::Fpca));
759 }
760
761 #[test]
762 fn add_and_retrieve_layers() {
763 let mut fd = FdaData::from_curves(
764 FdMatrix::zeros(5, 20),
765 (0..20).map(|i| i as f64 / 19.0).collect(),
766 );
767
768 fd.set_layer(
769 LayerKey::Depth,
770 Layer::Depth(DepthLayer {
771 scores: vec![0.5; 5],
772 method: "fraiman_muniz".into(),
773 }),
774 );
775
776 assert!(fd.has_layer(&LayerKey::Depth));
777 assert!(!fd.has_layer(&LayerKey::Fpca));
778 assert!(fd.depth().is_some());
779 assert_eq!(fd.depth().unwrap().scores.len(), 5);
780 assert_eq!(fd.layer_keys().len(), 1);
781 }
782
783 #[test]
784 fn require_missing_layer_errors() {
785 let fd = FdaData::from_curves(FdMatrix::zeros(3, 10), vec![0.0; 10]);
786 assert!(fd.require_layer(&LayerKey::Fpca).is_err());
787 }
788
789 #[test]
790 fn scalar_vars() {
791 let mut fd = FdaData::empty();
792 fd.add_scalar("height", vec![170.0, 180.0, 165.0]);
793 assert_eq!(fd.get_scalar("height").unwrap(), &[170.0, 180.0, 165.0]);
794 assert!(fd.get_scalar("weight").is_none());
795 assert_eq!(fd.n_obs(), 3);
796 }
797
798 #[test]
799 fn multiple_layers_compose() {
800 let mut fd = FdaData::from_curves(FdMatrix::zeros(10, 30), vec![0.0; 30]);
801
802 fd.set_layer(
803 LayerKey::Depth,
804 Layer::Depth(DepthLayer {
805 scores: vec![0.5; 10],
806 method: "fm".into(),
807 }),
808 );
809 fd.set_layer(
810 LayerKey::Outliers,
811 Layer::Outliers(OutlierLayer {
812 flags: vec![false; 10],
813 threshold: 0.1,
814 method: "lrt".into(),
815 mei: None,
816 mbd: None,
817 magnitude: None,
818 shape: None,
819 outliergram_a0: None,
820 outliergram_a1: None,
821 outliergram_a2: None,
822 }),
823 );
824 fd.set_layer(
825 LayerKey::Distances,
826 Layer::Distances(DistancesLayer {
827 dist_mat: FdMatrix::zeros(10, 10),
828 method: "elastic".into(),
829 }),
830 );
831
832 assert_eq!(fd.layer_keys().len(), 3);
833 assert!(fd.depth().is_some());
834 assert!(fd.outliers().is_some());
835 assert!(fd.distances().is_some());
836 }
837
838 #[test]
839 fn regression_layer_from_fregre_lm() {
840 let (n, m) = (20, 30);
841 let data = FdMatrix::from_column_major(
842 (0..n * m)
843 .map(|k| {
844 let i = (k % n) as f64;
845 let j = (k / n) as f64;
846 ((i + 1.0) * j * 0.2).sin()
847 })
848 .collect(),
849 n,
850 m,
851 )
852 .unwrap();
853 let y: Vec<f64> = (0..n).map(|i| (i as f64 * 0.5).sin()).collect();
854 let fit = crate::scalar_on_function::fregre_lm(&data, &y, None, 3).unwrap();
855
856 let layer = RegressionLayer::from(&fit);
857 assert_eq!(layer.method, "fregre_lm");
858 assert_eq!(layer.ncomp, 3);
859 assert_eq!(layer.fitted_values.len(), n);
860 assert_eq!(layer.residuals.len(), n);
861 assert!(layer.fpca.is_some());
862 let fpca = layer.fpca.as_ref().unwrap();
863 assert_eq!(fpca.ncomp, 3);
864 assert_eq!(fpca.mean.len(), m);
865 assert_eq!(fpca.eigenfunctions.shape(), (m, 3));
866 assert_eq!(fpca.scores.shape(), (n, 3));
867 assert_eq!(fpca.weights.len(), m);
868 assert_eq!(fpca.eigenvalues.len(), 3);
869 let ve_sum: f64 = fpca.variance_explained.iter().sum();
871 assert!(
872 (ve_sum - 1.0).abs() < 1e-10,
873 "variance_explained sum = {ve_sum}"
874 );
875 assert!(layer.beta_t.is_some());
876 assert!(layer.beta_se.is_some());
877 assert_eq!(layer.n_obs, Some(n));
878 assert!((layer.r_squared - fit.r_squared).abs() < 1e-14);
879 }
880
881 #[test]
882 fn regression_layer_from_pls() {
883 let n = 30;
884 let m = 50;
885 let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
886 let vals: Vec<f64> = (0..n)
887 .flat_map(|i| {
888 t.iter()
889 .map(move |&tj| (2.0 * std::f64::consts::PI * tj).sin() + 0.1 * i as f64)
890 })
891 .collect();
892 let data = FdMatrix::from_column_major(vals, n, m).unwrap();
893 let y: Vec<f64> = (0..n).map(|i| 2.0 + 0.5 * i as f64).collect();
894
895 let fit = crate::scalar_on_function::fregre_pls(&data, &y, &t, 3, None).unwrap();
896
897 let layer = RegressionLayer::from(&fit);
898 assert_eq!(layer.method, "fregre_pls");
899 assert_eq!(layer.ncomp, 3);
900 assert_eq!(layer.fitted_values.len(), n);
901 assert!(layer.fpca.is_none()); assert!(layer.beta_t.is_some());
903 assert!(layer.beta_se.is_none()); assert_eq!(layer.n_obs, Some(n));
905 assert!((layer.r_squared - fit.r_squared).abs() < 1e-14);
906 }
907}