1use crate::helpers::{simpsons_weights, simpsons_weights_2d, NUMERICAL_EPS};
4use crate::iter_maybe_parallel;
5use crate::matrix::FdMatrix;
6#[cfg(feature = "parallel")]
7use rayon::iter::ParallelIterator;
8
9fn finite_diff_1d(
14 values: impl Fn(usize) -> f64,
15 idx: usize,
16 n_points: usize,
17 step_sizes: &[f64],
18) -> f64 {
19 if idx == 0 {
20 (values(1) - values(0)) / step_sizes[0]
21 } else if idx == n_points - 1 {
22 (values(n_points - 1) - values(n_points - 2)) / step_sizes[n_points - 1]
23 } else {
24 (values(idx + 1) - values(idx - 1)) / step_sizes[idx]
25 }
26}
27
28fn compute_2d_derivatives(
32 get_val: impl Fn(usize, usize) -> f64,
33 si: usize,
34 ti: usize,
35 m1: usize,
36 m2: usize,
37 hs: &[f64],
38 ht: &[f64],
39) -> (f64, f64, f64) {
40 let ds = finite_diff_1d(|s| get_val(s, ti), si, m1, hs);
42
43 let dt = finite_diff_1d(|t| get_val(si, t), ti, m2, ht);
45
46 let denom = hs[si] * ht[ti];
48
49 let (s_lo, s_hi) = if si == 0 {
51 (0, 1)
52 } else if si == m1 - 1 {
53 (m1 - 2, m1 - 1)
54 } else {
55 (si - 1, si + 1)
56 };
57
58 let (t_lo, t_hi) = if ti == 0 {
59 (0, 1)
60 } else if ti == m2 - 1 {
61 (m2 - 2, m2 - 1)
62 } else {
63 (ti - 1, ti + 1)
64 };
65
66 let dsdt = (get_val(s_hi, t_hi) - get_val(s_lo, t_hi) - get_val(s_hi, t_lo)
67 + get_val(s_lo, t_lo))
68 / denom;
69
70 (ds, dt, dsdt)
71}
72
73fn weiszfeld_iteration(data: &FdMatrix, weights: &[f64], max_iter: usize, tol: f64) -> Vec<f64> {
77 let (n, m) = data.shape();
78
79 let mut median: Vec<f64> = (0..m)
81 .map(|j| {
82 let col = data.column(j);
83 col.iter().sum::<f64>() / n as f64
84 })
85 .collect();
86
87 for _ in 0..max_iter {
88 let distances: Vec<f64> = (0..n)
90 .map(|i| {
91 let mut dist_sq = 0.0;
92 for j in 0..m {
93 let diff = data[(i, j)] - median[j];
94 dist_sq += diff * diff * weights[j];
95 }
96 dist_sq.sqrt()
97 })
98 .collect();
99
100 let inv_distances: Vec<f64> = distances
102 .iter()
103 .map(|d| {
104 if *d > NUMERICAL_EPS {
105 1.0 / d
106 } else {
107 1.0 / NUMERICAL_EPS
108 }
109 })
110 .collect();
111
112 let sum_inv_dist: f64 = inv_distances.iter().sum();
113
114 let new_median: Vec<f64> = (0..m)
116 .map(|j| {
117 let mut weighted_sum = 0.0;
118 for i in 0..n {
119 weighted_sum += data[(i, j)] * inv_distances[i];
120 }
121 weighted_sum / sum_inv_dist
122 })
123 .collect();
124
125 let diff: f64 = median
127 .iter()
128 .zip(new_median.iter())
129 .map(|(a, b)| (a - b).abs())
130 .sum::<f64>()
131 / m as f64;
132
133 median = new_median;
134
135 if diff < tol {
136 break;
137 }
138 }
139
140 median
141}
142
143pub fn mean_1d(data: &FdMatrix) -> Vec<f64> {
151 let (n, m) = data.shape();
152 if n == 0 || m == 0 {
153 return Vec::new();
154 }
155
156 iter_maybe_parallel!(0..m)
157 .map(|j| {
158 let col = data.column(j);
159 col.iter().sum::<f64>() / n as f64
160 })
161 .collect()
162}
163
164pub fn mean_2d(data: &FdMatrix) -> Vec<f64> {
168 mean_1d(data)
170}
171
172pub fn center_1d(data: &FdMatrix) -> FdMatrix {
180 let (n, m) = data.shape();
181 if n == 0 || m == 0 {
182 return FdMatrix::zeros(0, 0);
183 }
184
185 let means: Vec<f64> = iter_maybe_parallel!(0..m)
187 .map(|j| {
188 let col = data.column(j);
189 col.iter().sum::<f64>() / n as f64
190 })
191 .collect();
192
193 let mut centered = FdMatrix::zeros(n, m);
195 for j in 0..m {
196 let col = centered.column_mut(j);
197 let src = data.column(j);
198 for i in 0..n {
199 col[i] = src[i] - means[j];
200 }
201 }
202
203 centered
204}
205
206pub fn norm_lp_1d(data: &FdMatrix, argvals: &[f64], p: f64) -> Vec<f64> {
216 let (n, m) = data.shape();
217 if n == 0 || m == 0 || argvals.len() != m {
218 return Vec::new();
219 }
220
221 let weights = simpsons_weights(argvals);
222
223 if (p - 2.0).abs() < 1e-14 {
224 iter_maybe_parallel!(0..n)
225 .map(|i| {
226 let mut integral = 0.0;
227 for j in 0..m {
228 let v = data[(i, j)];
229 integral += v * v * weights[j];
230 }
231 integral.sqrt()
232 })
233 .collect()
234 } else if (p - 1.0).abs() < 1e-14 {
235 iter_maybe_parallel!(0..n)
236 .map(|i| {
237 let mut integral = 0.0;
238 for j in 0..m {
239 integral += data[(i, j)].abs() * weights[j];
240 }
241 integral
242 })
243 .collect()
244 } else {
245 iter_maybe_parallel!(0..n)
246 .map(|i| {
247 let mut integral = 0.0;
248 for j in 0..m {
249 integral += data[(i, j)].abs().powf(p) * weights[j];
250 }
251 integral.powf(1.0 / p)
252 })
253 .collect()
254 }
255}
256
257fn deriv_1d_step(
268 current: &FdMatrix,
269 n: usize,
270 m: usize,
271 h0: f64,
272 hn: f64,
273 h_central: &[f64],
274) -> FdMatrix {
275 let mut next = FdMatrix::zeros(n, m);
276 let src_col0 = current.column(0);
278 let src_col1 = current.column(1);
279 let dst = next.column_mut(0);
280 for i in 0..n {
281 dst[i] = (src_col1[i] - src_col0[i]) / h0;
282 }
283 for j in 1..(m - 1) {
285 let src_prev = current.column(j - 1);
286 let src_next = current.column(j + 1);
287 let dst = next.column_mut(j);
288 let h = h_central[j - 1];
289 for i in 0..n {
290 dst[i] = (src_next[i] - src_prev[i]) / h;
291 }
292 }
293 let src_colm2 = current.column(m - 2);
295 let src_colm1 = current.column(m - 1);
296 let dst = next.column_mut(m - 1);
297 for i in 0..n {
298 dst[i] = (src_colm1[i] - src_colm2[i]) / hn;
299 }
300 next
301}
302
303pub fn deriv_1d(data: &FdMatrix, argvals: &[f64], nderiv: usize) -> FdMatrix {
304 let (n, m) = data.shape();
305 if n == 0 || m == 0 || argvals.len() != m || nderiv < 1 {
306 return FdMatrix::zeros(n, m);
307 }
308
309 let mut current = data.clone();
310
311 let h0 = argvals[1] - argvals[0];
313 let hn = argvals[m - 1] - argvals[m - 2];
314 let h_central: Vec<f64> = (1..(m - 1))
315 .map(|j| argvals[j + 1] - argvals[j - 1])
316 .collect();
317
318 for _ in 0..nderiv {
319 current = deriv_1d_step(¤t, n, m, h0, hn, &h_central);
320 }
321
322 current
323}
324
325pub struct Deriv2DResult {
327 pub ds: FdMatrix,
329 pub dt: FdMatrix,
331 pub dsdt: FdMatrix,
333}
334
335fn compute_step_sizes(argvals: &[f64]) -> Vec<f64> {
339 let m = argvals.len();
340 (0..m)
341 .map(|j| {
342 if j == 0 {
343 argvals[1] - argvals[0]
344 } else if j == m - 1 {
345 argvals[m - 1] - argvals[m - 2]
346 } else {
347 argvals[j + 1] - argvals[j - 1]
348 }
349 })
350 .collect()
351}
352
353fn reassemble_colmajor(rows: &[Vec<f64>], n: usize, ncol: usize) -> FdMatrix {
355 let mut mat = FdMatrix::zeros(n, ncol);
356 for i in 0..n {
357 for j in 0..ncol {
358 mat[(i, j)] = rows[i][j];
359 }
360 }
361 mat
362}
363
364pub fn deriv_2d(
378 data: &FdMatrix,
379 argvals_s: &[f64],
380 argvals_t: &[f64],
381 m1: usize,
382 m2: usize,
383) -> Option<Deriv2DResult> {
384 let n = data.nrows();
385 let ncol = m1 * m2;
386 if n == 0 || ncol == 0 || argvals_s.len() != m1 || argvals_t.len() != m2 {
387 return None;
388 }
389
390 let hs = compute_step_sizes(argvals_s);
391 let ht = compute_step_sizes(argvals_t);
392
393 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
395 .map(|i| {
396 let mut ds = vec![0.0; ncol];
397 let mut dt = vec![0.0; ncol];
398 let mut dsdt = vec![0.0; ncol];
399
400 let get_val = |si: usize, ti: usize| -> f64 { data[(i, si + ti * m1)] };
401
402 for ti in 0..m2 {
403 for si in 0..m1 {
404 let idx = si + ti * m1;
405 let (ds_val, dt_val, dsdt_val) =
406 compute_2d_derivatives(get_val, si, ti, m1, m2, &hs, &ht);
407 ds[idx] = ds_val;
408 dt[idx] = dt_val;
409 dsdt[idx] = dsdt_val;
410 }
411 }
412
413 (ds, dt, dsdt)
414 })
415 .collect();
416
417 let (ds_vecs, (dt_vecs, dsdt_vecs)): (Vec<Vec<f64>>, (Vec<Vec<f64>>, Vec<Vec<f64>>)) =
418 results.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
419
420 Some(Deriv2DResult {
421 ds: reassemble_colmajor(&ds_vecs, n, ncol),
422 dt: reassemble_colmajor(&dt_vecs, n, ncol),
423 dsdt: reassemble_colmajor(&dsdt_vecs, n, ncol),
424 })
425}
426
427pub fn geometric_median_1d(
437 data: &FdMatrix,
438 argvals: &[f64],
439 max_iter: usize,
440 tol: f64,
441) -> Vec<f64> {
442 let (n, m) = data.shape();
443 if n == 0 || m == 0 || argvals.len() != m {
444 return Vec::new();
445 }
446
447 let weights = simpsons_weights(argvals);
448 weiszfeld_iteration(data, &weights, max_iter, tol)
449}
450
451pub fn geometric_median_2d(
462 data: &FdMatrix,
463 argvals_s: &[f64],
464 argvals_t: &[f64],
465 max_iter: usize,
466 tol: f64,
467) -> Vec<f64> {
468 let (n, m) = data.shape();
469 let expected_cols = argvals_s.len() * argvals_t.len();
470 if n == 0 || m == 0 || m != expected_cols {
471 return Vec::new();
472 }
473
474 let weights = simpsons_weights_2d(argvals_s, argvals_t);
475 weiszfeld_iteration(data, &weights, max_iter, tol)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use std::f64::consts::PI;
482
483 fn uniform_grid(n: usize) -> Vec<f64> {
484 (0..n).map(|i| i as f64 / (n - 1) as f64).collect()
485 }
486
487 #[test]
490 fn test_mean_1d() {
491 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0]; let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
497 let mean = mean_1d(&mat);
498 assert_eq!(mean, vec![2.0, 3.0, 4.0]);
499 }
500
501 #[test]
502 fn test_mean_1d_single_sample() {
503 let data = vec![1.0, 2.0, 3.0];
504 let mat = FdMatrix::from_column_major(data, 1, 3).unwrap();
505 let mean = mean_1d(&mat);
506 assert_eq!(mean, vec![1.0, 2.0, 3.0]);
507 }
508
509 #[test]
510 fn test_mean_1d_invalid() {
511 assert!(mean_1d(&FdMatrix::zeros(0, 0)).is_empty());
512 }
513
514 #[test]
515 fn test_mean_2d_delegates() {
516 let data = vec![1.0, 3.0, 2.0, 4.0];
517 let mat = FdMatrix::from_column_major(data, 2, 2).unwrap();
518 let mean1d = mean_1d(&mat);
519 let mean2d = mean_2d(&mat);
520 assert_eq!(mean1d, mean2d);
521 }
522
523 #[test]
526 fn test_center_1d() {
527 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0]; let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
529 let centered = center_1d(&mat);
530 assert_eq!(centered.as_slice(), &[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);
532 }
533
534 #[test]
535 fn test_center_1d_mean_zero() {
536 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0];
537 let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
538 let centered = center_1d(&mat);
539 let centered_mean = mean_1d(¢ered);
540 for m in centered_mean {
541 assert!(m.abs() < 1e-10, "Centered data should have zero mean");
542 }
543 }
544
545 #[test]
546 fn test_center_1d_invalid() {
547 let centered = center_1d(&FdMatrix::zeros(0, 0));
548 assert!(centered.is_empty());
549 }
550
551 #[test]
554 fn test_norm_lp_1d_constant() {
555 let argvals = uniform_grid(21);
557 let data: Vec<f64> = vec![2.0; 21];
558 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
559 let norms = norm_lp_1d(&mat, &argvals, 2.0);
560 assert_eq!(norms.len(), 1);
561 assert!(
562 (norms[0] - 2.0).abs() < 0.1,
563 "L2 norm of constant 2 should be 2"
564 );
565 }
566
567 #[test]
568 fn test_norm_lp_1d_sine() {
569 let argvals = uniform_grid(101);
571 let data: Vec<f64> = argvals.iter().map(|&x| (PI * x).sin()).collect();
572 let mat = FdMatrix::from_column_major(data, 1, 101).unwrap();
573 let norms = norm_lp_1d(&mat, &argvals, 2.0);
574 let expected = 0.5_f64.sqrt();
575 assert!(
576 (norms[0] - expected).abs() < 0.05,
577 "Expected {}, got {}",
578 expected,
579 norms[0]
580 );
581 }
582
583 #[test]
584 fn test_norm_lp_1d_invalid() {
585 assert!(norm_lp_1d(&FdMatrix::zeros(0, 0), &[], 2.0).is_empty());
586 }
587
588 #[test]
591 fn test_deriv_1d_linear() {
592 let argvals = uniform_grid(21);
594 let data = argvals.clone();
595 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
596 let deriv = deriv_1d(&mat, &argvals, 1);
597 for j in 2..19 {
599 assert!(
600 (deriv[(0, j)] - 1.0).abs() < 0.1,
601 "Derivative of x should be 1"
602 );
603 }
604 }
605
606 #[test]
607 fn test_deriv_1d_quadratic() {
608 let argvals = uniform_grid(51);
610 let data: Vec<f64> = argvals.iter().map(|&x| x * x).collect();
611 let mat = FdMatrix::from_column_major(data, 1, 51).unwrap();
612 let deriv = deriv_1d(&mat, &argvals, 1);
613 for j in 5..45 {
615 let expected = 2.0 * argvals[j];
616 assert!(
617 (deriv[(0, j)] - expected).abs() < 0.1,
618 "Derivative of x^2 should be 2x"
619 );
620 }
621 }
622
623 #[test]
624 fn test_deriv_1d_invalid() {
625 let result = deriv_1d(&FdMatrix::zeros(0, 0), &[], 1);
626 assert!(result.is_empty() || result.as_slice().iter().all(|&x| x == 0.0));
627 }
628
629 #[test]
632 fn test_geometric_median_identical_curves() {
633 let argvals = uniform_grid(21);
635 let n = 5;
636 let m = 21;
637 let mut data = vec![0.0; n * m];
638 for i in 0..n {
639 for j in 0..m {
640 data[i + j * n] = (2.0 * PI * argvals[j]).sin();
641 }
642 }
643 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
644 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
645 for j in 0..m {
646 let expected = (2.0 * PI * argvals[j]).sin();
647 assert!(
648 (median[j] - expected).abs() < 0.01,
649 "Median should equal all curves"
650 );
651 }
652 }
653
654 #[test]
655 fn test_geometric_median_converges() {
656 let argvals = uniform_grid(21);
657 let n = 10;
658 let m = 21;
659 let mut data = vec![0.0; n * m];
660 for i in 0..n {
661 for j in 0..m {
662 data[i + j * n] = (i as f64 / n as f64) * argvals[j];
663 }
664 }
665 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
666 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
667 assert_eq!(median.len(), m);
668 assert!(median.iter().all(|&x| x.is_finite()));
669 }
670
671 #[test]
672 fn test_geometric_median_invalid() {
673 assert!(geometric_median_1d(&FdMatrix::zeros(0, 0), &[], 100, 1e-6).is_empty());
674 }
675
676 #[test]
679 fn test_deriv_2d_linear_surface() {
680 let m1 = 11;
683 let m2 = 11;
684 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
685 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
686
687 let n = 1; let ncol = m1 * m2;
689 let mut data = vec![0.0; n * ncol];
690
691 for si in 0..m1 {
692 for ti in 0..m2 {
693 let s = argvals_s[si];
694 let t = argvals_t[ti];
695 let idx = si + ti * m1;
696 data[idx] = 2.0 * s + 3.0 * t;
697 }
698 }
699
700 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
701 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
702
703 for si in 2..(m1 - 2) {
705 for ti in 2..(m2 - 2) {
706 let idx = si + ti * m1;
707 assert!(
708 (result.ds[(0, idx)] - 2.0).abs() < 0.2,
709 "∂f/∂s at ({}, {}) = {}, expected 2",
710 si,
711 ti,
712 result.ds[(0, idx)]
713 );
714 }
715 }
716
717 for si in 2..(m1 - 2) {
719 for ti in 2..(m2 - 2) {
720 let idx = si + ti * m1;
721 assert!(
722 (result.dt[(0, idx)] - 3.0).abs() < 0.2,
723 "∂f/∂t at ({}, {}) = {}, expected 3",
724 si,
725 ti,
726 result.dt[(0, idx)]
727 );
728 }
729 }
730
731 for si in 2..(m1 - 2) {
733 for ti in 2..(m2 - 2) {
734 let idx = si + ti * m1;
735 assert!(
736 result.dsdt[(0, idx)].abs() < 0.5,
737 "∂²f/∂s∂t at ({}, {}) = {}, expected 0",
738 si,
739 ti,
740 result.dsdt[(0, idx)]
741 );
742 }
743 }
744 }
745
746 #[test]
747 fn test_deriv_2d_quadratic_surface() {
748 let m1 = 21;
751 let m2 = 21;
752 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
753 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
754
755 let n = 1;
756 let ncol = m1 * m2;
757 let mut data = vec![0.0; n * ncol];
758
759 for si in 0..m1 {
760 for ti in 0..m2 {
761 let s = argvals_s[si];
762 let t = argvals_t[ti];
763 let idx = si + ti * m1;
764 data[idx] = s * t;
765 }
766 }
767
768 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
769 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
770
771 for si in 3..(m1 - 3) {
773 for ti in 3..(m2 - 3) {
774 let idx = si + ti * m1;
775 let expected = argvals_t[ti];
776 assert!(
777 (result.ds[(0, idx)] - expected).abs() < 0.1,
778 "∂f/∂s at ({}, {}) = {}, expected {}",
779 si,
780 ti,
781 result.ds[(0, idx)],
782 expected
783 );
784 }
785 }
786
787 for si in 3..(m1 - 3) {
789 for ti in 3..(m2 - 3) {
790 let idx = si + ti * m1;
791 let expected = argvals_s[si];
792 assert!(
793 (result.dt[(0, idx)] - expected).abs() < 0.1,
794 "∂f/∂t at ({}, {}) = {}, expected {}",
795 si,
796 ti,
797 result.dt[(0, idx)],
798 expected
799 );
800 }
801 }
802
803 for si in 3..(m1 - 3) {
805 for ti in 3..(m2 - 3) {
806 let idx = si + ti * m1;
807 assert!(
808 (result.dsdt[(0, idx)] - 1.0).abs() < 0.3,
809 "∂²f/∂s∂t at ({}, {}) = {}, expected 1",
810 si,
811 ti,
812 result.dsdt[(0, idx)]
813 );
814 }
815 }
816 }
817
818 #[test]
819 fn test_deriv_2d_invalid_input() {
820 let result = deriv_2d(&FdMatrix::zeros(0, 0), &[], &[], 0, 0);
822 assert!(result.is_none());
823
824 let mat = FdMatrix::from_column_major(vec![1.0; 4], 1, 4).unwrap();
826 let argvals = vec![0.0, 1.0];
827 let result = deriv_2d(&mat, &argvals, &[0.0, 0.5, 1.0], 2, 2);
828 assert!(result.is_none());
829 }
830
831 #[test]
834 fn test_geometric_median_2d_basic() {
835 let m1 = 5;
837 let m2 = 5;
838 let m = m1 * m2;
839 let n = 3;
840 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
841 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
842
843 let mut data = vec![0.0; n * m];
844
845 for i in 0..n {
847 for si in 0..m1 {
848 for ti in 0..m2 {
849 let idx = si + ti * m1;
850 let s = argvals_s[si];
851 let t = argvals_t[ti];
852 data[i + idx * n] = s + t;
853 }
854 }
855 }
856
857 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
858 let median = geometric_median_2d(&mat, &argvals_s, &argvals_t, 100, 1e-6);
859 assert_eq!(median.len(), m);
860
861 for si in 0..m1 {
863 for ti in 0..m2 {
864 let idx = si + ti * m1;
865 let expected = argvals_s[si] + argvals_t[ti];
866 assert!(
867 (median[idx] - expected).abs() < 0.01,
868 "Median at ({}, {}) = {}, expected {}",
869 si,
870 ti,
871 median[idx],
872 expected
873 );
874 }
875 }
876 }
877}