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    // Per-curve coefficient rows. Collect as `Vec<Vec<f64>>` (one row per curve)
120    // and scatter into the column-major FdMatrix via `[(i, k)]` indexing. The
121    // previous code built a curve-major flat buffer and passed it to
122    // `from_column_major`, which transposes/scrambles the result whenever
123    // `n > 1` (it round-trips only for a single curve) — GH #33.
124    let rows: Vec<Vec<f64>> = iter_maybe_parallel!(0..n)
125        .map(|i| {
126            let curve: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
127            (0..actual_nbasis)
128                .map(|k| {
129                    let mut sum = 0.0;
130                    for j in 0..m {
131                        sum += proj[(k, j)] * curve[j];
132                    }
133                    sum
134                })
135                .collect::<Vec<_>>()
136        })
137        .collect();
138
139    let mut coefficients = FdMatrix::zeros(n, actual_nbasis);
140    for (i, row) in rows.iter().enumerate() {
141        for (k, &c) in row.iter().enumerate() {
142            coefficients[(i, k)] = c;
143        }
144    }
145
146    Some(BasisProjectionResult {
147        coefficients,
148        n_basis: actual_nbasis,
149    })
150}
151
152/// Project functional data to basis coefficients (legacy i32 interface).
153///
154/// # Arguments
155/// * `data` - Column-major FdMatrix (n x m)
156/// * `argvals` - Evaluation points
157/// * `nbasis` - Number of basis functions
158/// * `basis_type` - 0 = B-spline, 1 = Fourier
159pub fn fdata_to_basis_1d(
160    data: &FdMatrix,
161    argvals: &[f64],
162    nbasis: usize,
163    basis_type: i32,
164) -> Option<BasisProjectionResult> {
165    fdata_to_basis(
166        data,
167        argvals,
168        nbasis,
169        ProjectionBasisType::from_i32(basis_type),
170    )
171}
172
173/// Reconstruct functional data from basis coefficients.
174///
175/// # Arguments
176/// * `coefs` - Coefficient matrix (n x n_basis)
177/// * `argvals` - Evaluation points
178/// * `nbasis` - Number of basis functions
179/// * `basis_type` - Basis type to use
180pub fn basis_to_fdata(
181    coefs: &FdMatrix,
182    argvals: &[f64],
183    nbasis: usize,
184    basis_type: ProjectionBasisType,
185) -> FdMatrix {
186    let n = coefs.nrows();
187    let coefs_ncols = coefs.ncols();
188    let m = argvals.len();
189    if n == 0 || m == 0 || nbasis < 2 {
190        return FdMatrix::zeros(0, 0);
191    }
192
193    let basis = evaluate_projection_basis(argvals, nbasis, basis_type);
194
195    let actual_nbasis = basis.len() / m;
196
197    // Per-curve reconstruction rows, scattered into the column-major FdMatrix
198    // via `[(i, j)]`. As in `fdata_to_basis`, building a curve-major flat buffer
199    // and passing it to `from_column_major` transposes the output for n > 1
200    // (GH #33).
201    let rows: Vec<Vec<f64>> = iter_maybe_parallel!(0..n)
202        .map(|i| {
203            (0..m)
204                .map(|j| {
205                    let mut sum = 0.0;
206                    for k in 0..actual_nbasis.min(coefs_ncols) {
207                        sum += coefs[(i, k)] * basis[j + k * m];
208                    }
209                    sum
210                })
211                .collect::<Vec<_>>()
212        })
213        .collect();
214
215    let mut out = FdMatrix::zeros(n, m);
216    for (i, row) in rows.iter().enumerate() {
217        for (j, &v) in row.iter().enumerate() {
218            out[(i, j)] = v;
219        }
220    }
221    out
222}
223
224/// Reconstruct functional data from basis coefficients (legacy i32 interface).
225pub fn basis_to_fdata_1d(
226    coefs: &FdMatrix,
227    argvals: &[f64],
228    nbasis: usize,
229    basis_type: i32,
230) -> FdMatrix {
231    basis_to_fdata(
232        coefs,
233        argvals,
234        nbasis,
235        ProjectionBasisType::from_i32(basis_type),
236    )
237}