Skip to main content

fdars_core/basis/
projection.rs

1//! Basis projection and reconstruction for functional data.
2
3use super::bspline::bspline_basis;
4use super::fourier::fourier_basis;
5use super::helpers::svd_pseudoinverse;
6use crate::iter_maybe_parallel;
7use crate::matrix::FdMatrix;
8use nalgebra::DMatrix;
9#[cfg(feature = "parallel")]
10use rayon::iter::ParallelIterator;
11
12/// Simple basis type selector for projection functions.
13///
14/// Used by [`fdata_to_basis_1d`] and [`basis_to_fdata_1d`] to choose between
15/// B-spline and Fourier basis systems. For penalized smoothing with additional
16/// parameters (order, period), see [`BasisType`](crate::smooth_basis::BasisType).
17#[derive(Debug, Clone, Copy, PartialEq)]
18#[non_exhaustive]
19pub enum ProjectionBasisType {
20    /// B-spline basis (order 4 / cubic).
21    Bspline,
22    /// Fourier basis.
23    Fourier,
24}
25
26impl ProjectionBasisType {
27    /// Convert from legacy integer encoding (0 = B-spline, 1 = Fourier).
28    ///
29    /// Returns `Bspline` for any value other than 1, matching the previous
30    /// `if basis_type == 1 { Fourier } else { Bspline }` convention.
31    #[must_use]
32    pub fn from_i32(value: i32) -> Self {
33        if value == 1 {
34            Self::Fourier
35        } else {
36            Self::Bspline
37        }
38    }
39
40    /// Convert to the legacy integer encoding (0 = B-spline, 1 = Fourier).
41    #[must_use]
42    pub fn to_i32(self) -> i32 {
43        match self {
44            Self::Bspline => 0,
45            Self::Fourier => 1,
46        }
47    }
48}
49
50/// Result of basis projection.
51#[derive(Debug, Clone)]
52#[non_exhaustive]
53pub struct BasisProjectionResult {
54    /// Coefficient matrix (n_samples x n_basis)
55    pub coefficients: FdMatrix,
56    /// Number of basis functions used
57    pub n_basis: usize,
58}
59
60/// Evaluate the appropriate basis on `argvals`.
61fn evaluate_projection_basis(
62    argvals: &[f64],
63    nbasis: usize,
64    basis_type: ProjectionBasisType,
65) -> Vec<f64> {
66    match basis_type {
67        ProjectionBasisType::Fourier => fourier_basis(argvals, nbasis),
68        ProjectionBasisType::Bspline => {
69            // For order 4 B-splines: nbasis = nknots + order, so nknots = nbasis - 4
70            bspline_basis(argvals, nbasis.saturating_sub(4).max(2), 4)
71        }
72    }
73}
74
75/// Project functional data to basis coefficients.
76///
77/// # Arguments
78/// * `data` - Column-major FdMatrix (n x m)
79/// * `argvals` - Evaluation points
80/// * `nbasis` - Number of basis functions
81/// * `basis_type` - Basis type to use
82///
83/// # Examples
84///
85/// ```
86/// use fdars_core::matrix::FdMatrix;
87/// use fdars_core::basis::projection::{fdata_to_basis, ProjectionBasisType};
88///
89/// let argvals: Vec<f64> = (0..20).map(|i| i as f64 / 19.0).collect();
90/// let data = FdMatrix::from_column_major(
91///     argvals.iter().map(|&t| (t * 6.0).sin()).collect(),
92///     1, 20,
93/// ).unwrap();
94/// let result = fdata_to_basis(&data, &argvals, 7, ProjectionBasisType::Fourier).unwrap();
95/// assert_eq!(result.coefficients.nrows(), 1);
96/// assert_eq!(result.n_basis, 7);
97/// ```
98pub fn fdata_to_basis(
99    data: &FdMatrix,
100    argvals: &[f64],
101    nbasis: usize,
102    basis_type: ProjectionBasisType,
103) -> Option<BasisProjectionResult> {
104    let n = data.nrows();
105    let m = data.ncols();
106    if n == 0 || m == 0 || argvals.len() != m || nbasis < 2 {
107        return None;
108    }
109
110    let basis = evaluate_projection_basis(argvals, nbasis, basis_type);
111
112    let actual_nbasis = basis.len() / m;
113    let b_mat = DMatrix::from_column_slice(m, actual_nbasis, &basis);
114
115    let btb = &b_mat.transpose() * &b_mat;
116    let btb_inv = svd_pseudoinverse(&btb)?;
117    let proj = btb_inv * b_mat.transpose();
118
119    let coefs: Vec<f64> = iter_maybe_parallel!(0..n)
120        .flat_map(|i| {
121            let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
122            (0..actual_nbasis)
123                .map(|k| {
124                    let mut sum = 0.0;
125                    for j in 0..m {
126                        sum += proj[(k, j)] * curve[j];
127                    }
128                    sum
129                })
130                .collect::<Vec<_>>()
131        })
132        .collect();
133
134    Some(BasisProjectionResult {
135        coefficients: FdMatrix::from_column_major(coefs, n, actual_nbasis)
136            .expect("dimension invariant: data.len() == n * m"),
137        n_basis: actual_nbasis,
138    })
139}
140
141/// Project functional data to basis coefficients (legacy i32 interface).
142///
143/// # Arguments
144/// * `data` - Column-major FdMatrix (n x m)
145/// * `argvals` - Evaluation points
146/// * `nbasis` - Number of basis functions
147/// * `basis_type` - 0 = B-spline, 1 = Fourier
148pub fn fdata_to_basis_1d(
149    data: &FdMatrix,
150    argvals: &[f64],
151    nbasis: usize,
152    basis_type: i32,
153) -> Option<BasisProjectionResult> {
154    fdata_to_basis(
155        data,
156        argvals,
157        nbasis,
158        ProjectionBasisType::from_i32(basis_type),
159    )
160}
161
162/// Reconstruct functional data from basis coefficients.
163///
164/// # Arguments
165/// * `coefs` - Coefficient matrix (n x n_basis)
166/// * `argvals` - Evaluation points
167/// * `nbasis` - Number of basis functions
168/// * `basis_type` - Basis type to use
169pub fn basis_to_fdata(
170    coefs: &FdMatrix,
171    argvals: &[f64],
172    nbasis: usize,
173    basis_type: ProjectionBasisType,
174) -> FdMatrix {
175    let n = coefs.nrows();
176    let coefs_ncols = coefs.ncols();
177    let m = argvals.len();
178    if n == 0 || m == 0 || nbasis < 2 {
179        return FdMatrix::zeros(0, 0);
180    }
181
182    let basis = evaluate_projection_basis(argvals, nbasis, basis_type);
183
184    let actual_nbasis = basis.len() / m;
185
186    let flat: Vec<f64> = iter_maybe_parallel!(0..n)
187        .flat_map(|i| {
188            (0..m)
189                .map(|j| {
190                    let mut sum = 0.0;
191                    for k in 0..actual_nbasis.min(coefs_ncols) {
192                        sum += coefs[(i, k)] * basis[j + k * m];
193                    }
194                    sum
195                })
196                .collect::<Vec<_>>()
197        })
198        .collect();
199
200    FdMatrix::from_column_major(flat, n, m).expect("dimension invariant: data.len() == n * m")
201}
202
203/// Reconstruct functional data from basis coefficients (legacy i32 interface).
204pub fn basis_to_fdata_1d(
205    coefs: &FdMatrix,
206    argvals: &[f64],
207    nbasis: usize,
208    basis_type: i32,
209) -> FdMatrix {
210    basis_to_fdata(
211        coefs,
212        argvals,
213        nbasis,
214        ProjectionBasisType::from_i32(basis_type),
215    )
216}