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}