use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone)]
pub struct FunctionalData {
pub grid: Vec<f64>,
pub observations: Vec<Vec<f64>>,
}
impl FunctionalData {
pub fn new(grid: Vec<f64>, observations: Vec<Vec<f64>>) -> crate::error::StatsResult<Self> {
if grid.is_empty() {
return Err(crate::error::StatsError::InvalidInput(
"Grid must not be empty".to_string(),
));
}
if observations.is_empty() {
return Err(crate::error::StatsError::InvalidInput(
"Observations must not be empty".to_string(),
));
}
let t = grid.len();
for (i, obs) in observations.iter().enumerate() {
if obs.len() != t {
return Err(crate::error::StatsError::DimensionMismatch(format!(
"Observation {} has length {}, expected {} (grid length)",
i,
obs.len(),
t
)));
}
}
Ok(Self { grid, observations })
}
pub fn n_curves(&self) -> usize {
self.observations.len()
}
pub fn n_grid(&self) -> usize {
self.grid.len()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum BasisType {
BSpline {
n_basis: usize,
degree: usize,
},
Fourier {
n_basis: usize,
},
Polynomial {
degree: usize,
},
}
#[derive(Debug, Clone)]
pub struct FunctionalConfig {
pub basis: BasisType,
pub smoothing_param: Option<f64>,
pub n_components: usize,
}
impl Default for FunctionalConfig {
fn default() -> Self {
Self {
basis: BasisType::BSpline {
n_basis: 15,
degree: 3,
},
smoothing_param: None,
n_components: 3,
}
}
}
#[derive(Debug, Clone)]
pub struct FPCAResult {
pub eigenvalues: Array1<f64>,
pub eigenfunctions: Array2<f64>,
pub scores: Array2<f64>,
pub variance_explained: Array1<f64>,
pub grid: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct SoFResult {
pub beta: Array1<f64>,
pub intercept: f64,
pub beta_coefficients: Array1<f64>,
pub basis: BasisType,
pub grid: Vec<f64>,
pub lambda: f64,
pub fitted_values: Array1<f64>,
pub r_squared: f64,
}
#[derive(Debug, Clone)]
pub struct FoFResult {
pub beta_surface: Array2<f64>,
pub beta_coefficients: Array1<f64>,
pub predictor_basis: BasisType,
pub response_basis: BasisType,
pub predictor_grid: Vec<f64>,
pub response_grid: Vec<f64>,
pub lambda: f64,
pub fitted_curves: Array2<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_functional_data_valid() {
let grid = vec![0.0, 0.5, 1.0];
let obs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
let data = FunctionalData::new(grid, obs).expect("Should succeed");
assert_eq!(data.n_curves(), 2);
assert_eq!(data.n_grid(), 3);
}
#[test]
fn test_functional_data_empty_grid() {
let result = FunctionalData::new(vec![], vec![vec![1.0]]);
assert!(result.is_err());
}
#[test]
fn test_functional_data_empty_observations() {
let result = FunctionalData::new(vec![0.0, 1.0], vec![]);
assert!(result.is_err());
}
#[test]
fn test_functional_data_dimension_mismatch() {
let result = FunctionalData::new(vec![0.0, 1.0], vec![vec![1.0, 2.0, 3.0]]);
assert!(result.is_err());
}
#[test]
fn test_functional_config_default() {
let config = FunctionalConfig::default();
assert!(config.smoothing_param.is_none());
assert_eq!(config.n_components, 3);
match &config.basis {
BasisType::BSpline { n_basis, degree } => {
assert_eq!(*n_basis, 15);
assert_eq!(*degree, 3);
}
_ => panic!("Default should be BSpline"),
}
}
}