1use crate::dim::Dim;
4use crate::error::FdarError;
5use crate::helpers::{simpsons_weights, simpsons_weights_2d, NUMERICAL_EPS};
6use crate::iter_maybe_parallel;
7use crate::matrix::FdMatrix;
8#[cfg(feature = "parallel")]
9use rayon::iter::ParallelIterator;
10
11fn finite_diff_1d(
16 values: impl Fn(usize) -> f64,
17 idx: usize,
18 n_points: usize,
19 step_sizes: &[f64],
20) -> f64 {
21 if idx == 0 {
22 (values(1) - values(0)) / step_sizes[0]
23 } else if idx == n_points - 1 {
24 (values(n_points - 1) - values(n_points - 2)) / step_sizes[n_points - 1]
25 } else {
26 (values(idx + 1) - values(idx - 1)) / step_sizes[idx]
27 }
28}
29
30fn compute_2d_derivatives(
34 get_val: impl Fn(usize, usize) -> f64,
35 si: usize,
36 ti: usize,
37 m1: usize,
38 m2: usize,
39 hs: &[f64],
40 ht: &[f64],
41) -> (f64, f64, f64) {
42 let ds = finite_diff_1d(|s| get_val(s, ti), si, m1, hs);
44
45 let dt = finite_diff_1d(|t| get_val(si, t), ti, m2, ht);
47
48 let denom = hs[si] * ht[ti];
50
51 let (s_lo, s_hi) = if si == 0 {
53 (0, 1)
54 } else if si == m1 - 1 {
55 (m1 - 2, m1 - 1)
56 } else {
57 (si - 1, si + 1)
58 };
59
60 let (t_lo, t_hi) = if ti == 0 {
61 (0, 1)
62 } else if ti == m2 - 1 {
63 (m2 - 2, m2 - 1)
64 } else {
65 (ti - 1, ti + 1)
66 };
67
68 let dsdt = (get_val(s_hi, t_hi) - get_val(s_lo, t_hi) - get_val(s_hi, t_lo)
69 + get_val(s_lo, t_lo))
70 / denom;
71
72 (ds, dt, dsdt)
73}
74
75fn weiszfeld_iteration(data: &FdMatrix, weights: &[f64], max_iter: usize, tol: f64) -> Vec<f64> {
79 let (n, m) = data.shape();
80
81 let mut median: Vec<f64> = (0..m)
83 .map(|j| {
84 let col = data.column(j);
85 col.iter().sum::<f64>() / n as f64
86 })
87 .collect();
88
89 for _ in 0..max_iter {
90 let distances: Vec<f64> = (0..n)
92 .map(|i| {
93 let mut dist_sq = 0.0;
94 for j in 0..m {
95 let diff = data[(i, j)] - median[j];
96 dist_sq += diff * diff * weights[j];
97 }
98 dist_sq.sqrt()
99 })
100 .collect();
101
102 let inv_distances: Vec<f64> = distances
104 .iter()
105 .map(|d| {
106 if *d > NUMERICAL_EPS {
107 1.0 / d
108 } else {
109 1.0 / NUMERICAL_EPS
110 }
111 })
112 .collect();
113
114 let sum_inv_dist: f64 = inv_distances.iter().sum();
115
116 let new_median: Vec<f64> = (0..m)
118 .map(|j| {
119 let mut weighted_sum = 0.0;
120 for i in 0..n {
121 weighted_sum += data[(i, j)] * inv_distances[i];
122 }
123 weighted_sum / sum_inv_dist
124 })
125 .collect();
126
127 let diff: f64 = median
129 .iter()
130 .zip(new_median.iter())
131 .map(|(a, b)| (a - b).abs())
132 .sum::<f64>()
133 / m as f64;
134
135 median = new_median;
136
137 if diff < tol {
138 break;
139 }
140 }
141
142 median
143}
144
145pub(crate) fn mean_1d(data: &FdMatrix) -> Vec<f64> {
170 let (n, m) = data.shape();
171 if n == 0 || m == 0 {
172 return Vec::new();
173 }
174
175 iter_maybe_parallel!(0..m)
176 .map(|j| {
177 let col = data.column(j);
178 col.iter().sum::<f64>() / n as f64
179 })
180 .collect()
181}
182
183#[must_use = "expensive computation whose result should not be discarded"]
194pub fn mean(data: &FdMatrix, dim: Dim) -> Vec<f64> {
195 match dim {
196 Dim::One | Dim::Two => mean_1d(data),
197 }
198}
199
200pub(crate) fn center_1d(data: &FdMatrix) -> FdMatrix {
225 let (n, m) = data.shape();
226 if n == 0 || m == 0 {
227 return FdMatrix::zeros(0, 0);
228 }
229
230 let means: Vec<f64> = iter_maybe_parallel!(0..m)
232 .map(|j| {
233 let col = data.column(j);
234 col.iter().sum::<f64>() / n as f64
235 })
236 .collect();
237
238 let mut centered = FdMatrix::zeros(n, m);
240 for j in 0..m {
241 let col = centered.column_mut(j);
242 let src = data.column(j);
243 for i in 0..n {
244 col[i] = src[i] - means[j];
245 }
246 }
247
248 centered
249}
250
251pub fn center(data: &FdMatrix, dim: Dim) -> FdMatrix {
261 match dim {
262 Dim::One | Dim::Two => center_1d(data),
263 }
264}
265
266pub fn functional_variance(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
296 let (n, m) = data.shape();
297 if n < 2 {
298 return Err(FdarError::InvalidDimension {
299 parameter: "data",
300 expected: ">= 2 rows".to_string(),
301 actual: n.to_string(),
302 });
303 }
304 let means = mean_1d(data);
305 let var: Vec<f64> = (0..m)
306 .map(|j| {
307 let col = data.column(j);
308 let mu = means[j];
309 col.iter().map(|&x| (x - mu).powi(2)).sum::<f64>() / (n - 1) as f64
310 })
311 .collect();
312 Ok(var)
313}
314
315pub fn functional_std(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
344 Ok(functional_variance(data)?
345 .iter()
346 .map(|v| v.sqrt())
347 .collect())
348}
349
350pub fn functional_covariance(data: &FdMatrix) -> Result<FdMatrix, FdarError> {
386 let (n, m) = data.shape();
387 if n < 2 {
388 return Err(FdarError::InvalidDimension {
389 parameter: "data",
390 expected: ">= 2 rows".to_string(),
391 actual: n.to_string(),
392 });
393 }
394 m.checked_mul(m)
396 .ok_or_else(|| FdarError::InvalidParameter {
397 parameter: "data",
398 message: format!(
399 "m={m} is too large: m*m would overflow usize (max {})",
400 usize::MAX
401 ),
402 })?;
403
404 let centered = center_1d(data);
405 let mut cov = FdMatrix::zeros(m, m);
406 let denom = (n - 1) as f64;
407 for j1 in 0..m {
408 let col1 = centered.column(j1);
409 for j2 in j1..m {
410 let col2 = centered.column(j2);
411 let val: f64 = col1
412 .iter()
413 .zip(col2.iter())
414 .map(|(&a, &b)| a * b)
415 .sum::<f64>()
416 / denom;
417 cov[(j1, j2)] = val;
418 cov[(j2, j1)] = val; }
420 }
421 Ok(cov)
422}
423
424pub fn depth_based_median(data: &FdMatrix) -> Result<usize, FdarError> {
456 let (n, _) = data.shape();
457 if n == 0 {
458 return Err(FdarError::InvalidDimension {
459 parameter: "data",
460 expected: ">= 1 row".to_string(),
461 actual: "0".to_string(),
462 });
463 }
464 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
465 depths
466 .iter()
467 .enumerate()
468 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
469 .map(|(i, _)| i)
470 .ok_or_else(|| FdarError::ComputationFailed {
471 operation: "depth_based_median",
472 detail: "depth vector is empty".to_string(),
473 })
474}
475
476pub fn trim_mean(data: &FdMatrix, alpha: f64) -> Result<Vec<f64>, FdarError> {
512 if !(0.0..1.0).contains(&alpha) {
513 return Err(FdarError::InvalidParameter {
514 parameter: "alpha",
515 message: format!("must be in [0, 1), got {alpha}"),
516 });
517 }
518 let (n, m) = data.shape();
519 if n == 0 {
520 return Err(FdarError::InvalidDimension {
521 parameter: "data",
522 expected: ">= 1 row".to_string(),
523 actual: "0".to_string(),
524 });
525 }
526
527 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
529
530 let k = (alpha * n as f64).floor() as usize;
532
533 let mut indices: Vec<usize> = (0..n).collect();
535 indices.sort_unstable_by(|&a, &b| {
536 depths[b]
537 .partial_cmp(&depths[a])
538 .unwrap_or(std::cmp::Ordering::Equal)
539 });
540 let retained = &indices[..n - k];
541
542 let n_ret = retained.len() as f64;
544 let mean: Vec<f64> = (0..m)
545 .map(|j| retained.iter().map(|&i| data[(i, j)]).sum::<f64>() / n_ret)
546 .collect();
547
548 Ok(mean)
549}
550
551#[derive(Debug, Clone, Copy, PartialEq)]
553#[non_exhaustive]
554pub enum NormalizationMethod {
555 Center,
557 Autoscale,
559 Pareto,
561 Range,
563 CurveCenter,
565 CurveStandardize,
567 CurveRange,
569 CurveLp(f64),
575}
576
577pub fn normalize(data: &FdMatrix, method: NormalizationMethod) -> FdMatrix {
612 match method {
613 NormalizationMethod::CurveLp(_) => {
614 panic!("CurveLp requires argvals — use normalize_with_argvals()")
615 }
616 _ => {
617 let argvals: Vec<f64> = (0..data.ncols())
618 .map(|j| j as f64 / (data.ncols() - 1).max(1) as f64)
619 .collect();
620 normalize_with_argvals(data, &argvals, method)
621 }
622 }
623}
624
625pub fn normalize_with_argvals(
644 data: &FdMatrix,
645 argvals: &[f64],
646 method: NormalizationMethod,
647) -> FdMatrix {
648 let (n, m) = data.shape();
649 if n == 0 || m == 0 {
650 return FdMatrix::zeros(n, m);
651 }
652
653 match method {
654 NormalizationMethod::Center => center_1d(data),
655 NormalizationMethod::Autoscale => column_scale(data, n, m, ScaleKind::StdDev),
656 NormalizationMethod::Pareto => column_scale(data, n, m, ScaleKind::SqrtStdDev),
657 NormalizationMethod::Range => column_scale(data, n, m, ScaleKind::Range),
658 NormalizationMethod::CurveCenter => row_normalize(data, n, m, RowNorm::Center),
659 NormalizationMethod::CurveStandardize => row_normalize(data, n, m, RowNorm::Standardize),
660 NormalizationMethod::CurveRange => row_normalize(data, n, m, RowNorm::Range),
661 NormalizationMethod::CurveLp(p) => curve_lp_normalize(data, argvals, n, m, p),
662 }
663}
664
665#[derive(Clone, Copy)]
666enum ScaleKind {
667 StdDev,
668 SqrtStdDev,
669 Range,
670}
671
672fn column_scale(data: &FdMatrix, n: usize, m: usize, kind: ScaleKind) -> FdMatrix {
673 let mut result = FdMatrix::zeros(n, m);
674 for j in 0..m {
675 let col = data.column(j);
676 let mean = col.iter().sum::<f64>() / n as f64;
677 let scale = match kind {
678 ScaleKind::StdDev => {
679 let var =
680 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
681 var.sqrt()
682 }
683 ScaleKind::SqrtStdDev => {
684 let var =
685 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
686 var.sqrt().sqrt()
687 }
688 ScaleKind::Range => {
689 let min = col.iter().copied().fold(f64::INFINITY, f64::min);
690 let max = col.iter().copied().fold(f64::NEG_INFINITY, f64::max);
691 max - min
692 }
693 };
694 let out = result.column_mut(j);
695 let denom = if scale > 1e-15 { scale } else { 1.0 };
696 for i in 0..n {
697 out[i] = (col[i] - mean) / denom;
698 }
699 }
700 result
701}
702
703#[derive(Clone, Copy)]
704enum RowNorm {
705 Center,
706 Standardize,
707 Range,
708}
709
710fn row_normalize(data: &FdMatrix, n: usize, m: usize, kind: RowNorm) -> FdMatrix {
711 let mut result = FdMatrix::zeros(n, m);
712 for i in 0..n {
713 let row: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
714 let mean = row.iter().sum::<f64>() / m as f64;
715 match kind {
716 RowNorm::Center => {
717 for j in 0..m {
718 result[(i, j)] = row[j] - mean;
719 }
720 }
721 RowNorm::Standardize => {
722 let std = (row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>()
723 / (m - 1).max(1) as f64)
724 .sqrt();
725 let denom = if std > 1e-15 { std } else { 1.0 };
726 for j in 0..m {
727 result[(i, j)] = (row[j] - mean) / denom;
728 }
729 }
730 RowNorm::Range => {
731 let min = row.iter().copied().fold(f64::INFINITY, f64::min);
732 let max = row.iter().copied().fold(f64::NEG_INFINITY, f64::max);
733 let range = max - min;
734 let denom = if range > 1e-15 { range } else { 1.0 };
735 for j in 0..m {
736 result[(i, j)] = (row[j] - min) / denom;
737 }
738 }
739 }
740 }
741 result
742}
743
744fn curve_lp_normalize(data: &FdMatrix, argvals: &[f64], n: usize, m: usize, p: f64) -> FdMatrix {
746 let mut result = FdMatrix::zeros(n, m);
747 if p.is_infinite() {
748 for i in 0..n {
750 let max_abs = (0..m).map(|j| data[(i, j)].abs()).fold(0.0f64, f64::max);
751 let denom = if max_abs > 1e-15 { max_abs } else { 1.0 };
752 for j in 0..m {
753 result[(i, j)] = data[(i, j)] / denom;
754 }
755 }
756 } else {
757 let norms = norm_lp_1d(data, argvals, p);
758 for i in 0..n {
759 let denom = if norms[i] > 1e-15 { norms[i] } else { 1.0 };
760 for j in 0..m {
761 result[(i, j)] = data[(i, j)] / denom;
762 }
763 }
764 }
765 result
766}
767
768pub(crate) fn norm_lp_1d(data: &FdMatrix, argvals: &[f64], p: f64) -> Vec<f64> {
778 let (n, m) = data.shape();
779 if n == 0 || m == 0 || argvals.len() != m {
780 return Vec::new();
781 }
782
783 let weights = simpsons_weights(argvals);
784
785 if (p - 2.0).abs() < 1e-14 {
786 iter_maybe_parallel!(0..n)
787 .map(|i| {
788 let mut integral = 0.0;
789 for j in 0..m {
790 let v = data[(i, j)];
791 integral += v * v * weights[j];
792 }
793 integral.sqrt()
794 })
795 .collect()
796 } else if (p - 1.0).abs() < 1e-14 {
797 iter_maybe_parallel!(0..n)
798 .map(|i| {
799 let mut integral = 0.0;
800 for j in 0..m {
801 integral += data[(i, j)].abs() * weights[j];
802 }
803 integral
804 })
805 .collect()
806 } else {
807 iter_maybe_parallel!(0..n)
808 .map(|i| {
809 let mut integral = 0.0;
810 for j in 0..m {
811 integral += data[(i, j)].abs().powf(p) * weights[j];
812 }
813 integral.powf(1.0 / p)
814 })
815 .collect()
816 }
817}
818
819pub fn norm_lp(data: &FdMatrix, argvals: &[f64], p: f64, dim: Dim) -> Vec<f64> {
831 match dim {
832 Dim::One | Dim::Two => norm_lp_1d(data, argvals, p),
833 }
834}
835
836fn deriv_1d_step(
838 current: &FdMatrix,
839 n: usize,
840 m: usize,
841 h0: f64,
842 hn: f64,
843 h_central: &[f64],
844) -> FdMatrix {
845 let mut next = FdMatrix::zeros(n, m);
846 let src_col0 = current.column(0);
848 let src_col1 = current.column(1);
849 let dst = next.column_mut(0);
850 for i in 0..n {
851 dst[i] = (src_col1[i] - src_col0[i]) / h0;
852 }
853 for j in 1..(m - 1) {
855 let src_prev = current.column(j - 1);
856 let src_next = current.column(j + 1);
857 let dst = next.column_mut(j);
858 let h = h_central[j - 1];
859 for i in 0..n {
860 dst[i] = (src_next[i] - src_prev[i]) / h;
861 }
862 }
863 let src_colm2 = current.column(m - 2);
865 let src_colm1 = current.column(m - 1);
866 let dst = next.column_mut(m - 1);
867 for i in 0..n {
868 dst[i] = (src_colm1[i] - src_colm2[i]) / hn;
869 }
870 next
871}
872
873#[derive(Debug, Clone, PartialEq)]
879pub enum DerivDomain<'a> {
880 OneD {
882 argvals: &'a [f64],
884 nderiv: usize,
886 },
887 TwoD {
889 argvals_s: &'a [f64],
891 argvals_t: &'a [f64],
893 m1: usize,
895 m2: usize,
897 },
898}
899
900#[derive(Debug, Clone, PartialEq)]
906pub enum DerivResult {
907 OneD(FdMatrix),
909 TwoD(Deriv2DResult),
911 None,
913}
914
915#[must_use]
947pub fn deriv(data: &FdMatrix, domain: DerivDomain<'_>) -> DerivResult {
948 match domain {
949 DerivDomain::OneD { argvals, nderiv } => {
950 DerivResult::OneD(deriv_1d_impl(data, argvals, nderiv))
951 }
952 DerivDomain::TwoD {
953 argvals_s,
954 argvals_t,
955 m1,
956 m2,
957 } => deriv_2d_impl(data, argvals_s, argvals_t, m1, m2)
958 .map(DerivResult::TwoD)
959 .unwrap_or(DerivResult::None),
960 }
961}
962
963fn deriv_1d_impl(data: &FdMatrix, argvals: &[f64], nderiv: usize) -> FdMatrix {
964 let (n, m) = data.shape();
965 if n == 0 || m < 2 || argvals.len() != m {
966 return FdMatrix::zeros(n, m);
967 }
968 if nderiv == 0 {
969 return data.clone();
970 }
971
972 let mut current = data.clone();
973
974 let h0 = argvals[1] - argvals[0];
976 let hn = argvals[m - 1] - argvals[m - 2];
977 let h_central: Vec<f64> = (1..(m - 1))
978 .map(|j| argvals[j + 1] - argvals[j - 1])
979 .collect();
980
981 for _ in 0..nderiv {
982 current = deriv_1d_step(¤t, n, m, h0, hn, &h_central);
983 }
984
985 current
986}
987
988#[derive(Debug, Clone, PartialEq)]
990#[non_exhaustive]
991pub struct Deriv2DResult {
992 pub ds: FdMatrix,
994 pub dt: FdMatrix,
996 pub dsdt: FdMatrix,
998}
999
1000fn compute_step_sizes(argvals: &[f64]) -> Vec<f64> {
1004 let m = argvals.len();
1005 if m < 2 {
1006 return vec![1.0; m];
1007 }
1008 (0..m)
1009 .map(|j| {
1010 if j == 0 {
1011 argvals[1] - argvals[0]
1012 } else if j == m - 1 {
1013 argvals[m - 1] - argvals[m - 2]
1014 } else {
1015 argvals[j + 1] - argvals[j - 1]
1016 }
1017 })
1018 .collect()
1019}
1020
1021fn reassemble_colmajor(rows: &[Vec<f64>], n: usize, ncol: usize) -> FdMatrix {
1023 let mut mat = FdMatrix::zeros(n, ncol);
1024 for i in 0..n {
1025 for j in 0..ncol {
1026 mat[(i, j)] = rows[i][j];
1027 }
1028 }
1029 mat
1030}
1031
1032fn deriv_2d_impl(
1046 data: &FdMatrix,
1047 argvals_s: &[f64],
1048 argvals_t: &[f64],
1049 m1: usize,
1050 m2: usize,
1051) -> Option<Deriv2DResult> {
1052 let n = data.nrows();
1053 let ncol = m1 * m2;
1054 if n == 0
1055 || ncol == 0
1056 || m1 < 2
1057 || m2 < 2
1058 || data.ncols() != ncol
1059 || argvals_s.len() != m1
1060 || argvals_t.len() != m2
1061 {
1062 return None;
1063 }
1064
1065 let hs = compute_step_sizes(argvals_s);
1066 let ht = compute_step_sizes(argvals_t);
1067
1068 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
1070 .map(|i| {
1071 let mut ds = vec![0.0; ncol];
1072 let mut dt = vec![0.0; ncol];
1073 let mut dsdt = vec![0.0; ncol];
1074
1075 let get_val = |si: usize, ti: usize| -> f64 { data[(i, si + ti * m1)] };
1076
1077 for ti in 0..m2 {
1078 for si in 0..m1 {
1079 let idx = si + ti * m1;
1080 let (ds_val, dt_val, dsdt_val) =
1081 compute_2d_derivatives(get_val, si, ti, m1, m2, &hs, &ht);
1082 ds[idx] = ds_val;
1083 dt[idx] = dt_val;
1084 dsdt[idx] = dsdt_val;
1085 }
1086 }
1087
1088 (ds, dt, dsdt)
1089 })
1090 .collect();
1091
1092 let (ds_vecs, (dt_vecs, dsdt_vecs)): (Vec<Vec<f64>>, (Vec<Vec<f64>>, Vec<Vec<f64>>)) =
1093 results.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
1094
1095 Some(Deriv2DResult {
1096 ds: reassemble_colmajor(&ds_vecs, n, ncol),
1097 dt: reassemble_colmajor(&dt_vecs, n, ncol),
1098 dsdt: reassemble_colmajor(&dsdt_vecs, n, ncol),
1099 })
1100}
1101
1102pub fn geometric_median(
1117 data: &FdMatrix,
1118 argvals: &[f64],
1119 argvals_t: Option<&[f64]>,
1120 max_iter: usize,
1121 tol: f64,
1122 dim: Dim,
1123) -> Vec<f64> {
1124 match dim {
1125 Dim::One => geometric_median_1d_impl(data, argvals, max_iter, tol),
1126 Dim::Two => {
1127 geometric_median_2d_impl(data, argvals, argvals_t.unwrap_or(&[]), max_iter, tol)
1128 }
1129 }
1130}
1131
1132fn geometric_median_1d_impl(
1142 data: &FdMatrix,
1143 argvals: &[f64],
1144 max_iter: usize,
1145 tol: f64,
1146) -> Vec<f64> {
1147 let (n, m) = data.shape();
1148 if n == 0 || m == 0 || argvals.len() != m {
1149 return Vec::new();
1150 }
1151
1152 let weights = simpsons_weights(argvals);
1153 weiszfeld_iteration(data, &weights, max_iter, tol)
1154}
1155
1156fn geometric_median_2d_impl(
1167 data: &FdMatrix,
1168 argvals_s: &[f64],
1169 argvals_t: &[f64],
1170 max_iter: usize,
1171 tol: f64,
1172) -> Vec<f64> {
1173 let (n, m) = data.shape();
1174 let expected_cols = argvals_s.len() * argvals_t.len();
1175 if n == 0 || m == 0 || m != expected_cols {
1176 return Vec::new();
1177 }
1178
1179 let weights = simpsons_weights_2d(argvals_s, argvals_t);
1180 weiszfeld_iteration(data, &weights, max_iter, tol)
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::*;
1186 use crate::test_helpers::uniform_grid;
1187 use std::f64::consts::PI;
1188
1189 #[test]
1192 fn test_mean_1d() {
1193 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();
1199 let mean = mean_1d(&mat);
1200 assert_eq!(mean, vec![2.0, 3.0, 4.0]);
1201 }
1202
1203 #[test]
1204 fn test_mean_1d_single_sample() {
1205 let data = vec![1.0, 2.0, 3.0];
1206 let mat = FdMatrix::from_column_major(data, 1, 3).unwrap();
1207 let mean = mean_1d(&mat);
1208 assert_eq!(mean, vec![1.0, 2.0, 3.0]);
1209 }
1210
1211 #[test]
1212 fn test_mean_1d_invalid() {
1213 assert!(mean_1d(&FdMatrix::zeros(0, 0)).is_empty());
1214 }
1215
1216 #[test]
1219 fn test_center_1d() {
1220 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();
1222 let centered = center_1d(&mat);
1223 assert_eq!(centered.as_slice(), &[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);
1225 }
1226
1227 #[test]
1228 fn test_center_1d_mean_zero() {
1229 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0];
1230 let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
1231 let centered = center_1d(&mat);
1232 let centered_mean = mean_1d(¢ered);
1233 for m in centered_mean {
1234 assert!(m.abs() < 1e-10, "Centered data should have zero mean");
1235 }
1236 }
1237
1238 #[test]
1239 fn test_center_1d_invalid() {
1240 let centered = center_1d(&FdMatrix::zeros(0, 0));
1241 assert!(centered.is_empty());
1242 }
1243
1244 #[test]
1247 fn test_norm_lp_1d_constant() {
1248 let argvals = uniform_grid(21);
1250 let data: Vec<f64> = vec![2.0; 21];
1251 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1252 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1253 assert_eq!(norms.len(), 1);
1254 assert!(
1255 (norms[0] - 2.0).abs() < 0.1,
1256 "L2 norm of constant 2 should be 2"
1257 );
1258 }
1259
1260 #[test]
1261 fn test_norm_lp_1d_sine() {
1262 let argvals = uniform_grid(101);
1264 let data: Vec<f64> = argvals.iter().map(|&x| (PI * x).sin()).collect();
1265 let mat = FdMatrix::from_column_major(data, 1, 101).unwrap();
1266 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1267 let expected = 0.5_f64.sqrt();
1268 assert!(
1269 (norms[0] - expected).abs() < 0.05,
1270 "Expected {}, got {}",
1271 expected,
1272 norms[0]
1273 );
1274 }
1275
1276 #[test]
1277 fn test_norm_lp_1d_invalid() {
1278 assert!(norm_lp_1d(&FdMatrix::zeros(0, 0), &[], 2.0).is_empty());
1279 }
1280
1281 #[test]
1284 fn test_deriv_1d_linear() {
1285 let argvals = uniform_grid(21);
1287 let data = argvals.clone();
1288 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1289 let DerivResult::OneD(deriv) = deriv(
1290 &mat,
1291 DerivDomain::OneD {
1292 argvals: &argvals,
1293 nderiv: 1,
1294 },
1295 ) else {
1296 panic!("expected 1D result");
1297 };
1298 for j in 2..19 {
1300 assert!(
1301 (deriv[(0, j)] - 1.0).abs() < 0.1,
1302 "Derivative of x should be 1"
1303 );
1304 }
1305 }
1306
1307 #[test]
1308 fn test_deriv_1d_quadratic() {
1309 let argvals = uniform_grid(51);
1311 let data: Vec<f64> = argvals.iter().map(|&x| x * x).collect();
1312 let mat = FdMatrix::from_column_major(data, 1, 51).unwrap();
1313 let DerivResult::OneD(deriv) = deriv(
1314 &mat,
1315 DerivDomain::OneD {
1316 argvals: &argvals,
1317 nderiv: 1,
1318 },
1319 ) else {
1320 panic!("expected 1D result");
1321 };
1322 for j in 5..45 {
1324 let expected = 2.0 * argvals[j];
1325 assert!(
1326 (deriv[(0, j)] - expected).abs() < 0.1,
1327 "Derivative of x^2 should be 2x"
1328 );
1329 }
1330 }
1331
1332 #[test]
1333 fn test_deriv_1d_invalid() {
1334 let DerivResult::OneD(result) = deriv(
1335 &FdMatrix::zeros(0, 0),
1336 DerivDomain::OneD {
1337 argvals: &[],
1338 nderiv: 1,
1339 },
1340 ) else {
1341 panic!("expected 1D result");
1342 };
1343 assert!(result.is_empty() || result.as_slice().iter().all(|&x| x == 0.0));
1344 }
1345
1346 #[test]
1349 fn test_geometric_median_identical_curves() {
1350 let argvals = uniform_grid(21);
1352 let n = 5;
1353 let m = 21;
1354 let mut data = vec![0.0; n * m];
1355 for i in 0..n {
1356 for j in 0..m {
1357 data[i + j * n] = (2.0 * PI * argvals[j]).sin();
1358 }
1359 }
1360 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1361 let median = geometric_median(&mat, &argvals, None, 100, 1e-6, crate::dim::Dim::One);
1362 for j in 0..m {
1363 let expected = (2.0 * PI * argvals[j]).sin();
1364 assert!(
1365 (median[j] - expected).abs() < 0.01,
1366 "Median should equal all curves"
1367 );
1368 }
1369 }
1370
1371 #[test]
1372 fn test_geometric_median_converges() {
1373 let argvals = uniform_grid(21);
1374 let n = 10;
1375 let m = 21;
1376 let mut data = vec![0.0; n * m];
1377 for i in 0..n {
1378 for j in 0..m {
1379 data[i + j * n] = (i as f64 / n as f64) * argvals[j];
1380 }
1381 }
1382 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1383 let median = geometric_median(&mat, &argvals, None, 100, 1e-6, crate::dim::Dim::One);
1384 assert_eq!(median.len(), m);
1385 assert!(median.iter().all(|&x| x.is_finite()));
1386 }
1387
1388 #[test]
1389 fn test_geometric_median_invalid() {
1390 assert!(geometric_median(
1391 &FdMatrix::zeros(0, 0),
1392 &[],
1393 None,
1394 100,
1395 1e-6,
1396 crate::dim::Dim::One
1397 )
1398 .is_empty());
1399 }
1400
1401 #[test]
1404 fn test_deriv_2d_linear_surface() {
1405 let m1 = 11;
1408 let m2 = 11;
1409 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1410 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1411
1412 let n = 1; let ncol = m1 * m2;
1414 let mut data = vec![0.0; n * ncol];
1415
1416 for si in 0..m1 {
1417 for ti in 0..m2 {
1418 let s = argvals_s[si];
1419 let t = argvals_t[ti];
1420 let idx = si + ti * m1;
1421 data[idx] = 2.0 * s + 3.0 * t;
1422 }
1423 }
1424
1425 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1426 let DerivResult::TwoD(result) = deriv(
1427 &mat,
1428 DerivDomain::TwoD {
1429 argvals_s: &argvals_s,
1430 argvals_t: &argvals_t,
1431 m1,
1432 m2,
1433 },
1434 ) else {
1435 panic!("expected 2D result");
1436 };
1437
1438 for si in 2..(m1 - 2) {
1440 for ti in 2..(m2 - 2) {
1441 let idx = si + ti * m1;
1442 assert!(
1443 (result.ds[(0, idx)] - 2.0).abs() < 0.2,
1444 "∂f/∂s at ({}, {}) = {}, expected 2",
1445 si,
1446 ti,
1447 result.ds[(0, idx)]
1448 );
1449 }
1450 }
1451
1452 for si in 2..(m1 - 2) {
1454 for ti in 2..(m2 - 2) {
1455 let idx = si + ti * m1;
1456 assert!(
1457 (result.dt[(0, idx)] - 3.0).abs() < 0.2,
1458 "∂f/∂t at ({}, {}) = {}, expected 3",
1459 si,
1460 ti,
1461 result.dt[(0, idx)]
1462 );
1463 }
1464 }
1465
1466 for si in 2..(m1 - 2) {
1468 for ti in 2..(m2 - 2) {
1469 let idx = si + ti * m1;
1470 assert!(
1471 result.dsdt[(0, idx)].abs() < 0.5,
1472 "∂²f/∂s∂t at ({}, {}) = {}, expected 0",
1473 si,
1474 ti,
1475 result.dsdt[(0, idx)]
1476 );
1477 }
1478 }
1479 }
1480
1481 #[test]
1482 fn test_deriv_2d_quadratic_surface() {
1483 let m1 = 21;
1486 let m2 = 21;
1487 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1488 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1489
1490 let n = 1;
1491 let ncol = m1 * m2;
1492 let mut data = vec![0.0; n * ncol];
1493
1494 for si in 0..m1 {
1495 for ti in 0..m2 {
1496 let s = argvals_s[si];
1497 let t = argvals_t[ti];
1498 let idx = si + ti * m1;
1499 data[idx] = s * t;
1500 }
1501 }
1502
1503 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1504 let DerivResult::TwoD(result) = deriv(
1505 &mat,
1506 DerivDomain::TwoD {
1507 argvals_s: &argvals_s,
1508 argvals_t: &argvals_t,
1509 m1,
1510 m2,
1511 },
1512 ) else {
1513 panic!("expected 2D result");
1514 };
1515
1516 for si in 3..(m1 - 3) {
1518 for ti in 3..(m2 - 3) {
1519 let idx = si + ti * m1;
1520 let expected = argvals_t[ti];
1521 assert!(
1522 (result.ds[(0, idx)] - expected).abs() < 0.1,
1523 "∂f/∂s at ({}, {}) = {}, expected {}",
1524 si,
1525 ti,
1526 result.ds[(0, idx)],
1527 expected
1528 );
1529 }
1530 }
1531
1532 for si in 3..(m1 - 3) {
1534 for ti in 3..(m2 - 3) {
1535 let idx = si + ti * m1;
1536 let expected = argvals_s[si];
1537 assert!(
1538 (result.dt[(0, idx)] - expected).abs() < 0.1,
1539 "∂f/∂t at ({}, {}) = {}, expected {}",
1540 si,
1541 ti,
1542 result.dt[(0, idx)],
1543 expected
1544 );
1545 }
1546 }
1547
1548 for si in 3..(m1 - 3) {
1550 for ti in 3..(m2 - 3) {
1551 let idx = si + ti * m1;
1552 assert!(
1553 (result.dsdt[(0, idx)] - 1.0).abs() < 0.3,
1554 "∂²f/∂s∂t at ({}, {}) = {}, expected 1",
1555 si,
1556 ti,
1557 result.dsdt[(0, idx)]
1558 );
1559 }
1560 }
1561 }
1562
1563 #[test]
1564 fn test_deriv_2d_invalid_input() {
1565 let result = deriv(
1567 &FdMatrix::zeros(0, 0),
1568 DerivDomain::TwoD {
1569 argvals_s: &[],
1570 argvals_t: &[],
1571 m1: 0,
1572 m2: 0,
1573 },
1574 );
1575 assert_eq!(result, DerivResult::None);
1576
1577 let mat = FdMatrix::from_column_major(vec![1.0; 4], 1, 4).unwrap();
1579 let argvals = vec![0.0, 1.0];
1580 let result = deriv(
1581 &mat,
1582 DerivDomain::TwoD {
1583 argvals_s: &argvals,
1584 argvals_t: &[0.0, 0.5, 1.0],
1585 m1: 2,
1586 m2: 2,
1587 },
1588 );
1589 assert_eq!(result, DerivResult::None);
1590 }
1591
1592 #[test]
1595 fn test_geometric_median_2d_basic() {
1596 let m1 = 5;
1598 let m2 = 5;
1599 let m = m1 * m2;
1600 let n = 3;
1601 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1602 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1603
1604 let mut data = vec![0.0; n * m];
1605
1606 for i in 0..n {
1608 for si in 0..m1 {
1609 for ti in 0..m2 {
1610 let idx = si + ti * m1;
1611 let s = argvals_s[si];
1612 let t = argvals_t[ti];
1613 data[i + idx * n] = s + t;
1614 }
1615 }
1616 }
1617
1618 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1619 let median = geometric_median(
1620 &mat,
1621 &argvals_s,
1622 Some(&argvals_t),
1623 100,
1624 1e-6,
1625 crate::dim::Dim::Two,
1626 );
1627 assert_eq!(median.len(), m);
1628
1629 for si in 0..m1 {
1631 for ti in 0..m2 {
1632 let idx = si + ti * m1;
1633 let expected = argvals_s[si] + argvals_t[ti];
1634 assert!(
1635 (median[idx] - expected).abs() < 0.01,
1636 "Median at ({}, {}) = {}, expected {}",
1637 si,
1638 ti,
1639 median[idx],
1640 expected
1641 );
1642 }
1643 }
1644 }
1645
1646 #[test]
1649 fn functional_variance_equals_std_squared() {
1650 let data = FdMatrix::from_column_major(
1652 vec![
1653 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1654 ],
1655 3,
1656 4,
1657 )
1658 .unwrap();
1659 let var = functional_variance(&data).unwrap();
1660 let std = functional_std(&data).unwrap();
1661 for j in 0..4 {
1662 assert!(
1663 (std[j].powi(2) - var[j]).abs() < 1e-10,
1664 "at j={j}: std^2={} != var={}",
1665 std[j].powi(2),
1666 var[j]
1667 );
1668 }
1669 }
1670
1671 #[test]
1672 fn functional_covariance_diagonal_matches_variance() {
1673 let data = FdMatrix::from_column_major(
1674 vec![
1675 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1676 ],
1677 3,
1678 4,
1679 )
1680 .unwrap();
1681 let var = functional_variance(&data).unwrap();
1682 let cov = functional_covariance(&data).unwrap();
1683 for j in 0..4 {
1684 assert!(
1685 (cov[(j, j)] - var[j]).abs() < 1e-10,
1686 "at j={j}: cov[j,j]={} != var={}",
1687 cov[(j, j)],
1688 var[j]
1689 );
1690 }
1691 }
1692
1693 #[test]
1694 fn functional_variance_hand_computed() {
1695 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 4.0, 2.0], 2, 2).unwrap();
1702 let var = functional_variance(&data).unwrap();
1703 assert!((var[0] - 2.0).abs() < 1e-10, "var[0]={}", var[0]);
1704 assert!((var[1] - 2.0).abs() < 1e-10, "var[1]={}", var[1]);
1705 }
1706
1707 #[test]
1710 fn depth_based_median_argmax() {
1711 let n = 5;
1715 let m = 10;
1716 let mut data_vec = vec![0.0f64; n * m];
1717 for j in 0..m {
1723 data_vec[j * n] = 0.0;
1724 data_vec[1 + j * n] = 0.25;
1725 data_vec[2 + j * n] = 0.5;
1726 data_vec[3 + j * n] = 0.75;
1727 data_vec[4 + j * n] = 1.0;
1728 }
1729 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1730 let idx = depth_based_median(&data).unwrap();
1731 assert_eq!(idx, 2, "most central curve should be at index 2, got {idx}");
1732 }
1733
1734 #[test]
1735 fn trim_mean_alpha_zero_equals_mean() {
1736 let n = 5;
1737 let m = 4;
1738 let mut data_vec = vec![0.0f64; n * m];
1739 for i in 0..n {
1740 for j in 0..m {
1741 data_vec[i + j * n] = (i + 1) as f64 * (j + 1) as f64;
1742 }
1743 }
1744 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1745 let tm = trim_mean(&data, 0.0).unwrap();
1746 let mu = mean_1d(&data);
1747 for j in 0..m {
1748 assert!(
1749 (tm[j] - mu[j]).abs() < 1e-10,
1750 "at j={j}: trim_mean(alpha=0)={} != mean={}",
1751 tm[j],
1752 mu[j]
1753 );
1754 }
1755 }
1756
1757 #[test]
1758 fn trim_mean_rejects_bad_alpha() {
1759 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
1760 let result = trim_mean(&data, 1.0);
1762 assert!(
1763 matches!(
1764 result,
1765 Err(FdarError::InvalidParameter {
1766 parameter: "alpha",
1767 ..
1768 })
1769 ),
1770 "expected InvalidParameter for alpha=1.0, got {result:?}"
1771 );
1772 let result2 = trim_mean(&data, -0.1);
1774 assert!(
1775 matches!(
1776 result2,
1777 Err(FdarError::InvalidParameter {
1778 parameter: "alpha",
1779 ..
1780 })
1781 ),
1782 "expected InvalidParameter for alpha=-0.1, got {result2:?}"
1783 );
1784 }
1785
1786 #[test]
1789 fn functional_stats_input_validation() {
1790 use crate::error::FdarError;
1791
1792 let one_row = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
1794 assert!(
1795 matches!(
1796 functional_variance(&one_row),
1797 Err(FdarError::InvalidDimension {
1798 parameter: "data",
1799 ..
1800 })
1801 ),
1802 "functional_variance should reject n=1"
1803 );
1804 assert!(
1805 matches!(
1806 functional_std(&one_row),
1807 Err(FdarError::InvalidDimension {
1808 parameter: "data",
1809 ..
1810 })
1811 ),
1812 "functional_std should reject n=1"
1813 );
1814 assert!(
1815 matches!(
1816 functional_covariance(&one_row),
1817 Err(FdarError::InvalidDimension {
1818 parameter: "data",
1819 ..
1820 })
1821 ),
1822 "functional_covariance should reject n=1"
1823 );
1824
1825 let zero_rows = FdMatrix::zeros(0, 3);
1827 assert!(
1828 matches!(
1829 depth_based_median(&zero_rows),
1830 Err(FdarError::InvalidDimension {
1831 parameter: "data",
1832 ..
1833 })
1834 ),
1835 "depth_based_median should reject n=0"
1836 );
1837 assert!(
1838 matches!(
1839 trim_mean(&zero_rows, 0.0),
1840 Err(FdarError::InvalidDimension {
1841 parameter: "data",
1842 ..
1843 })
1844 ),
1845 "trim_mean should reject n=0"
1846 );
1847 }
1848
1849 #[test]
1850 fn test_nan_mean_no_panic() {
1851 let mut data_vec = vec![1.0; 6];
1852 data_vec[2] = f64::NAN;
1853 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1854 let m = mean_1d(&data);
1855 assert_eq!(m.len(), 3);
1856 }
1857
1858 #[test]
1859 fn test_nan_center_no_panic() {
1860 let mut data_vec = vec![1.0; 6];
1861 data_vec[2] = f64::NAN;
1862 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1863 let c = center_1d(&data);
1864 assert_eq!(c.nrows(), 2);
1865 }
1866
1867 #[test]
1868 fn test_nan_norm_no_panic() {
1869 let mut data_vec = vec![1.0; 6];
1870 data_vec[2] = f64::NAN;
1871 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1872 let argvals = vec![0.0, 0.5, 1.0];
1873 let norms = norm_lp_1d(&data, &argvals, 2.0);
1874 assert_eq!(norms.len(), 2);
1875 }
1876
1877 #[test]
1878 fn test_n1_norm() {
1879 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0], 1, 3).unwrap();
1880 let argvals = vec![0.0, 0.5, 1.0];
1881 let norms = norm_lp_1d(&data, &argvals, 2.0);
1882 assert_eq!(norms.len(), 1);
1883 assert!(norms[0] > 0.0);
1884 }
1885
1886 #[test]
1887 fn test_n2_center() {
1888 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0], 2, 2).unwrap();
1889 let centered = center_1d(&data);
1890 assert!((centered[(0, 0)] - (-1.0)).abs() < 1e-12);
1893 assert!((centered[(1, 0)] - 1.0).abs() < 1e-12);
1894 }
1895
1896 #[test]
1897 fn test_deriv_nderiv0() {
1898 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).unwrap();
1900 let argvals = vec![0.0, 0.5, 1.0];
1901 let DerivResult::OneD(result) = deriv(
1902 &data,
1903 DerivDomain::OneD {
1904 argvals: &argvals,
1905 nderiv: 0,
1906 },
1907 ) else {
1908 panic!("expected 1D result");
1909 };
1910 assert_eq!(result.shape(), data.shape());
1911 for i in 0..2 {
1912 for j in 0..3 {
1913 assert!((result[(i, j)] - data[(i, j)]).abs() < 1e-12);
1914 }
1915 }
1916 }
1917
1918 #[test]
1921 fn test_normalize_autoscale() {
1922 let data = FdMatrix::from_column_major(
1924 vec![
1925 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1926 ],
1927 3,
1928 4,
1929 )
1930 .unwrap();
1931 let scaled = normalize(&data, NormalizationMethod::Autoscale);
1932 for j in 0..4 {
1934 let col = scaled.column(j);
1935 let mean = col.iter().sum::<f64>() / 3.0;
1936 assert!(
1937 mean.abs() < 1e-10,
1938 "column {j} mean should be 0, got {mean}"
1939 );
1940 let var = col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1941 assert!(
1942 (var - 1.0).abs() < 1e-10,
1943 "column {j} variance should be 1, got {var}"
1944 );
1945 }
1946 }
1947
1948 #[test]
1949 fn test_normalize_pareto() {
1950 let data =
1951 FdMatrix::from_column_major(vec![1.0, 5.0, 3.0, 10.0, 20.0, 30.0], 2, 3).unwrap();
1952 let scaled = normalize(&data, NormalizationMethod::Pareto);
1953 for j in 0..3 {
1955 let col = scaled.column(j);
1956 let mean = col.iter().sum::<f64>() / 2.0;
1957 assert!(mean.abs() < 1e-10, "column {j} mean should be 0");
1958 }
1959 }
1960
1961 #[test]
1962 fn test_normalize_range() {
1963 let data = FdMatrix::from_column_major(vec![0.0, 10.0, 2.0, 8.0], 2, 2).unwrap();
1964 let scaled = normalize(&data, NormalizationMethod::Range);
1965 assert!((scaled[(0, 0)] - (-0.5)).abs() < 1e-10);
1967 assert!((scaled[(1, 0)] - 0.5).abs() < 1e-10);
1968 }
1969
1970 #[test]
1971 fn test_normalize_curve_center() {
1972 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1973 let result = normalize(&data, NormalizationMethod::CurveCenter);
1974 assert!((result[(0, 0)] - (-2.0)).abs() < 1e-10);
1976 assert!((result[(0, 1)] - 0.0).abs() < 1e-10);
1977 assert!((result[(0, 2)] - 2.0).abs() < 1e-10);
1978 }
1979
1980 #[test]
1981 fn test_normalize_curve_standardize() {
1982 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1983 let result = normalize(&data, NormalizationMethod::CurveStandardize);
1984 for i in 0..2 {
1986 let row: Vec<f64> = (0..3).map(|j| result[(i, j)]).collect();
1987 let mean = row.iter().sum::<f64>() / 3.0;
1988 assert!(mean.abs() < 1e-10, "row {i} mean should be 0");
1989 let var = row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1990 assert!((var - 1.0).abs() < 1e-10, "row {i} variance should be 1");
1991 }
1992 }
1993
1994 #[test]
1995 fn test_normalize_curve_range() {
1996 let data =
1997 FdMatrix::from_column_major(vec![2.0, 10.0, 4.0, 20.0, 6.0, 30.0], 2, 3).unwrap();
1998 let result = normalize(&data, NormalizationMethod::CurveRange);
1999 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
2001 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
2002 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
2003 }
2004
2005 #[test]
2006 fn test_normalize_center_matches_center_1d() {
2007 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0], 2, 3).unwrap();
2008 let a = center_1d(&data);
2009 let b = normalize(&data, NormalizationMethod::Center);
2010 assert_eq!(a.as_slice(), b.as_slice());
2011 }
2012
2013 #[test]
2014 fn test_normalize_curve_lp_l2() {
2015 let data = FdMatrix::from_column_major(vec![3.0, 0.0, 0.0, 4.0, 0.0, 0.0], 2, 3).unwrap();
2017 let t = vec![0.0, 0.5, 1.0];
2018 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
2019 let norms = norm_lp_1d(&result, &t, 2.0);
2022 assert!(
2023 (norms[0] - 1.0).abs() < 0.1,
2024 "L2 norm after normalization should be ≈ 1, got {}",
2025 norms[0]
2026 );
2027 }
2028
2029 #[test]
2030 fn test_normalize_curve_lp_l1() {
2031 let data = FdMatrix::from_column_major(vec![2.0, 4.0, 6.0, 8.0], 2, 2).unwrap();
2032 let t = vec![0.0, 1.0];
2033 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(1.0));
2034 let norms = norm_lp_1d(&result, &t, 1.0);
2036 for (i, &norm) in norms.iter().enumerate() {
2037 assert!(
2038 (norm - 1.0).abs() < 0.1,
2039 "curve {i} L1 norm after normalization should be ≈ 1, got {norm}"
2040 );
2041 }
2042 }
2043
2044 #[test]
2045 fn test_normalize_curve_lp_linf() {
2046 let data =
2047 FdMatrix::from_column_major(vec![2.0, -5.0, 4.0, -10.0, 6.0, 15.0], 2, 3).unwrap();
2048 let t = vec![0.0, 0.5, 1.0];
2049 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(f64::INFINITY));
2050 for i in 0..2 {
2052 let max_abs: f64 = (0..3).map(|j| result[(i, j)].abs()).fold(0.0, f64::max);
2053 assert!(
2054 (max_abs - 1.0).abs() < 1e-10,
2055 "curve {i} max abs after L-inf normalization should be 1, got {max_abs}"
2056 );
2057 }
2058 }
2059
2060 #[test]
2061 fn test_normalize_curve_lp_zero_curve() {
2062 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0, 2.0], 2, 2).unwrap();
2064 let t = vec![0.0, 1.0];
2065 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
2066 assert!((result[(0, 0)]).abs() < 1e-15);
2068 assert!((result[(0, 1)]).abs() < 1e-15);
2069 assert!(result[(1, 0)].abs() > 0.0);
2071 }
2072}