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> {
167 let (n, m) = data.shape();
168 if n == 0 || m == 0 {
169 return Vec::new();
170 }
171
172 iter_maybe_parallel!(0..m)
173 .map(|j| {
174 let col = data.column(j);
175 col.iter().sum::<f64>() / n as f64
176 })
177 .collect()
178}
179
180pub fn mean_2d(data: &FdMatrix) -> Vec<f64> {
184 mean_1d(data)
186}
187
188pub fn center_1d(data: &FdMatrix) -> FdMatrix {
212 let (n, m) = data.shape();
213 if n == 0 || m == 0 {
214 return FdMatrix::zeros(0, 0);
215 }
216
217 let means: Vec<f64> = iter_maybe_parallel!(0..m)
219 .map(|j| {
220 let col = data.column(j);
221 col.iter().sum::<f64>() / n as f64
222 })
223 .collect();
224
225 let mut centered = FdMatrix::zeros(n, m);
227 for j in 0..m {
228 let col = centered.column_mut(j);
229 let src = data.column(j);
230 for i in 0..n {
231 col[i] = src[i] - means[j];
232 }
233 }
234
235 centered
236}
237
238#[derive(Debug, Clone, Copy, PartialEq)]
240#[non_exhaustive]
241pub enum NormalizationMethod {
242 Center,
244 Autoscale,
246 Pareto,
248 Range,
250 CurveCenter,
252 CurveStandardize,
254 CurveRange,
256 CurveLp(f64),
262}
263
264pub fn normalize(data: &FdMatrix, method: NormalizationMethod) -> FdMatrix {
299 match method {
300 NormalizationMethod::CurveLp(_) => {
301 panic!("CurveLp requires argvals — use normalize_with_argvals()")
302 }
303 _ => {
304 let argvals: Vec<f64> = (0..data.ncols())
305 .map(|j| j as f64 / (data.ncols() - 1).max(1) as f64)
306 .collect();
307 normalize_with_argvals(data, &argvals, method)
308 }
309 }
310}
311
312pub fn normalize_with_argvals(
331 data: &FdMatrix,
332 argvals: &[f64],
333 method: NormalizationMethod,
334) -> FdMatrix {
335 let (n, m) = data.shape();
336 if n == 0 || m == 0 {
337 return FdMatrix::zeros(n, m);
338 }
339
340 match method {
341 NormalizationMethod::Center => center_1d(data),
342 NormalizationMethod::Autoscale => column_scale(data, n, m, ScaleKind::StdDev),
343 NormalizationMethod::Pareto => column_scale(data, n, m, ScaleKind::SqrtStdDev),
344 NormalizationMethod::Range => column_scale(data, n, m, ScaleKind::Range),
345 NormalizationMethod::CurveCenter => row_normalize(data, n, m, RowNorm::Center),
346 NormalizationMethod::CurveStandardize => row_normalize(data, n, m, RowNorm::Standardize),
347 NormalizationMethod::CurveRange => row_normalize(data, n, m, RowNorm::Range),
348 NormalizationMethod::CurveLp(p) => curve_lp_normalize(data, argvals, n, m, p),
349 }
350}
351
352#[derive(Clone, Copy)]
353enum ScaleKind {
354 StdDev,
355 SqrtStdDev,
356 Range,
357}
358
359fn column_scale(data: &FdMatrix, n: usize, m: usize, kind: ScaleKind) -> FdMatrix {
360 let mut result = FdMatrix::zeros(n, m);
361 for j in 0..m {
362 let col = data.column(j);
363 let mean = col.iter().sum::<f64>() / n as f64;
364 let scale = match kind {
365 ScaleKind::StdDev => {
366 let var =
367 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
368 var.sqrt()
369 }
370 ScaleKind::SqrtStdDev => {
371 let var =
372 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
373 var.sqrt().sqrt()
374 }
375 ScaleKind::Range => {
376 let min = col.iter().copied().fold(f64::INFINITY, f64::min);
377 let max = col.iter().copied().fold(f64::NEG_INFINITY, f64::max);
378 max - min
379 }
380 };
381 let out = result.column_mut(j);
382 let denom = if scale > 1e-15 { scale } else { 1.0 };
383 for i in 0..n {
384 out[i] = (col[i] - mean) / denom;
385 }
386 }
387 result
388}
389
390#[derive(Clone, Copy)]
391enum RowNorm {
392 Center,
393 Standardize,
394 Range,
395}
396
397fn row_normalize(data: &FdMatrix, n: usize, m: usize, kind: RowNorm) -> FdMatrix {
398 let mut result = FdMatrix::zeros(n, m);
399 for i in 0..n {
400 let row: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
401 let mean = row.iter().sum::<f64>() / m as f64;
402 match kind {
403 RowNorm::Center => {
404 for j in 0..m {
405 result[(i, j)] = row[j] - mean;
406 }
407 }
408 RowNorm::Standardize => {
409 let std = (row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>()
410 / (m - 1).max(1) as f64)
411 .sqrt();
412 let denom = if std > 1e-15 { std } else { 1.0 };
413 for j in 0..m {
414 result[(i, j)] = (row[j] - mean) / denom;
415 }
416 }
417 RowNorm::Range => {
418 let min = row.iter().copied().fold(f64::INFINITY, f64::min);
419 let max = row.iter().copied().fold(f64::NEG_INFINITY, f64::max);
420 let range = max - min;
421 let denom = if range > 1e-15 { range } else { 1.0 };
422 for j in 0..m {
423 result[(i, j)] = (row[j] - min) / denom;
424 }
425 }
426 }
427 }
428 result
429}
430
431fn curve_lp_normalize(data: &FdMatrix, argvals: &[f64], n: usize, m: usize, p: f64) -> FdMatrix {
433 let mut result = FdMatrix::zeros(n, m);
434 if p.is_infinite() {
435 for i in 0..n {
437 let max_abs = (0..m).map(|j| data[(i, j)].abs()).fold(0.0f64, f64::max);
438 let denom = if max_abs > 1e-15 { max_abs } else { 1.0 };
439 for j in 0..m {
440 result[(i, j)] = data[(i, j)] / denom;
441 }
442 }
443 } else {
444 let norms = norm_lp_1d(data, argvals, p);
445 for i in 0..n {
446 let denom = if norms[i] > 1e-15 { norms[i] } else { 1.0 };
447 for j in 0..m {
448 result[(i, j)] = data[(i, j)] / denom;
449 }
450 }
451 }
452 result
453}
454
455pub fn norm_lp_1d(data: &FdMatrix, argvals: &[f64], p: f64) -> Vec<f64> {
465 let (n, m) = data.shape();
466 if n == 0 || m == 0 || argvals.len() != m {
467 return Vec::new();
468 }
469
470 let weights = simpsons_weights(argvals);
471
472 if (p - 2.0).abs() < 1e-14 {
473 iter_maybe_parallel!(0..n)
474 .map(|i| {
475 let mut integral = 0.0;
476 for j in 0..m {
477 let v = data[(i, j)];
478 integral += v * v * weights[j];
479 }
480 integral.sqrt()
481 })
482 .collect()
483 } else if (p - 1.0).abs() < 1e-14 {
484 iter_maybe_parallel!(0..n)
485 .map(|i| {
486 let mut integral = 0.0;
487 for j in 0..m {
488 integral += data[(i, j)].abs() * weights[j];
489 }
490 integral
491 })
492 .collect()
493 } else {
494 iter_maybe_parallel!(0..n)
495 .map(|i| {
496 let mut integral = 0.0;
497 for j in 0..m {
498 integral += data[(i, j)].abs().powf(p) * weights[j];
499 }
500 integral.powf(1.0 / p)
501 })
502 .collect()
503 }
504}
505
506fn deriv_1d_step(
532 current: &FdMatrix,
533 n: usize,
534 m: usize,
535 h0: f64,
536 hn: f64,
537 h_central: &[f64],
538) -> FdMatrix {
539 let mut next = FdMatrix::zeros(n, m);
540 let src_col0 = current.column(0);
542 let src_col1 = current.column(1);
543 let dst = next.column_mut(0);
544 for i in 0..n {
545 dst[i] = (src_col1[i] - src_col0[i]) / h0;
546 }
547 for j in 1..(m - 1) {
549 let src_prev = current.column(j - 1);
550 let src_next = current.column(j + 1);
551 let dst = next.column_mut(j);
552 let h = h_central[j - 1];
553 for i in 0..n {
554 dst[i] = (src_next[i] - src_prev[i]) / h;
555 }
556 }
557 let src_colm2 = current.column(m - 2);
559 let src_colm1 = current.column(m - 1);
560 let dst = next.column_mut(m - 1);
561 for i in 0..n {
562 dst[i] = (src_colm1[i] - src_colm2[i]) / hn;
563 }
564 next
565}
566
567pub fn deriv_1d(data: &FdMatrix, argvals: &[f64], nderiv: usize) -> FdMatrix {
568 let (n, m) = data.shape();
569 if n == 0 || m < 2 || argvals.len() != m {
570 return FdMatrix::zeros(n, m);
571 }
572 if nderiv == 0 {
573 return data.clone();
574 }
575
576 let mut current = data.clone();
577
578 let h0 = argvals[1] - argvals[0];
580 let hn = argvals[m - 1] - argvals[m - 2];
581 let h_central: Vec<f64> = (1..(m - 1))
582 .map(|j| argvals[j + 1] - argvals[j - 1])
583 .collect();
584
585 for _ in 0..nderiv {
586 current = deriv_1d_step(¤t, n, m, h0, hn, &h_central);
587 }
588
589 current
590}
591
592#[derive(Debug, Clone, PartialEq)]
594#[non_exhaustive]
595pub struct Deriv2DResult {
596 pub ds: FdMatrix,
598 pub dt: FdMatrix,
600 pub dsdt: FdMatrix,
602}
603
604fn compute_step_sizes(argvals: &[f64]) -> Vec<f64> {
608 let m = argvals.len();
609 if m < 2 {
610 return vec![1.0; m];
611 }
612 (0..m)
613 .map(|j| {
614 if j == 0 {
615 argvals[1] - argvals[0]
616 } else if j == m - 1 {
617 argvals[m - 1] - argvals[m - 2]
618 } else {
619 argvals[j + 1] - argvals[j - 1]
620 }
621 })
622 .collect()
623}
624
625fn reassemble_colmajor(rows: &[Vec<f64>], n: usize, ncol: usize) -> FdMatrix {
627 let mut mat = FdMatrix::zeros(n, ncol);
628 for i in 0..n {
629 for j in 0..ncol {
630 mat[(i, j)] = rows[i][j];
631 }
632 }
633 mat
634}
635
636pub fn deriv_2d(
650 data: &FdMatrix,
651 argvals_s: &[f64],
652 argvals_t: &[f64],
653 m1: usize,
654 m2: usize,
655) -> Option<Deriv2DResult> {
656 let n = data.nrows();
657 let ncol = m1 * m2;
658 if n == 0
659 || ncol == 0
660 || m1 < 2
661 || m2 < 2
662 || data.ncols() != ncol
663 || argvals_s.len() != m1
664 || argvals_t.len() != m2
665 {
666 return None;
667 }
668
669 let hs = compute_step_sizes(argvals_s);
670 let ht = compute_step_sizes(argvals_t);
671
672 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
674 .map(|i| {
675 let mut ds = vec![0.0; ncol];
676 let mut dt = vec![0.0; ncol];
677 let mut dsdt = vec![0.0; ncol];
678
679 let get_val = |si: usize, ti: usize| -> f64 { data[(i, si + ti * m1)] };
680
681 for ti in 0..m2 {
682 for si in 0..m1 {
683 let idx = si + ti * m1;
684 let (ds_val, dt_val, dsdt_val) =
685 compute_2d_derivatives(get_val, si, ti, m1, m2, &hs, &ht);
686 ds[idx] = ds_val;
687 dt[idx] = dt_val;
688 dsdt[idx] = dsdt_val;
689 }
690 }
691
692 (ds, dt, dsdt)
693 })
694 .collect();
695
696 let (ds_vecs, (dt_vecs, dsdt_vecs)): (Vec<Vec<f64>>, (Vec<Vec<f64>>, Vec<Vec<f64>>)) =
697 results.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
698
699 Some(Deriv2DResult {
700 ds: reassemble_colmajor(&ds_vecs, n, ncol),
701 dt: reassemble_colmajor(&dt_vecs, n, ncol),
702 dsdt: reassemble_colmajor(&dsdt_vecs, n, ncol),
703 })
704}
705
706pub fn geometric_median_1d(
716 data: &FdMatrix,
717 argvals: &[f64],
718 max_iter: usize,
719 tol: f64,
720) -> Vec<f64> {
721 let (n, m) = data.shape();
722 if n == 0 || m == 0 || argvals.len() != m {
723 return Vec::new();
724 }
725
726 let weights = simpsons_weights(argvals);
727 weiszfeld_iteration(data, &weights, max_iter, tol)
728}
729
730pub fn geometric_median_2d(
741 data: &FdMatrix,
742 argvals_s: &[f64],
743 argvals_t: &[f64],
744 max_iter: usize,
745 tol: f64,
746) -> Vec<f64> {
747 let (n, m) = data.shape();
748 let expected_cols = argvals_s.len() * argvals_t.len();
749 if n == 0 || m == 0 || m != expected_cols {
750 return Vec::new();
751 }
752
753 let weights = simpsons_weights_2d(argvals_s, argvals_t);
754 weiszfeld_iteration(data, &weights, max_iter, tol)
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use crate::test_helpers::uniform_grid;
761 use std::f64::consts::PI;
762
763 #[test]
766 fn test_mean_1d() {
767 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();
773 let mean = mean_1d(&mat);
774 assert_eq!(mean, vec![2.0, 3.0, 4.0]);
775 }
776
777 #[test]
778 fn test_mean_1d_single_sample() {
779 let data = vec![1.0, 2.0, 3.0];
780 let mat = FdMatrix::from_column_major(data, 1, 3).unwrap();
781 let mean = mean_1d(&mat);
782 assert_eq!(mean, vec![1.0, 2.0, 3.0]);
783 }
784
785 #[test]
786 fn test_mean_1d_invalid() {
787 assert!(mean_1d(&FdMatrix::zeros(0, 0)).is_empty());
788 }
789
790 #[test]
791 fn test_mean_2d_delegates() {
792 let data = vec![1.0, 3.0, 2.0, 4.0];
793 let mat = FdMatrix::from_column_major(data, 2, 2).unwrap();
794 let mean1d = mean_1d(&mat);
795 let mean2d = mean_2d(&mat);
796 assert_eq!(mean1d, mean2d);
797 }
798
799 #[test]
802 fn test_center_1d() {
803 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();
805 let centered = center_1d(&mat);
806 assert_eq!(centered.as_slice(), &[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);
808 }
809
810 #[test]
811 fn test_center_1d_mean_zero() {
812 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0];
813 let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
814 let centered = center_1d(&mat);
815 let centered_mean = mean_1d(¢ered);
816 for m in centered_mean {
817 assert!(m.abs() < 1e-10, "Centered data should have zero mean");
818 }
819 }
820
821 #[test]
822 fn test_center_1d_invalid() {
823 let centered = center_1d(&FdMatrix::zeros(0, 0));
824 assert!(centered.is_empty());
825 }
826
827 #[test]
830 fn test_norm_lp_1d_constant() {
831 let argvals = uniform_grid(21);
833 let data: Vec<f64> = vec![2.0; 21];
834 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
835 let norms = norm_lp_1d(&mat, &argvals, 2.0);
836 assert_eq!(norms.len(), 1);
837 assert!(
838 (norms[0] - 2.0).abs() < 0.1,
839 "L2 norm of constant 2 should be 2"
840 );
841 }
842
843 #[test]
844 fn test_norm_lp_1d_sine() {
845 let argvals = uniform_grid(101);
847 let data: Vec<f64> = argvals.iter().map(|&x| (PI * x).sin()).collect();
848 let mat = FdMatrix::from_column_major(data, 1, 101).unwrap();
849 let norms = norm_lp_1d(&mat, &argvals, 2.0);
850 let expected = 0.5_f64.sqrt();
851 assert!(
852 (norms[0] - expected).abs() < 0.05,
853 "Expected {}, got {}",
854 expected,
855 norms[0]
856 );
857 }
858
859 #[test]
860 fn test_norm_lp_1d_invalid() {
861 assert!(norm_lp_1d(&FdMatrix::zeros(0, 0), &[], 2.0).is_empty());
862 }
863
864 #[test]
867 fn test_deriv_1d_linear() {
868 let argvals = uniform_grid(21);
870 let data = argvals.clone();
871 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
872 let deriv = deriv_1d(&mat, &argvals, 1);
873 for j in 2..19 {
875 assert!(
876 (deriv[(0, j)] - 1.0).abs() < 0.1,
877 "Derivative of x should be 1"
878 );
879 }
880 }
881
882 #[test]
883 fn test_deriv_1d_quadratic() {
884 let argvals = uniform_grid(51);
886 let data: Vec<f64> = argvals.iter().map(|&x| x * x).collect();
887 let mat = FdMatrix::from_column_major(data, 1, 51).unwrap();
888 let deriv = deriv_1d(&mat, &argvals, 1);
889 for j in 5..45 {
891 let expected = 2.0 * argvals[j];
892 assert!(
893 (deriv[(0, j)] - expected).abs() < 0.1,
894 "Derivative of x^2 should be 2x"
895 );
896 }
897 }
898
899 #[test]
900 fn test_deriv_1d_invalid() {
901 let result = deriv_1d(&FdMatrix::zeros(0, 0), &[], 1);
902 assert!(result.is_empty() || result.as_slice().iter().all(|&x| x == 0.0));
903 }
904
905 #[test]
908 fn test_geometric_median_identical_curves() {
909 let argvals = uniform_grid(21);
911 let n = 5;
912 let m = 21;
913 let mut data = vec![0.0; n * m];
914 for i in 0..n {
915 for j in 0..m {
916 data[i + j * n] = (2.0 * PI * argvals[j]).sin();
917 }
918 }
919 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
920 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
921 for j in 0..m {
922 let expected = (2.0 * PI * argvals[j]).sin();
923 assert!(
924 (median[j] - expected).abs() < 0.01,
925 "Median should equal all curves"
926 );
927 }
928 }
929
930 #[test]
931 fn test_geometric_median_converges() {
932 let argvals = uniform_grid(21);
933 let n = 10;
934 let m = 21;
935 let mut data = vec![0.0; n * m];
936 for i in 0..n {
937 for j in 0..m {
938 data[i + j * n] = (i as f64 / n as f64) * argvals[j];
939 }
940 }
941 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
942 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
943 assert_eq!(median.len(), m);
944 assert!(median.iter().all(|&x| x.is_finite()));
945 }
946
947 #[test]
948 fn test_geometric_median_invalid() {
949 assert!(geometric_median_1d(&FdMatrix::zeros(0, 0), &[], 100, 1e-6).is_empty());
950 }
951
952 #[test]
955 fn test_deriv_2d_linear_surface() {
956 let m1 = 11;
959 let m2 = 11;
960 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
961 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
962
963 let n = 1; let ncol = m1 * m2;
965 let mut data = vec![0.0; n * ncol];
966
967 for si in 0..m1 {
968 for ti in 0..m2 {
969 let s = argvals_s[si];
970 let t = argvals_t[ti];
971 let idx = si + ti * m1;
972 data[idx] = 2.0 * s + 3.0 * t;
973 }
974 }
975
976 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
977 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
978
979 for si in 2..(m1 - 2) {
981 for ti in 2..(m2 - 2) {
982 let idx = si + ti * m1;
983 assert!(
984 (result.ds[(0, idx)] - 2.0).abs() < 0.2,
985 "∂f/∂s at ({}, {}) = {}, expected 2",
986 si,
987 ti,
988 result.ds[(0, idx)]
989 );
990 }
991 }
992
993 for si in 2..(m1 - 2) {
995 for ti in 2..(m2 - 2) {
996 let idx = si + ti * m1;
997 assert!(
998 (result.dt[(0, idx)] - 3.0).abs() < 0.2,
999 "∂f/∂t at ({}, {}) = {}, expected 3",
1000 si,
1001 ti,
1002 result.dt[(0, idx)]
1003 );
1004 }
1005 }
1006
1007 for si in 2..(m1 - 2) {
1009 for ti in 2..(m2 - 2) {
1010 let idx = si + ti * m1;
1011 assert!(
1012 result.dsdt[(0, idx)].abs() < 0.5,
1013 "∂²f/∂s∂t at ({}, {}) = {}, expected 0",
1014 si,
1015 ti,
1016 result.dsdt[(0, idx)]
1017 );
1018 }
1019 }
1020 }
1021
1022 #[test]
1023 fn test_deriv_2d_quadratic_surface() {
1024 let m1 = 21;
1027 let m2 = 21;
1028 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1029 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1030
1031 let n = 1;
1032 let ncol = m1 * m2;
1033 let mut data = vec![0.0; n * ncol];
1034
1035 for si in 0..m1 {
1036 for ti in 0..m2 {
1037 let s = argvals_s[si];
1038 let t = argvals_t[ti];
1039 let idx = si + ti * m1;
1040 data[idx] = s * t;
1041 }
1042 }
1043
1044 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1045 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
1046
1047 for si in 3..(m1 - 3) {
1049 for ti in 3..(m2 - 3) {
1050 let idx = si + ti * m1;
1051 let expected = argvals_t[ti];
1052 assert!(
1053 (result.ds[(0, idx)] - expected).abs() < 0.1,
1054 "∂f/∂s at ({}, {}) = {}, expected {}",
1055 si,
1056 ti,
1057 result.ds[(0, idx)],
1058 expected
1059 );
1060 }
1061 }
1062
1063 for si in 3..(m1 - 3) {
1065 for ti in 3..(m2 - 3) {
1066 let idx = si + ti * m1;
1067 let expected = argvals_s[si];
1068 assert!(
1069 (result.dt[(0, idx)] - expected).abs() < 0.1,
1070 "∂f/∂t at ({}, {}) = {}, expected {}",
1071 si,
1072 ti,
1073 result.dt[(0, idx)],
1074 expected
1075 );
1076 }
1077 }
1078
1079 for si in 3..(m1 - 3) {
1081 for ti in 3..(m2 - 3) {
1082 let idx = si + ti * m1;
1083 assert!(
1084 (result.dsdt[(0, idx)] - 1.0).abs() < 0.3,
1085 "∂²f/∂s∂t at ({}, {}) = {}, expected 1",
1086 si,
1087 ti,
1088 result.dsdt[(0, idx)]
1089 );
1090 }
1091 }
1092 }
1093
1094 #[test]
1095 fn test_deriv_2d_invalid_input() {
1096 let result = deriv_2d(&FdMatrix::zeros(0, 0), &[], &[], 0, 0);
1098 assert!(result.is_none());
1099
1100 let mat = FdMatrix::from_column_major(vec![1.0; 4], 1, 4).unwrap();
1102 let argvals = vec![0.0, 1.0];
1103 let result = deriv_2d(&mat, &argvals, &[0.0, 0.5, 1.0], 2, 2);
1104 assert!(result.is_none());
1105 }
1106
1107 #[test]
1110 fn test_geometric_median_2d_basic() {
1111 let m1 = 5;
1113 let m2 = 5;
1114 let m = m1 * m2;
1115 let n = 3;
1116 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1117 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1118
1119 let mut data = vec![0.0; n * m];
1120
1121 for i in 0..n {
1123 for si in 0..m1 {
1124 for ti in 0..m2 {
1125 let idx = si + ti * m1;
1126 let s = argvals_s[si];
1127 let t = argvals_t[ti];
1128 data[i + idx * n] = s + t;
1129 }
1130 }
1131 }
1132
1133 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1134 let median = geometric_median_2d(&mat, &argvals_s, &argvals_t, 100, 1e-6);
1135 assert_eq!(median.len(), m);
1136
1137 for si in 0..m1 {
1139 for ti in 0..m2 {
1140 let idx = si + ti * m1;
1141 let expected = argvals_s[si] + argvals_t[ti];
1142 assert!(
1143 (median[idx] - expected).abs() < 0.01,
1144 "Median at ({}, {}) = {}, expected {}",
1145 si,
1146 ti,
1147 median[idx],
1148 expected
1149 );
1150 }
1151 }
1152 }
1153
1154 #[test]
1155 fn test_nan_mean_no_panic() {
1156 let mut data_vec = vec![1.0; 6];
1157 data_vec[2] = f64::NAN;
1158 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1159 let m = mean_1d(&data);
1160 assert_eq!(m.len(), 3);
1161 }
1162
1163 #[test]
1164 fn test_nan_center_no_panic() {
1165 let mut data_vec = vec![1.0; 6];
1166 data_vec[2] = f64::NAN;
1167 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1168 let c = center_1d(&data);
1169 assert_eq!(c.nrows(), 2);
1170 }
1171
1172 #[test]
1173 fn test_nan_norm_no_panic() {
1174 let mut data_vec = vec![1.0; 6];
1175 data_vec[2] = f64::NAN;
1176 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1177 let argvals = vec![0.0, 0.5, 1.0];
1178 let norms = norm_lp_1d(&data, &argvals, 2.0);
1179 assert_eq!(norms.len(), 2);
1180 }
1181
1182 #[test]
1183 fn test_n1_norm() {
1184 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0], 1, 3).unwrap();
1185 let argvals = vec![0.0, 0.5, 1.0];
1186 let norms = norm_lp_1d(&data, &argvals, 2.0);
1187 assert_eq!(norms.len(), 1);
1188 assert!(norms[0] > 0.0);
1189 }
1190
1191 #[test]
1192 fn test_n2_center() {
1193 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0], 2, 2).unwrap();
1194 let centered = center_1d(&data);
1195 assert!((centered[(0, 0)] - (-1.0)).abs() < 1e-12);
1198 assert!((centered[(1, 0)] - 1.0).abs() < 1e-12);
1199 }
1200
1201 #[test]
1202 fn test_deriv_nderiv0() {
1203 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).unwrap();
1205 let argvals = vec![0.0, 0.5, 1.0];
1206 let result = deriv_1d(&data, &argvals, 0);
1207 assert_eq!(result.shape(), data.shape());
1208 for i in 0..2 {
1209 for j in 0..3 {
1210 assert!((result[(i, j)] - data[(i, j)]).abs() < 1e-12);
1211 }
1212 }
1213 }
1214
1215 #[test]
1218 fn test_normalize_autoscale() {
1219 let data = FdMatrix::from_column_major(
1221 vec![
1222 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1223 ],
1224 3,
1225 4,
1226 )
1227 .unwrap();
1228 let scaled = normalize(&data, NormalizationMethod::Autoscale);
1229 for j in 0..4 {
1231 let col = scaled.column(j);
1232 let mean = col.iter().sum::<f64>() / 3.0;
1233 assert!(
1234 mean.abs() < 1e-10,
1235 "column {j} mean should be 0, got {mean}"
1236 );
1237 let var = col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1238 assert!(
1239 (var - 1.0).abs() < 1e-10,
1240 "column {j} variance should be 1, got {var}"
1241 );
1242 }
1243 }
1244
1245 #[test]
1246 fn test_normalize_pareto() {
1247 let data =
1248 FdMatrix::from_column_major(vec![1.0, 5.0, 3.0, 10.0, 20.0, 30.0], 2, 3).unwrap();
1249 let scaled = normalize(&data, NormalizationMethod::Pareto);
1250 for j in 0..3 {
1252 let col = scaled.column(j);
1253 let mean = col.iter().sum::<f64>() / 2.0;
1254 assert!(mean.abs() < 1e-10, "column {j} mean should be 0");
1255 }
1256 }
1257
1258 #[test]
1259 fn test_normalize_range() {
1260 let data = FdMatrix::from_column_major(vec![0.0, 10.0, 2.0, 8.0], 2, 2).unwrap();
1261 let scaled = normalize(&data, NormalizationMethod::Range);
1262 assert!((scaled[(0, 0)] - (-0.5)).abs() < 1e-10);
1264 assert!((scaled[(1, 0)] - 0.5).abs() < 1e-10);
1265 }
1266
1267 #[test]
1268 fn test_normalize_curve_center() {
1269 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1270 let result = normalize(&data, NormalizationMethod::CurveCenter);
1271 assert!((result[(0, 0)] - (-2.0)).abs() < 1e-10);
1273 assert!((result[(0, 1)] - 0.0).abs() < 1e-10);
1274 assert!((result[(0, 2)] - 2.0).abs() < 1e-10);
1275 }
1276
1277 #[test]
1278 fn test_normalize_curve_standardize() {
1279 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1280 let result = normalize(&data, NormalizationMethod::CurveStandardize);
1281 for i in 0..2 {
1283 let row: Vec<f64> = (0..3).map(|j| result[(i, j)]).collect();
1284 let mean = row.iter().sum::<f64>() / 3.0;
1285 assert!(mean.abs() < 1e-10, "row {i} mean should be 0");
1286 let var = row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1287 assert!((var - 1.0).abs() < 1e-10, "row {i} variance should be 1");
1288 }
1289 }
1290
1291 #[test]
1292 fn test_normalize_curve_range() {
1293 let data =
1294 FdMatrix::from_column_major(vec![2.0, 10.0, 4.0, 20.0, 6.0, 30.0], 2, 3).unwrap();
1295 let result = normalize(&data, NormalizationMethod::CurveRange);
1296 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1298 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1299 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1300 }
1301
1302 #[test]
1303 fn test_normalize_center_matches_center_1d() {
1304 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0], 2, 3).unwrap();
1305 let a = center_1d(&data);
1306 let b = normalize(&data, NormalizationMethod::Center);
1307 assert_eq!(a.as_slice(), b.as_slice());
1308 }
1309
1310 #[test]
1311 fn test_normalize_curve_lp_l2() {
1312 let data = FdMatrix::from_column_major(vec![3.0, 0.0, 0.0, 4.0, 0.0, 0.0], 2, 3).unwrap();
1314 let t = vec![0.0, 0.5, 1.0];
1315 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1316 let norms = norm_lp_1d(&result, &t, 2.0);
1319 assert!(
1320 (norms[0] - 1.0).abs() < 0.1,
1321 "L2 norm after normalization should be ≈ 1, got {}",
1322 norms[0]
1323 );
1324 }
1325
1326 #[test]
1327 fn test_normalize_curve_lp_l1() {
1328 let data = FdMatrix::from_column_major(vec![2.0, 4.0, 6.0, 8.0], 2, 2).unwrap();
1329 let t = vec![0.0, 1.0];
1330 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(1.0));
1331 let norms = norm_lp_1d(&result, &t, 1.0);
1333 for (i, &norm) in norms.iter().enumerate() {
1334 assert!(
1335 (norm - 1.0).abs() < 0.1,
1336 "curve {i} L1 norm after normalization should be ≈ 1, got {norm}"
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn test_normalize_curve_lp_linf() {
1343 let data =
1344 FdMatrix::from_column_major(vec![2.0, -5.0, 4.0, -10.0, 6.0, 15.0], 2, 3).unwrap();
1345 let t = vec![0.0, 0.5, 1.0];
1346 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(f64::INFINITY));
1347 for i in 0..2 {
1349 let max_abs: f64 = (0..3).map(|j| result[(i, j)].abs()).fold(0.0, f64::max);
1350 assert!(
1351 (max_abs - 1.0).abs() < 1e-10,
1352 "curve {i} max abs after L-inf normalization should be 1, got {max_abs}"
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 fn test_normalize_curve_lp_zero_curve() {
1359 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0, 2.0], 2, 2).unwrap();
1361 let t = vec![0.0, 1.0];
1362 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1363 assert!((result[(0, 0)]).abs() < 1e-15);
1365 assert!((result[(0, 1)]).abs() < 1e-15);
1366 assert!(result[(1, 0)].abs() > 0.0);
1368 }
1369}