1use crate::alignment::{srsf_inverse, srsf_transform, KarcherMeanResult};
13
14use crate::matrix::FdMatrix;
15use crate::warping::{exp_map_sphere, inv_exp_map_sphere, l2_norm_l2, psi_to_gam};
16use nalgebra::SVD;
17
18#[derive(Debug, Clone)]
22pub struct VertFpcaResult {
23 pub scores: FdMatrix,
25 pub eigenfunctions_q: FdMatrix,
27 pub eigenfunctions_f: FdMatrix,
29 pub eigenvalues: Vec<f64>,
31 pub cumulative_variance: Vec<f64>,
33 pub mean_q: Vec<f64>,
35}
36
37#[derive(Debug, Clone)]
39pub struct HorizFpcaResult {
40 pub scores: FdMatrix,
42 pub eigenfunctions_psi: FdMatrix,
44 pub eigenfunctions_gam: FdMatrix,
46 pub eigenvalues: Vec<f64>,
48 pub cumulative_variance: Vec<f64>,
50 pub mean_psi: Vec<f64>,
52 pub shooting_vectors: FdMatrix,
54}
55
56#[derive(Debug, Clone)]
58pub struct JointFpcaResult {
59 pub scores: FdMatrix,
61 pub eigenvalues: Vec<f64>,
63 pub cumulative_variance: Vec<f64>,
65 pub balance_c: f64,
67 pub vert_component: FdMatrix,
69 pub horiz_component: FdMatrix,
71}
72
73pub fn vert_fpca(
87 karcher: &KarcherMeanResult,
88 argvals: &[f64],
89 ncomp: usize,
90) -> Option<VertFpcaResult> {
91 let (n, m) = karcher.aligned_data.shape();
92 if n < 2 || m < 2 || ncomp < 1 || argvals.len() != m {
93 return None;
94 }
95 let ncomp = ncomp.min(n - 1).min(m);
96 let m_aug = m + 1;
97
98 let qn = match &karcher.aligned_srsfs {
99 Some(srsfs) => srsfs.clone(),
100 None => srsf_transform(&karcher.aligned_data, argvals),
101 };
102
103 let q_aug = build_augmented_srsfs(&qn, &karcher.aligned_data, n, m);
104
105 let (_, mean_q) = center_matrix(&q_aug, n, m_aug);
107 let k_mat = build_symmetric_covariance(&q_aug, &mean_q, n, m_aug);
108
109 let svd = SVD::new(k_mat, true, true);
110 let u_cov = svd.u.as_ref()?;
111
112 let eigenvalues: Vec<f64> = svd.singular_values.iter().take(ncomp).copied().collect();
113 let cumulative_variance = cumulative_variance_from_eigenvalues(&eigenvalues);
114
115 let mut eigenfunctions_q = FdMatrix::zeros(ncomp, m_aug);
117 for k in 0..ncomp {
118 for j in 0..m_aug {
119 eigenfunctions_q[(k, j)] = u_cov[(j, k)];
120 }
121 }
122
123 let scores = project_onto_eigenvectors(&q_aug, &mean_q, u_cov, n, m_aug, ncomp);
125
126 let mut eigenfunctions_f = FdMatrix::zeros(ncomp, m);
128 for k in 0..ncomp {
129 let q_k: Vec<f64> = (0..m)
130 .map(|j| mean_q[j] + eigenfunctions_q[(k, j)])
131 .collect();
132 let aug_val = mean_q[m] + eigenfunctions_q[(k, m)];
133 let f0 = aug_val.signum() * aug_val * aug_val;
134 let f_k = srsf_inverse(&q_k, argvals, f0);
135 for j in 0..m {
136 eigenfunctions_f[(k, j)] = f_k[j];
137 }
138 }
139
140 Some(VertFpcaResult {
141 scores,
142 eigenfunctions_q,
143 eigenfunctions_f,
144 eigenvalues,
145 cumulative_variance,
146 mean_q,
147 })
148}
149
150pub fn horiz_fpca(
164 karcher: &KarcherMeanResult,
165 argvals: &[f64],
166 ncomp: usize,
167) -> Option<HorizFpcaResult> {
168 let (n, m) = karcher.gammas.shape();
169 if n < 2 || m < 2 || ncomp < 1 || argvals.len() != m {
170 return None;
171 }
172 let ncomp = ncomp.min(n - 1).min(m);
173
174 let t0 = argvals[0];
175 let domain = argvals[m - 1] - t0;
176 let time: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
177
178 let psis = warps_to_normalized_psi(&karcher.gammas, argvals);
179 let mu_psi = sphere_karcher_mean(&psis, &time, 50);
180 let shooting = shooting_vectors_from_psis(&psis, &mu_psi, &time);
181
182 let (centered, _mean_v) = center_matrix(&shooting, n, m);
184
185 let svd = SVD::new(centered.to_dmatrix(), true, true);
186 let v_t = svd.v_t.as_ref()?;
187 let (scores, eigenvalues) = svd_scores_and_eigenvalues(&svd, ncomp, n)?;
188 let cumulative_variance = cumulative_variance_from_eigenvalues(&eigenvalues);
189
190 let mut eigenfunctions_psi = FdMatrix::zeros(ncomp, m);
192 for k in 0..ncomp {
193 for j in 0..m {
194 eigenfunctions_psi[(k, j)] = v_t[(k, j)];
195 }
196 }
197
198 let mut eigenfunctions_gam = FdMatrix::zeros(ncomp, m);
200 for k in 0..ncomp {
201 let v_k: Vec<f64> = (0..m).map(|j| eigenfunctions_psi[(k, j)]).collect();
202 let psi_k = exp_map_sphere(&mu_psi, &v_k, &time);
203 let gam_k = psi_to_gam(&psi_k, &time);
204 for j in 0..m {
205 eigenfunctions_gam[(k, j)] = t0 + gam_k[j] * domain;
206 }
207 }
208
209 Some(HorizFpcaResult {
210 scores,
211 eigenfunctions_psi,
212 eigenfunctions_gam,
213 eigenvalues,
214 cumulative_variance,
215 mean_psi: mu_psi,
216 shooting_vectors: shooting,
217 })
218}
219
220pub fn joint_fpca(
233 karcher: &KarcherMeanResult,
234 argvals: &[f64],
235 ncomp: usize,
236 balance_c: Option<f64>,
237) -> Option<JointFpcaResult> {
238 let (n, m) = karcher.aligned_data.shape();
239 if n < 2 || m < 2 || ncomp < 1 || argvals.len() != m {
240 return None;
241 }
242
243 let _vert = vert_fpca(karcher, argvals, ncomp)?;
244 let horiz = horiz_fpca(karcher, argvals, ncomp)?;
245
246 let m_aug = m + 1;
247 let ncomp = ncomp.min(n - 1);
248
249 let qn = match &karcher.aligned_srsfs {
250 Some(srsfs) => srsfs.clone(),
251 None => srsf_transform(&karcher.aligned_data, argvals),
252 };
253 let q_aug = build_augmented_srsfs(&qn, &karcher.aligned_data, n, m);
254 let (q_centered, _mean_q) = center_matrix(&q_aug, n, m_aug);
255
256 let shooting = &horiz.shooting_vectors;
257 let c = match balance_c {
258 Some(c) => c,
259 None => optimize_balance_c(karcher, argvals, &q_centered, shooting, ncomp),
260 };
261
262 let combined = build_combined_representation(&q_centered, shooting, c, n, m_aug, m);
264
265 let svd = SVD::new(combined.to_dmatrix(), true, true);
266 let v_t = svd.v_t.as_ref()?;
267 let (scores, eigenvalues) = svd_scores_and_eigenvalues(&svd, ncomp, n)?;
268 let cumulative_variance = cumulative_variance_from_eigenvalues(&eigenvalues);
269
270 let (vert_component, horiz_component) = split_joint_eigenvectors(v_t, ncomp, m_aug, m);
272
273 Some(JointFpcaResult {
274 scores,
275 eigenvalues,
276 cumulative_variance,
277 balance_c: c,
278 vert_component,
279 horiz_component,
280 })
281}
282
283fn cumulative_variance_from_eigenvalues(eigenvalues: &[f64]) -> Vec<f64> {
287 let total_var: f64 = eigenvalues.iter().sum();
288 let mut cum = Vec::with_capacity(eigenvalues.len());
289 let mut running = 0.0;
290 for ev in eigenvalues {
291 running += ev;
292 cum.push(if total_var > 0.0 {
293 running / total_var
294 } else {
295 0.0
296 });
297 }
298 cum
299}
300
301pub(crate) fn warps_to_normalized_psi(gammas: &FdMatrix, argvals: &[f64]) -> Vec<Vec<f64>> {
303 let (n, m) = gammas.shape();
304 let t0 = argvals[0];
305 let domain = argvals[m - 1] - t0;
306 let time: Vec<f64> = (0..m).map(|i| i as f64 / (m - 1) as f64).collect();
307 let binsize = 1.0 / (m - 1) as f64;
308
309 (0..n)
310 .map(|i| {
311 let gam_01: Vec<f64> = (0..m).map(|j| (gammas[(i, j)] - t0) / domain).collect();
312 let mut grad = vec![0.0; m];
313 grad[0] = (gam_01[1] - gam_01[0]) / binsize;
314 for j in 1..m - 1 {
315 grad[j] = (gam_01[j + 1] - gam_01[j - 1]) / (2.0 * binsize);
316 }
317 grad[m - 1] = (gam_01[m - 1] - gam_01[m - 2]) / binsize;
318 let mut psi: Vec<f64> = grad.iter().map(|&g| g.max(0.0).sqrt()).collect();
319 let norm = l2_norm_l2(&psi, &time);
320 if norm > 1e-10 {
321 for v in &mut psi {
322 *v /= norm;
323 }
324 }
325 psi
326 })
327 .collect()
328}
329
330pub(crate) fn sphere_karcher_mean(psis: &[Vec<f64>], time: &[f64], max_iter: usize) -> Vec<f64> {
332 let n = psis.len();
333 let m = psis[0].len();
334
335 let mut mu_psi = vec![0.0; m];
337 for psi in psis {
338 for j in 0..m {
339 mu_psi[j] += psi[j];
340 }
341 }
342 for j in 0..m {
343 mu_psi[j] /= n as f64;
344 }
345 normalize_to_sphere(&mut mu_psi, time);
346
347 for _ in 0..max_iter {
349 let mean_v = mean_tangent_vector(psis, &mu_psi, time);
350 let step_norm = l2_norm_l2(&mean_v, time);
351 if step_norm < 1e-8 {
352 break;
353 }
354 mu_psi = exp_map_sphere(&mu_psi, &mean_v, time);
355 normalize_to_sphere(&mut mu_psi, time);
356 }
357
358 mu_psi
359}
360
361pub(crate) fn shooting_vectors_from_psis(
363 psis: &[Vec<f64>],
364 mu_psi: &[f64],
365 time: &[f64],
366) -> FdMatrix {
367 let n = psis.len();
368 let m = psis[0].len();
369 let mut shooting = FdMatrix::zeros(n, m);
370 for i in 0..n {
371 let v = inv_exp_map_sphere(mu_psi, &psis[i], time);
372 for j in 0..m {
373 shooting[(i, j)] = v[j];
374 }
375 }
376 shooting
377}
378
379pub(crate) fn build_augmented_srsfs(
381 qn: &FdMatrix,
382 aligned_data: &FdMatrix,
383 n: usize,
384 m: usize,
385) -> FdMatrix {
386 let id = m / 2;
387 let m_aug = m + 1;
388 let mut q_aug = FdMatrix::zeros(n, m_aug);
389 for i in 0..n {
390 for j in 0..m {
391 q_aug[(i, j)] = qn[(i, j)];
392 }
393 let fid = aligned_data[(i, id)];
394 q_aug[(i, m)] = fid.signum() * fid.abs().sqrt();
395 }
396 q_aug
397}
398
399pub(crate) fn center_matrix(mat: &FdMatrix, n: usize, m: usize) -> (FdMatrix, Vec<f64>) {
401 let mut mean = vec![0.0; m];
402 for j in 0..m {
403 for i in 0..n {
404 mean[j] += mat[(i, j)];
405 }
406 mean[j] /= n as f64;
407 }
408 let mut centered = FdMatrix::zeros(n, m);
409 for i in 0..n {
410 for j in 0..m {
411 centered[(i, j)] = mat[(i, j)] - mean[j];
412 }
413 }
414 (centered, mean)
415}
416
417fn svd_scores_and_eigenvalues(
419 svd: &SVD<f64, nalgebra::Dyn, nalgebra::Dyn>,
420 ncomp: usize,
421 n: usize,
422) -> Option<(FdMatrix, Vec<f64>)> {
423 let u = svd.u.as_ref()?;
424 let eigenvalues: Vec<f64> = svd
425 .singular_values
426 .iter()
427 .take(ncomp)
428 .map(|&s| s * s / (n - 1) as f64)
429 .collect();
430 let mut scores = FdMatrix::zeros(n, ncomp);
431 for k in 0..ncomp {
432 let sv = svd.singular_values[k];
433 for i in 0..n {
434 scores[(i, k)] = u[(i, k)] * sv;
435 }
436 }
437 Some((scores, eigenvalues))
438}
439
440fn split_joint_eigenvectors(
442 v_t: &nalgebra::DMatrix<f64>,
443 ncomp: usize,
444 m_aug: usize,
445 m: usize,
446) -> (FdMatrix, FdMatrix) {
447 let mut vert_component = FdMatrix::zeros(ncomp, m_aug);
448 let mut horiz_component = FdMatrix::zeros(ncomp, m);
449 for k in 0..ncomp {
450 for j in 0..m_aug {
451 vert_component[(k, j)] = v_t[(k, j)];
452 }
453 for j in 0..m {
454 horiz_component[(k, j)] = v_t[(k, m_aug + j)];
455 }
456 }
457 (vert_component, horiz_component)
458}
459
460fn build_symmetric_covariance(
462 data: &FdMatrix,
463 mean: &[f64],
464 n: usize,
465 d: usize,
466) -> nalgebra::DMatrix<f64> {
467 let nf = (n - 1) as f64;
468 let mut k_mat = nalgebra::DMatrix::zeros(d, d);
469 for i in 0..n {
470 for p in 0..d {
471 let dp = data[(i, p)] - mean[p];
472 for q in p..d {
473 k_mat[(p, q)] += dp * (data[(i, q)] - mean[q]);
474 }
475 }
476 }
477 for p in 0..d {
478 k_mat[(p, p)] /= nf;
479 for q in (p + 1)..d {
480 k_mat[(p, q)] /= nf;
481 k_mat[(q, p)] = k_mat[(p, q)];
482 }
483 }
484 k_mat
485}
486
487fn project_onto_eigenvectors(
489 data: &FdMatrix,
490 mean: &[f64],
491 u_cov: &nalgebra::DMatrix<f64>,
492 n: usize,
493 d: usize,
494 ncomp: usize,
495) -> FdMatrix {
496 let mut scores = FdMatrix::zeros(n, ncomp);
497 for k in 0..ncomp {
498 for i in 0..n {
499 let mut s = 0.0;
500 for j in 0..d {
501 s += (data[(i, j)] - mean[j]) * u_cov[(j, k)];
502 }
503 scores[(i, k)] = s;
504 }
505 }
506 scores
507}
508
509fn normalize_to_sphere(mu: &mut [f64], time: &[f64]) {
511 let norm = l2_norm_l2(mu, time);
512 if norm > 1e-10 {
513 for v in mu.iter_mut() {
514 *v /= norm;
515 }
516 }
517}
518
519fn mean_tangent_vector(psis: &[Vec<f64>], mu_psi: &[f64], time: &[f64]) -> Vec<f64> {
521 let n = psis.len();
522 let m = mu_psi.len();
523 let mut mean_v = vec![0.0; m];
524 for psi in psis {
525 let v = inv_exp_map_sphere(mu_psi, psi, time);
526 for j in 0..m {
527 mean_v[j] += v[j];
528 }
529 }
530 for j in 0..m {
531 mean_v[j] /= n as f64;
532 }
533 mean_v
534}
535
536fn build_combined_representation(
538 q_centered: &FdMatrix,
539 shooting: &FdMatrix,
540 c: f64,
541 n: usize,
542 m_aug: usize,
543 m: usize,
544) -> FdMatrix {
545 let combined_dim = m_aug + m;
546 let mut combined = FdMatrix::zeros(n, combined_dim);
547 for i in 0..n {
548 for j in 0..m_aug {
549 combined[(i, j)] = q_centered[(i, j)];
550 }
551 for j in 0..m {
552 combined[(i, m_aug + j)] = c * shooting[(i, j)];
553 }
554 }
555 combined
556}
557
558fn optimize_balance_c(
562 _karcher: &KarcherMeanResult,
563 _argvals: &[f64],
564 q_centered: &FdMatrix,
565 shooting: &FdMatrix,
566 ncomp: usize,
567) -> f64 {
568 let (n, m) = shooting.shape();
569 let m_aug = q_centered.ncols();
570 let combined_dim = m_aug + m;
571
572 let golden_ratio = (5.0_f64.sqrt() - 1.0) / 2.0;
573 let mut a = 0.0_f64;
574 let mut b = 10.0_f64;
575
576 let eval_c = |c: f64| -> f64 {
577 let mut combined = FdMatrix::zeros(n, combined_dim);
578 for i in 0..n {
579 for j in 0..m_aug {
580 combined[(i, j)] = q_centered[(i, j)];
581 }
582 for j in 0..m {
583 combined[(i, m_aug + j)] = c * shooting[(i, j)];
584 }
585 }
586
587 let svd = SVD::new(combined.to_dmatrix(), true, true);
588 if let (Some(_u), Some(_v_t)) = (svd.u.as_ref(), svd.v_t.as_ref()) {
589 let nc = ncomp.min(svd.singular_values.len());
590 let total_var: f64 = svd.singular_values.iter().map(|&s| s * s).sum();
592 let explained: f64 = svd.singular_values.iter().take(nc).map(|&s| s * s).sum();
593 total_var - explained
594 } else {
595 f64::INFINITY
596 }
597 };
598
599 for _ in 0..20 {
600 let c1 = b - golden_ratio * (b - a);
601 let c2 = a + golden_ratio * (b - a);
602 if eval_c(c1) < eval_c(c2) {
603 b = c2;
604 } else {
605 a = c1;
606 }
607 }
608
609 (a + b) / 2.0
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::alignment::karcher_mean;
616 use std::f64::consts::PI;
617
618 fn generate_test_data(n: usize, m: usize) -> (FdMatrix, Vec<f64>) {
619 let t: Vec<f64> = (0..m).map(|j| j as f64 / (m - 1) as f64).collect();
620 let mut data = FdMatrix::zeros(n, m);
621 for i in 0..n {
622 let shift = 0.1 * (i as f64 - n as f64 / 2.0);
623 let scale = 1.0 + 0.2 * (i as f64 / n as f64);
624 for j in 0..m {
625 data[(i, j)] = scale * (2.0 * PI * (t[j] + shift)).sin();
626 }
627 }
628 (data, t)
629 }
630
631 #[test]
632 fn test_vert_fpca_basic() {
633 let (data, t) = generate_test_data(15, 51);
634 let km = karcher_mean(&data, &t, 10, 1e-4, 0.0);
635 let result = vert_fpca(&km, &t, 3);
636 assert!(result.is_some(), "vert_fpca should succeed");
637
638 let res = result.unwrap();
639 assert_eq!(res.scores.shape(), (15, 3));
640 assert_eq!(res.eigenvalues.len(), 3);
641 assert_eq!(res.eigenfunctions_q.shape(), (3, 52)); assert_eq!(res.eigenfunctions_f.shape(), (3, 51));
643
644 for ev in &res.eigenvalues {
646 assert!(*ev >= -1e-10, "Eigenvalue should be non-negative: {}", ev);
647 }
648 for i in 1..res.eigenvalues.len() {
649 assert!(
650 res.eigenvalues[i] <= res.eigenvalues[i - 1] + 1e-10,
651 "Eigenvalues should be decreasing"
652 );
653 }
654
655 for i in 1..res.cumulative_variance.len() {
657 assert!(res.cumulative_variance[i] >= res.cumulative_variance[i - 1] - 1e-10);
658 }
659 assert!(*res.cumulative_variance.last().unwrap() <= 1.0 + 1e-10);
660 }
661
662 #[test]
663 fn test_horiz_fpca_basic() {
664 let (data, t) = generate_test_data(15, 51);
665 let km = karcher_mean(&data, &t, 10, 1e-4, 0.0);
666 let result = horiz_fpca(&km, &t, 3);
667 assert!(result.is_some(), "horiz_fpca should succeed");
668
669 let res = result.unwrap();
670 assert_eq!(res.scores.shape(), (15, 3));
671 assert_eq!(res.eigenvalues.len(), 3);
672 assert_eq!(res.eigenfunctions_psi.shape(), (3, 51));
673 assert_eq!(res.shooting_vectors.shape(), (15, 51));
674 }
675
676 #[test]
677 fn test_joint_fpca_basic() {
678 let (data, t) = generate_test_data(15, 51);
679 let km = karcher_mean(&data, &t, 10, 1e-4, 0.0);
680 let result = joint_fpca(&km, &t, 3, Some(1.0));
681 assert!(result.is_some(), "joint_fpca should succeed");
682
683 let res = result.unwrap();
684 assert_eq!(res.scores.shape(), (15, 3));
685 assert_eq!(res.eigenvalues.len(), 3);
686 assert!(res.balance_c >= 0.0);
687 }
688
689 #[test]
690 fn test_joint_fpca_optimize_c() {
691 let (data, t) = generate_test_data(15, 51);
692 let km = karcher_mean(&data, &t, 10, 1e-4, 0.0);
693 let result = joint_fpca(&km, &t, 3, None);
694 assert!(
695 result.is_some(),
696 "joint_fpca with C optimization should succeed"
697 );
698 }
699
700 #[test]
701 fn test_vert_fpca_invalid_input() {
702 let data = FdMatrix::zeros(1, 10); let t: Vec<f64> = (0..10).map(|i| i as f64 / 9.0).collect();
704 let km = KarcherMeanResult {
705 mean: vec![0.0; 10],
706 mean_srsf: vec![0.0; 10],
707 gammas: FdMatrix::zeros(1, 10),
708 aligned_data: data,
709 n_iter: 0,
710 converged: true,
711 aligned_srsfs: None,
712 };
713 assert!(vert_fpca(&km, &t, 3).is_none());
714 }
715}