Skip to main content

scirs2_stats/functional/
types.rs

1//! Types for functional data analysis and functional regression.
2//!
3//! This module provides core data structures for representing functional data
4//! (curves observed on a common grid), basis function specifications, and
5//! results from functional PCA and regression.
6
7use scirs2_core::ndarray::{Array1, Array2};
8
9/// Functional data: a collection of curves observed on a common grid.
10///
11/// Each observation is a function sampled at the same grid points.
12/// For example, temperature curves measured hourly over multiple days,
13/// or growth curves of different subjects measured at the same ages.
14#[derive(Debug, Clone)]
15pub struct FunctionalData {
16    /// Common evaluation grid (sorted, length T)
17    pub grid: Vec<f64>,
18    /// Observed curves: `observations[i]` is the i-th curve, each of length T
19    pub observations: Vec<Vec<f64>>,
20}
21
22impl FunctionalData {
23    /// Create new functional data, validating dimensions.
24    ///
25    /// # Errors
26    /// Returns an error if:
27    /// - `grid` is empty
28    /// - `observations` is empty
29    /// - Any observation has a different length than `grid`
30    pub fn new(grid: Vec<f64>, observations: Vec<Vec<f64>>) -> crate::error::StatsResult<Self> {
31        if grid.is_empty() {
32            return Err(crate::error::StatsError::InvalidInput(
33                "Grid must not be empty".to_string(),
34            ));
35        }
36        if observations.is_empty() {
37            return Err(crate::error::StatsError::InvalidInput(
38                "Observations must not be empty".to_string(),
39            ));
40        }
41        let t = grid.len();
42        for (i, obs) in observations.iter().enumerate() {
43            if obs.len() != t {
44                return Err(crate::error::StatsError::DimensionMismatch(format!(
45                    "Observation {} has length {}, expected {} (grid length)",
46                    i,
47                    obs.len(),
48                    t
49                )));
50            }
51        }
52        Ok(Self { grid, observations })
53    }
54
55    /// Number of curves (observations).
56    pub fn n_curves(&self) -> usize {
57        self.observations.len()
58    }
59
60    /// Number of grid points.
61    pub fn n_grid(&self) -> usize {
62        self.grid.len()
63    }
64}
65
66/// Type of basis functions for representing functional data.
67#[derive(Debug, Clone)]
68#[non_exhaustive]
69pub enum BasisType {
70    /// B-spline basis with specified number of basis functions and polynomial degree.
71    BSpline {
72        /// Number of basis functions
73        n_basis: usize,
74        /// Polynomial degree (typically 3 for cubic)
75        degree: usize,
76    },
77    /// Fourier basis (sin/cos pairs) with specified number of basis functions.
78    /// `n_basis` should be odd: 1 constant + pairs of (sin, cos).
79    Fourier {
80        /// Number of basis functions (should be odd for complete pairs)
81        n_basis: usize,
82    },
83    /// Polynomial basis: 1, t, t^2, ..., t^degree
84    Polynomial {
85        /// Maximum polynomial degree
86        degree: usize,
87    },
88}
89
90/// Configuration for functional data analysis.
91#[derive(Debug, Clone)]
92pub struct FunctionalConfig {
93    /// Basis function type
94    pub basis: BasisType,
95    /// Smoothing parameter (lambda). If None, selected by GCV.
96    pub smoothing_param: Option<f64>,
97    /// Number of principal components to retain
98    pub n_components: usize,
99}
100
101impl Default for FunctionalConfig {
102    fn default() -> Self {
103        Self {
104            basis: BasisType::BSpline {
105                n_basis: 15,
106                degree: 3,
107            },
108            smoothing_param: None,
109            n_components: 3,
110        }
111    }
112}
113
114/// Result of functional PCA.
115#[derive(Debug, Clone)]
116pub struct FPCAResult {
117    /// Eigenvalues in descending order (length = n_components)
118    pub eigenvalues: Array1<f64>,
119    /// Eigenfunctions evaluated on the grid: rows = components, cols = grid points
120    pub eigenfunctions: Array2<f64>,
121    /// Scores: `scores[[i, k]]` = score of curve i on component k
122    pub scores: Array2<f64>,
123    /// Fraction of variance explained by each component
124    pub variance_explained: Array1<f64>,
125    /// The grid on which eigenfunctions are evaluated
126    pub grid: Vec<f64>,
127}
128
129/// Result of scalar-on-function regression.
130#[derive(Debug, Clone)]
131pub struct SoFResult {
132    /// Estimated coefficient function beta(t) evaluated on the grid
133    pub beta: Array1<f64>,
134    /// Intercept
135    pub intercept: f64,
136    /// Basis coefficients of beta
137    pub beta_coefficients: Array1<f64>,
138    /// The basis type used
139    pub basis: BasisType,
140    /// The grid
141    pub grid: Vec<f64>,
142    /// Smoothing parameter used
143    pub lambda: f64,
144    /// Fitted values
145    pub fitted_values: Array1<f64>,
146    /// R-squared
147    pub r_squared: f64,
148}
149
150/// Result of function-on-function regression.
151#[derive(Debug, Clone)]
152pub struct FoFResult {
153    /// Estimated bivariate coefficient function beta(s,t)
154    /// Shape: (n_grid_s, n_grid_t)
155    pub beta_surface: Array2<f64>,
156    /// Basis coefficients (vectorized)
157    pub beta_coefficients: Array1<f64>,
158    /// Predictor basis type
159    pub predictor_basis: BasisType,
160    /// Response basis type
161    pub response_basis: BasisType,
162    /// Predictor grid
163    pub predictor_grid: Vec<f64>,
164    /// Response grid
165    pub response_grid: Vec<f64>,
166    /// Smoothing parameter used
167    pub lambda: f64,
168    /// Fitted curves: each row is a fitted response curve
169    pub fitted_curves: Array2<f64>,
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_functional_data_valid() {
178        let grid = vec![0.0, 0.5, 1.0];
179        let obs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
180        let data = FunctionalData::new(grid, obs).expect("Should succeed");
181        assert_eq!(data.n_curves(), 2);
182        assert_eq!(data.n_grid(), 3);
183    }
184
185    #[test]
186    fn test_functional_data_empty_grid() {
187        let result = FunctionalData::new(vec![], vec![vec![1.0]]);
188        assert!(result.is_err());
189    }
190
191    #[test]
192    fn test_functional_data_empty_observations() {
193        let result = FunctionalData::new(vec![0.0, 1.0], vec![]);
194        assert!(result.is_err());
195    }
196
197    #[test]
198    fn test_functional_data_dimension_mismatch() {
199        let result = FunctionalData::new(vec![0.0, 1.0], vec![vec![1.0, 2.0, 3.0]]);
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn test_functional_config_default() {
205        let config = FunctionalConfig::default();
206        assert!(config.smoothing_param.is_none());
207        assert_eq!(config.n_components, 3);
208        match &config.basis {
209            BasisType::BSpline { n_basis, degree } => {
210                assert_eq!(*n_basis, 15);
211                assert_eq!(*degree, 3);
212            }
213            _ => panic!("Default should be BSpline"),
214        }
215    }
216}