1use crate::error::FdarError;
42use crate::matrix::FdMatrix;
43use nalgebra::SVD;
44
45#[non_exhaustive]
49#[derive(Debug, Clone, PartialEq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub struct MfpcaConfig {
52 pub ncomp: usize,
54 pub weighted: bool,
56}
57
58impl Default for MfpcaConfig {
59 fn default() -> Self {
60 Self {
61 ncomp: 5,
62 weighted: true,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70#[non_exhaustive]
71pub struct MfpcaResult {
72 pub scores: FdMatrix,
74 pub eigenfunctions: Vec<FdMatrix>,
76 pub eigenvalues: Vec<f64>,
78 pub means: Vec<Vec<f64>>,
80 pub scales: Vec<f64>,
82 pub grid_sizes: Vec<usize>,
84 pub(super) combined_rotation: FdMatrix,
86 pub(super) scale_threshold: f64,
88}
89
90impl MfpcaResult {
91 pub fn project(&self, new_data: &[&FdMatrix]) -> Result<FdMatrix, FdarError> {
100 if new_data.len() != self.means.len() {
101 return Err(FdarError::InvalidDimension {
102 parameter: "new_data",
103 expected: format!("{} variables", self.means.len()),
104 actual: format!("{} variables", new_data.len()),
105 });
106 }
107
108 let total_input_cols: usize = new_data.iter().map(|v| v.ncols()).sum();
111 let expected_total: usize = self.grid_sizes.iter().sum();
112 if total_input_cols != expected_total {
113 return Err(FdarError::InvalidDimension {
114 parameter: "new_data",
115 expected: format!("{expected_total} total columns across all variables"),
116 actual: format!("{total_input_cols} total columns"),
117 });
118 }
119
120 let n_new = new_data[0].nrows();
121 let ncomp = self.scores.ncols();
122 let total_cols: usize = self.grid_sizes.iter().sum();
123
124 let mut stacked = FdMatrix::zeros(n_new, total_cols);
126 let mut col_offset = 0;
127 for (p, &var) in new_data.iter().enumerate() {
128 let m_p = self.grid_sizes[p];
129 if var.ncols() != m_p {
130 return Err(FdarError::InvalidDimension {
131 parameter: "new_data",
132 expected: format!("{m_p} columns for variable {p}"),
133 actual: format!("{} columns", var.ncols()),
134 });
135 }
136 if var.nrows() != n_new {
137 return Err(FdarError::InvalidDimension {
138 parameter: "new_data",
139 expected: format!("{n_new} rows for all variables"),
140 actual: format!("{} rows for variable {p}", var.nrows()),
141 });
142 }
143 let scale = if self.scales[p] >= self.scale_threshold {
144 self.scales[p]
145 } else {
146 1.0
147 };
148 for i in 0..n_new {
149 for j in 0..m_p {
150 let centered = var[(i, j)] - self.means[p][j];
151 stacked[(i, col_offset + j)] = centered / scale;
152 }
153 }
154 col_offset += m_p;
155 }
156
157 let mut scores = FdMatrix::zeros(n_new, ncomp);
159 for i in 0..n_new {
160 for k in 0..ncomp {
161 let mut sum = 0.0;
162 for j in 0..total_cols {
163 sum += stacked[(i, j)] * self.combined_rotation[(j, k)];
164 }
165 scores[(i, k)] = sum;
166 }
167 }
168 Ok(scores)
169 }
170
171 pub fn reconstruct(&self, scores: &FdMatrix, ncomp: usize) -> Result<Vec<FdMatrix>, FdarError> {
179 let max_comp = self.combined_rotation.ncols().min(scores.ncols());
180 if ncomp == 0 || ncomp > max_comp {
181 return Err(FdarError::InvalidParameter {
182 parameter: "ncomp",
183 message: format!("ncomp={ncomp} must be in 1..={max_comp}"),
184 });
185 }
186
187 let n = scores.nrows();
188 let total_cols: usize = self.grid_sizes.iter().sum();
189
190 let mut stacked = FdMatrix::zeros(n, total_cols);
192 for i in 0..n {
193 for j in 0..total_cols {
194 let mut val = 0.0;
195 for k in 0..ncomp {
196 val += scores[(i, k)] * self.combined_rotation[(j, k)];
197 }
198 stacked[(i, j)] = val;
199 }
200 }
201
202 let mut result = Vec::with_capacity(self.means.len());
204 let mut col_offset = 0;
205 for (p, m_p) in self.grid_sizes.iter().enumerate() {
206 let scale = if self.scales[p] >= self.scale_threshold {
207 self.scales[p]
208 } else {
209 1.0
210 };
211 let mut var_mat = FdMatrix::zeros(n, *m_p);
212 for i in 0..n {
213 for j in 0..*m_p {
214 var_mat[(i, j)] = stacked[(i, col_offset + j)] * scale + self.means[p][j];
215 }
216 }
217 col_offset += m_p;
218 result.push(var_mat);
219 }
220 Ok(result)
221 }
222}
223
224#[must_use = "expensive computation whose result should not be discarded"]
251pub fn mfpca(variables: &[&FdMatrix], config: &MfpcaConfig) -> Result<MfpcaResult, FdarError> {
252 if variables.is_empty() {
253 return Err(FdarError::InvalidDimension {
254 parameter: "variables",
255 expected: "at least 1 variable".to_string(),
256 actual: "0 variables".to_string(),
257 });
258 }
259
260 let n = variables[0].nrows();
261 if n < 2 {
262 return Err(FdarError::InvalidDimension {
263 parameter: "variables",
264 expected: "at least 2 observations".to_string(),
265 actual: format!("{n} observations"),
266 });
267 }
268
269 for (p, var) in variables.iter().enumerate() {
270 if var.nrows() != n {
271 return Err(FdarError::InvalidDimension {
272 parameter: "variables",
273 expected: format!("{n} rows for all variables"),
274 actual: format!("{} rows for variable {p}", var.nrows()),
275 });
276 }
277 }
278
279 let grid_sizes: Vec<usize> = variables.iter().map(|v| v.ncols()).collect();
280 let total_cols: usize = grid_sizes.iter().sum();
281 let ncomp = config.ncomp.min(n).min(total_cols);
282
283 let mut means: Vec<Vec<f64>> = Vec::with_capacity(variables.len());
285 let mut scales: Vec<f64> = Vec::with_capacity(variables.len());
286
287 for var in variables.iter() {
288 let (_, m_p) = var.shape();
289 let mut mean = vec![0.0; m_p];
290 for j in 0..m_p {
291 let col = var.column(j);
292 mean[j] = col.iter().sum::<f64>() / n as f64;
293 }
294
295 let mut mean_var = 0.0;
297 for j in 0..m_p {
298 let col = var.column(j);
299 let var_j: f64 =
300 col.iter().map(|&v| (v - mean[j]).powi(2)).sum::<f64>() / (n as f64 - 1.0);
301 mean_var += var_j;
302 }
303 mean_var /= m_p as f64;
304 let scale = mean_var.sqrt();
305
306 means.push(mean);
307 scales.push(scale);
308 }
309
310 let max_scale = scales.iter().cloned().fold(0.0_f64, f64::max);
315 let scale_threshold = 1e-12 * max_scale.max(1e-15); let mut stacked = FdMatrix::zeros(n, total_cols);
319 let mut col_offset = 0;
320 for (p, var) in variables.iter().enumerate() {
321 let m_p = grid_sizes[p];
322 let scale = if config.weighted && scales[p] > scale_threshold {
323 scales[p]
324 } else {
325 1.0
326 };
327 if !config.weighted {
329 scales[p] = 1.0;
330 }
331 for i in 0..n {
332 for j in 0..m_p {
333 let centered = var[(i, j)] - means[p][j];
334 stacked[(i, col_offset + j)] = centered / scale;
335 }
336 }
337 col_offset += m_p;
338 }
339
340 let svd = SVD::new(stacked.to_dmatrix(), true, true);
342
343 let v_t = svd
344 .v_t
345 .as_ref()
346 .ok_or_else(|| FdarError::ComputationFailed {
347 operation: "MFPCA SVD",
348 detail: "SVD failed to produce V_t matrix".to_string(),
349 })?;
350
351 let u = svd.u.as_ref().ok_or_else(|| FdarError::ComputationFailed {
352 operation: "MFPCA SVD",
353 detail: "SVD failed to produce U matrix".to_string(),
354 })?;
355
356 let singular_values: Vec<f64> = svd.singular_values.iter().take(ncomp).copied().collect();
358
359 let eigenvalues: Vec<f64> = singular_values
361 .iter()
362 .map(|&sv| sv * sv / (n as f64 - 1.0))
363 .collect();
364
365 let mut combined_rotation = FdMatrix::zeros(total_cols, ncomp);
367 for k in 0..ncomp {
368 for j in 0..total_cols {
369 combined_rotation[(j, k)] = v_t[(k, j)];
370 }
371 }
372
373 let mut scores = FdMatrix::zeros(n, ncomp);
375 for k in 0..ncomp {
376 let sv_k = singular_values[k];
377 for i in 0..n {
378 scores[(i, k)] = u[(i, k)] * sv_k;
379 }
380 }
381
382 let mut eigenfunctions = Vec::with_capacity(variables.len());
384 let mut col_off = 0;
385 for m_p in &grid_sizes {
386 let mut ef = FdMatrix::zeros(*m_p, ncomp);
387 for k in 0..ncomp {
388 for j in 0..*m_p {
389 ef[(j, k)] = combined_rotation[(col_off + j, k)];
390 }
391 }
392 col_off += m_p;
393 eigenfunctions.push(ef);
394 }
395
396 Ok(MfpcaResult {
397 scores,
398 eigenfunctions,
399 eigenvalues,
400 means,
401 scales,
402 grid_sizes,
403 combined_rotation,
404 scale_threshold,
405 })
406}