use scirs2_core::ndarray::{s, Array1, Array2, Axis, ScalarOperand};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::Debug;
use super::pca::{compute_covariance_matrix, compute_eigendecomposition};
use crate::error::{Result, TimeSeriesError};
#[derive(Debug, Clone)]
pub struct FunctionalPCAConfig {
pub n_components: Option<usize>,
pub smoothing_parameter: f64,
pub nbasis_functions: usize,
pub basis_type: BasisType,
pub center_functions: bool,
pub estimate_derivatives: bool,
pub derivative_order: usize,
pub regularization_parameter: f64,
}
impl Default for FunctionalPCAConfig {
fn default() -> Self {
Self {
n_components: None,
smoothing_parameter: 0.01,
nbasis_functions: 20,
basis_type: BasisType::BSpline,
center_functions: true,
estimate_derivatives: false,
derivative_order: 0,
regularization_parameter: 1e-4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BasisType {
BSpline,
Fourier,
Polynomial,
Wavelet,
}
#[derive(Debug, Clone)]
pub struct FunctionalPCAResult<F> {
pub functional_components: Array2<F>,
pub explained_variance: Array1<F>,
pub explained_variance_ratio: Array1<F>,
pub mean_function: Array1<F>,
pub basis_evaluation: Array2<F>,
pub scores: Array2<F>,
pub reconstructed_functions: Array2<F>,
pub smoothness_measures: Array1<F>,
}
#[allow(dead_code)]
pub fn apply_functional_pca<F>(
functional_data: &Array2<F>,
config: &FunctionalPCAConfig,
) -> Result<FunctionalPCAResult<F>>
where
F: Float + FromPrimitive + Debug + Clone + ScalarOperand + 'static,
{
let (n_functions, n_points) = functional_data.dim();
if n_functions == 0 || n_points == 0 {
return Err(TimeSeriesError::InvalidInput(
"Functional _data matrix cannot be empty".to_string(),
));
}
let basis_evaluation = createbasis_functions(n_points, config)?;
let nbasis = basis_evaluation.ncols();
let basis_coefficients = project_ontobasis(functional_data, &basis_evaluation)?;
let centered_coefficients = if config.center_functions {
let mean_function = basis_coefficients
.mean_axis(Axis(0))
.expect("Operation failed");
let mut centered = basis_coefficients.clone();
for mut row in centered.axis_iter_mut(Axis(0)) {
for (j, &mean_val) in mean_function.iter().enumerate() {
row[j] = row[j] - mean_val;
}
}
(centered, mean_function)
} else {
let mean_function = Array1::zeros(nbasis);
(basis_coefficients, mean_function)
};
let regularized_covariance = apply_smoothness_regularization(
¢ered_coefficients.0,
config.regularization_parameter,
&basis_evaluation,
)?;
let (eigenvalues, eigenvectors) = compute_eigendecomposition(®ularized_covariance)?;
let n_components = config
.n_components
.unwrap_or(std::cmp::min(n_functions.saturating_sub(1), nbasis));
let n_components = std::cmp::min(n_components, eigenvalues.len());
let functional_components = eigenvectors.slice(s![.., ..n_components]).to_owned();
let explained_variance = eigenvalues.slice(s![..n_components]).to_owned();
let total_variance = eigenvalues.sum();
let explained_variance_ratio = &explained_variance / total_variance;
let scores = centered_coefficients.0.dot(&functional_components);
let reconstructed_coefficients = scores.dot(&functional_components.t());
let reconstructed_functions = reconstructed_coefficients.dot(&basis_evaluation.t());
let smoothness_measures =
compute_smoothness_measures(&functional_components, &basis_evaluation)?;
Ok(FunctionalPCAResult {
functional_components,
explained_variance,
explained_variance_ratio,
mean_function: centered_coefficients.1,
basis_evaluation,
scores,
reconstructed_functions,
smoothness_measures,
})
}
#[allow(dead_code)]
fn createbasis_functions<F>(_npoints: usize, config: &FunctionalPCAConfig) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
match config.basis_type {
BasisType::BSpline => create_bsplinebasis(_npoints, config.nbasis_functions),
BasisType::Fourier => create_fourierbasis(_npoints, config.nbasis_functions),
BasisType::Polynomial => create_polynomialbasis(_npoints, config.nbasis_functions),
BasisType::Wavelet => create_waveletbasis(_npoints, config.nbasis_functions),
}
}
#[allow(dead_code)]
fn create_bsplinebasis<F>(_n_points: usize, nbasis: usize) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let mut basis = Array2::zeros((_n_points, nbasis));
for j in 0..nbasis {
for i in 0.._n_points {
let t = F::from(i).expect("Failed to convert to float")
/ F::from(_n_points - 1).expect("Failed to convert to float");
let center = F::from(j).expect("Failed to convert to float")
/ F::from(nbasis - 1).expect("Failed to convert to float");
let width = F::one() / F::from(nbasis).expect("Failed to convert to float");
let diff = (t - center) / width;
basis[(i, j)] = (-diff * diff).exp();
}
}
Ok(basis)
}
#[allow(dead_code)]
fn create_fourierbasis<F>(_n_points: usize, nbasis: usize) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let mut basis = Array2::zeros((_n_points, nbasis));
let pi = F::from(std::f64::consts::PI).expect("Failed to convert to float");
for j in 0..nbasis {
for i in 0.._n_points {
let t = F::from(i).expect("Failed to convert to float")
/ F::from(_n_points - 1).expect("Failed to convert to float");
let freq = F::from(j + 1).expect("Failed to convert to float");
if j % 2 == 0 {
basis[(i, j)] =
(F::from(2.0).expect("Failed to convert constant to float") * pi * freq * t)
.cos();
} else {
basis[(i, j)] =
(F::from(2.0).expect("Failed to convert constant to float") * pi * freq * t)
.sin();
}
}
}
Ok(basis)
}
#[allow(dead_code)]
fn create_polynomialbasis<F>(_n_points: usize, nbasis: usize) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let mut basis = Array2::zeros((_n_points, nbasis));
for j in 0..nbasis {
for i in 0.._n_points {
let t = F::from(i).expect("Failed to convert to float")
/ F::from(_n_points - 1).expect("Failed to convert to float");
basis[(i, j)] = t.powf(F::from(j).expect("Failed to convert to float"));
}
}
Ok(basis)
}
#[allow(dead_code)]
fn create_waveletbasis<F>(n_points: usize, nbasis: usize) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let mut basis = Array2::zeros((n_points, nbasis));
for i in 0..n_points {
basis[(i, 0)] = F::one()
/ F::from(n_points)
.expect("Failed to convert to float")
.sqrt();
}
for j in 1..nbasis {
let scale = 1 << (j / 2); let shift = j % scale;
for i in 0..n_points {
let t = F::from(i).expect("Failed to convert to float")
/ F::from(n_points - 1).expect("Failed to convert to float");
let scaled_t = t * F::from(scale).expect("Failed to convert to float")
- F::from(shift).expect("Failed to convert to float");
if scaled_t >= F::zero() && scaled_t < F::one() {
if scaled_t < F::from(0.5).expect("Failed to convert constant to float") {
basis[(i, j)] = F::one();
} else {
basis[(i, j)] = -F::one();
}
basis[(i, j)] =
basis[(i, j)] / F::from(scale).expect("Failed to convert to float").sqrt();
}
}
}
Ok(basis)
}
#[allow(dead_code)]
fn project_ontobasis<F>(
functional_data: &Array2<F>,
basis_evaluation: &Array2<F>,
) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let coefficients = functional_data.dot(basis_evaluation);
Ok(coefficients)
}
#[allow(dead_code)]
fn apply_smoothness_regularization<F>(
coefficients: &Array2<F>,
lambda: f64,
_basis_evaluation: &Array2<F>,
) -> Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + ScalarOperand + 'static,
{
let covariance = compute_covariance_matrix(coefficients)?;
let lambda_f = F::from(lambda).expect("Failed to convert to float");
let identity = Array2::eye(covariance.ncols());
let regularized = covariance - identity.mapv(|x: F| x * lambda_f);
Ok(regularized)
}
#[allow(dead_code)]
fn compute_smoothness_measures<F>(
components: &Array2<F>,
_basis_evaluation: &Array2<F>,
) -> Result<Array1<F>>
where
F: Float + FromPrimitive + Debug + Clone + 'static,
{
let n_components = components.ncols();
let mut smoothness = Array1::zeros(n_components);
for j in 0..n_components {
let component = components.column(j);
let mut roughness = F::zero();
for i in 1..component.len() {
let diff = component[i] - component[i - 1];
roughness = roughness + diff * diff;
}
smoothness[j] = roughness;
}
Ok(smoothness)
}
pub struct FunctionalPCA {
n_components: usize,
}
impl FunctionalPCA {
pub fn new(n_components: usize) -> Self {
Self { n_components }
}
pub fn fit_transform<F>(&self, data: &Array2<F>) -> crate::error::Result<Array2<F>>
where
F: Float + FromPrimitive + Debug + Clone + ScalarOperand + 'static,
{
let config = FunctionalPCAConfig {
n_components: Some(self.n_components),
..Default::default()
};
let result = apply_functional_pca(data, &config)?;
Ok(result.scores)
}
}