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 fn mean_1d(data: &FdMatrix) -> Vec<f64> {
169 let (n, m) = data.shape();
170 if n == 0 || m == 0 {
171 return Vec::new();
172 }
173
174 iter_maybe_parallel!(0..m)
175 .map(|j| {
176 let col = data.column(j);
177 col.iter().sum::<f64>() / n as f64
178 })
179 .collect()
180}
181
182#[must_use = "expensive computation whose result should not be discarded"]
193pub fn mean(data: &FdMatrix, dim: Dim) -> Vec<f64> {
194 match dim {
195 Dim::One | Dim::Two => mean_1d(data),
196 }
197}
198
199#[deprecated(
203 since = "0.30.0",
204 note = "redundant with `mean(…, Dim::Two)`; body just forwards to `mean_1d`"
205)]
206pub fn mean_2d(data: &FdMatrix) -> Vec<f64> {
207 mean_1d(data)
209}
210
211pub fn center_1d(data: &FdMatrix) -> FdMatrix {
235 let (n, m) = data.shape();
236 if n == 0 || m == 0 {
237 return FdMatrix::zeros(0, 0);
238 }
239
240 let means: Vec<f64> = iter_maybe_parallel!(0..m)
242 .map(|j| {
243 let col = data.column(j);
244 col.iter().sum::<f64>() / n as f64
245 })
246 .collect();
247
248 let mut centered = FdMatrix::zeros(n, m);
250 for j in 0..m {
251 let col = centered.column_mut(j);
252 let src = data.column(j);
253 for i in 0..n {
254 col[i] = src[i] - means[j];
255 }
256 }
257
258 centered
259}
260
261pub fn functional_variance(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
291 let (n, m) = data.shape();
292 if n < 2 {
293 return Err(FdarError::InvalidDimension {
294 parameter: "data",
295 expected: ">= 2 rows".to_string(),
296 actual: n.to_string(),
297 });
298 }
299 let means = mean_1d(data);
300 let var: Vec<f64> = (0..m)
301 .map(|j| {
302 let col = data.column(j);
303 let mu = means[j];
304 col.iter().map(|&x| (x - mu).powi(2)).sum::<f64>() / (n - 1) as f64
305 })
306 .collect();
307 Ok(var)
308}
309
310pub fn functional_std(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
339 Ok(functional_variance(data)?
340 .iter()
341 .map(|v| v.sqrt())
342 .collect())
343}
344
345pub fn functional_covariance(data: &FdMatrix) -> Result<FdMatrix, FdarError> {
381 let (n, m) = data.shape();
382 if n < 2 {
383 return Err(FdarError::InvalidDimension {
384 parameter: "data",
385 expected: ">= 2 rows".to_string(),
386 actual: n.to_string(),
387 });
388 }
389 m.checked_mul(m)
391 .ok_or_else(|| FdarError::InvalidParameter {
392 parameter: "data",
393 message: format!(
394 "m={m} is too large: m*m would overflow usize (max {})",
395 usize::MAX
396 ),
397 })?;
398
399 let centered = center_1d(data);
400 let mut cov = FdMatrix::zeros(m, m);
401 let denom = (n - 1) as f64;
402 for j1 in 0..m {
403 let col1 = centered.column(j1);
404 for j2 in j1..m {
405 let col2 = centered.column(j2);
406 let val: f64 = col1
407 .iter()
408 .zip(col2.iter())
409 .map(|(&a, &b)| a * b)
410 .sum::<f64>()
411 / denom;
412 cov[(j1, j2)] = val;
413 cov[(j2, j1)] = val; }
415 }
416 Ok(cov)
417}
418
419pub fn depth_based_median(data: &FdMatrix) -> Result<usize, FdarError> {
451 let (n, _) = data.shape();
452 if n == 0 {
453 return Err(FdarError::InvalidDimension {
454 parameter: "data",
455 expected: ">= 1 row".to_string(),
456 actual: "0".to_string(),
457 });
458 }
459 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
460 depths
461 .iter()
462 .enumerate()
463 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
464 .map(|(i, _)| i)
465 .ok_or_else(|| FdarError::ComputationFailed {
466 operation: "depth_based_median",
467 detail: "depth vector is empty".to_string(),
468 })
469}
470
471pub fn trim_mean(data: &FdMatrix, alpha: f64) -> Result<Vec<f64>, FdarError> {
506 if !(0.0..1.0).contains(&alpha) {
507 return Err(FdarError::InvalidParameter {
508 parameter: "alpha",
509 message: format!("must be in [0, 1), got {alpha}"),
510 });
511 }
512 let (n, m) = data.shape();
513 if n == 0 {
514 return Err(FdarError::InvalidDimension {
515 parameter: "data",
516 expected: ">= 1 row".to_string(),
517 actual: "0".to_string(),
518 });
519 }
520
521 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
523
524 let k = (alpha * n as f64).floor() as usize;
526
527 let mut indices: Vec<usize> = (0..n).collect();
529 indices.sort_unstable_by(|&a, &b| {
530 depths[b]
531 .partial_cmp(&depths[a])
532 .unwrap_or(std::cmp::Ordering::Equal)
533 });
534 let retained = &indices[..n - k];
535
536 let n_ret = retained.len() as f64;
538 let mean: Vec<f64> = (0..m)
539 .map(|j| retained.iter().map(|&i| data[(i, j)]).sum::<f64>() / n_ret)
540 .collect();
541
542 Ok(mean)
543}
544
545#[derive(Debug, Clone, Copy, PartialEq)]
547#[non_exhaustive]
548pub enum NormalizationMethod {
549 Center,
551 Autoscale,
553 Pareto,
555 Range,
557 CurveCenter,
559 CurveStandardize,
561 CurveRange,
563 CurveLp(f64),
569}
570
571pub fn normalize(data: &FdMatrix, method: NormalizationMethod) -> FdMatrix {
606 match method {
607 NormalizationMethod::CurveLp(_) => {
608 panic!("CurveLp requires argvals — use normalize_with_argvals()")
609 }
610 _ => {
611 let argvals: Vec<f64> = (0..data.ncols())
612 .map(|j| j as f64 / (data.ncols() - 1).max(1) as f64)
613 .collect();
614 normalize_with_argvals(data, &argvals, method)
615 }
616 }
617}
618
619pub fn normalize_with_argvals(
638 data: &FdMatrix,
639 argvals: &[f64],
640 method: NormalizationMethod,
641) -> FdMatrix {
642 let (n, m) = data.shape();
643 if n == 0 || m == 0 {
644 return FdMatrix::zeros(n, m);
645 }
646
647 match method {
648 NormalizationMethod::Center => center_1d(data),
649 NormalizationMethod::Autoscale => column_scale(data, n, m, ScaleKind::StdDev),
650 NormalizationMethod::Pareto => column_scale(data, n, m, ScaleKind::SqrtStdDev),
651 NormalizationMethod::Range => column_scale(data, n, m, ScaleKind::Range),
652 NormalizationMethod::CurveCenter => row_normalize(data, n, m, RowNorm::Center),
653 NormalizationMethod::CurveStandardize => row_normalize(data, n, m, RowNorm::Standardize),
654 NormalizationMethod::CurveRange => row_normalize(data, n, m, RowNorm::Range),
655 NormalizationMethod::CurveLp(p) => curve_lp_normalize(data, argvals, n, m, p),
656 }
657}
658
659#[derive(Clone, Copy)]
660enum ScaleKind {
661 StdDev,
662 SqrtStdDev,
663 Range,
664}
665
666fn column_scale(data: &FdMatrix, n: usize, m: usize, kind: ScaleKind) -> FdMatrix {
667 let mut result = FdMatrix::zeros(n, m);
668 for j in 0..m {
669 let col = data.column(j);
670 let mean = col.iter().sum::<f64>() / n as f64;
671 let scale = match kind {
672 ScaleKind::StdDev => {
673 let var =
674 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
675 var.sqrt()
676 }
677 ScaleKind::SqrtStdDev => {
678 let var =
679 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
680 var.sqrt().sqrt()
681 }
682 ScaleKind::Range => {
683 let min = col.iter().copied().fold(f64::INFINITY, f64::min);
684 let max = col.iter().copied().fold(f64::NEG_INFINITY, f64::max);
685 max - min
686 }
687 };
688 let out = result.column_mut(j);
689 let denom = if scale > 1e-15 { scale } else { 1.0 };
690 for i in 0..n {
691 out[i] = (col[i] - mean) / denom;
692 }
693 }
694 result
695}
696
697#[derive(Clone, Copy)]
698enum RowNorm {
699 Center,
700 Standardize,
701 Range,
702}
703
704fn row_normalize(data: &FdMatrix, n: usize, m: usize, kind: RowNorm) -> FdMatrix {
705 let mut result = FdMatrix::zeros(n, m);
706 for i in 0..n {
707 let row: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
708 let mean = row.iter().sum::<f64>() / m as f64;
709 match kind {
710 RowNorm::Center => {
711 for j in 0..m {
712 result[(i, j)] = row[j] - mean;
713 }
714 }
715 RowNorm::Standardize => {
716 let std = (row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>()
717 / (m - 1).max(1) as f64)
718 .sqrt();
719 let denom = if std > 1e-15 { std } else { 1.0 };
720 for j in 0..m {
721 result[(i, j)] = (row[j] - mean) / denom;
722 }
723 }
724 RowNorm::Range => {
725 let min = row.iter().copied().fold(f64::INFINITY, f64::min);
726 let max = row.iter().copied().fold(f64::NEG_INFINITY, f64::max);
727 let range = max - min;
728 let denom = if range > 1e-15 { range } else { 1.0 };
729 for j in 0..m {
730 result[(i, j)] = (row[j] - min) / denom;
731 }
732 }
733 }
734 }
735 result
736}
737
738fn curve_lp_normalize(data: &FdMatrix, argvals: &[f64], n: usize, m: usize, p: f64) -> FdMatrix {
740 let mut result = FdMatrix::zeros(n, m);
741 if p.is_infinite() {
742 for i in 0..n {
744 let max_abs = (0..m).map(|j| data[(i, j)].abs()).fold(0.0f64, f64::max);
745 let denom = if max_abs > 1e-15 { max_abs } else { 1.0 };
746 for j in 0..m {
747 result[(i, j)] = data[(i, j)] / denom;
748 }
749 }
750 } else {
751 let norms = norm_lp_1d(data, argvals, p);
752 for i in 0..n {
753 let denom = if norms[i] > 1e-15 { norms[i] } else { 1.0 };
754 for j in 0..m {
755 result[(i, j)] = data[(i, j)] / denom;
756 }
757 }
758 }
759 result
760}
761
762pub fn norm_lp_1d(data: &FdMatrix, argvals: &[f64], p: f64) -> Vec<f64> {
772 let (n, m) = data.shape();
773 if n == 0 || m == 0 || argvals.len() != m {
774 return Vec::new();
775 }
776
777 let weights = simpsons_weights(argvals);
778
779 if (p - 2.0).abs() < 1e-14 {
780 iter_maybe_parallel!(0..n)
781 .map(|i| {
782 let mut integral = 0.0;
783 for j in 0..m {
784 let v = data[(i, j)];
785 integral += v * v * weights[j];
786 }
787 integral.sqrt()
788 })
789 .collect()
790 } else if (p - 1.0).abs() < 1e-14 {
791 iter_maybe_parallel!(0..n)
792 .map(|i| {
793 let mut integral = 0.0;
794 for j in 0..m {
795 integral += data[(i, j)].abs() * weights[j];
796 }
797 integral
798 })
799 .collect()
800 } else {
801 iter_maybe_parallel!(0..n)
802 .map(|i| {
803 let mut integral = 0.0;
804 for j in 0..m {
805 integral += data[(i, j)].abs().powf(p) * weights[j];
806 }
807 integral.powf(1.0 / p)
808 })
809 .collect()
810 }
811}
812
813fn deriv_1d_step(
839 current: &FdMatrix,
840 n: usize,
841 m: usize,
842 h0: f64,
843 hn: f64,
844 h_central: &[f64],
845) -> FdMatrix {
846 let mut next = FdMatrix::zeros(n, m);
847 let src_col0 = current.column(0);
849 let src_col1 = current.column(1);
850 let dst = next.column_mut(0);
851 for i in 0..n {
852 dst[i] = (src_col1[i] - src_col0[i]) / h0;
853 }
854 for j in 1..(m - 1) {
856 let src_prev = current.column(j - 1);
857 let src_next = current.column(j + 1);
858 let dst = next.column_mut(j);
859 let h = h_central[j - 1];
860 for i in 0..n {
861 dst[i] = (src_next[i] - src_prev[i]) / h;
862 }
863 }
864 let src_colm2 = current.column(m - 2);
866 let src_colm1 = current.column(m - 1);
867 let dst = next.column_mut(m - 1);
868 for i in 0..n {
869 dst[i] = (src_colm1[i] - src_colm2[i]) / hn;
870 }
871 next
872}
873
874pub fn deriv_1d(data: &FdMatrix, argvals: &[f64], nderiv: usize) -> FdMatrix {
875 let (n, m) = data.shape();
876 if n == 0 || m < 2 || argvals.len() != m {
877 return FdMatrix::zeros(n, m);
878 }
879 if nderiv == 0 {
880 return data.clone();
881 }
882
883 let mut current = data.clone();
884
885 let h0 = argvals[1] - argvals[0];
887 let hn = argvals[m - 1] - argvals[m - 2];
888 let h_central: Vec<f64> = (1..(m - 1))
889 .map(|j| argvals[j + 1] - argvals[j - 1])
890 .collect();
891
892 for _ in 0..nderiv {
893 current = deriv_1d_step(¤t, n, m, h0, hn, &h_central);
894 }
895
896 current
897}
898
899#[derive(Debug, Clone, PartialEq)]
901#[non_exhaustive]
902pub struct Deriv2DResult {
903 pub ds: FdMatrix,
905 pub dt: FdMatrix,
907 pub dsdt: FdMatrix,
909}
910
911fn compute_step_sizes(argvals: &[f64]) -> Vec<f64> {
915 let m = argvals.len();
916 if m < 2 {
917 return vec![1.0; m];
918 }
919 (0..m)
920 .map(|j| {
921 if j == 0 {
922 argvals[1] - argvals[0]
923 } else if j == m - 1 {
924 argvals[m - 1] - argvals[m - 2]
925 } else {
926 argvals[j + 1] - argvals[j - 1]
927 }
928 })
929 .collect()
930}
931
932fn reassemble_colmajor(rows: &[Vec<f64>], n: usize, ncol: usize) -> FdMatrix {
934 let mut mat = FdMatrix::zeros(n, ncol);
935 for i in 0..n {
936 for j in 0..ncol {
937 mat[(i, j)] = rows[i][j];
938 }
939 }
940 mat
941}
942
943pub fn deriv_2d(
957 data: &FdMatrix,
958 argvals_s: &[f64],
959 argvals_t: &[f64],
960 m1: usize,
961 m2: usize,
962) -> Option<Deriv2DResult> {
963 let n = data.nrows();
964 let ncol = m1 * m2;
965 if n == 0
966 || ncol == 0
967 || m1 < 2
968 || m2 < 2
969 || data.ncols() != ncol
970 || argvals_s.len() != m1
971 || argvals_t.len() != m2
972 {
973 return None;
974 }
975
976 let hs = compute_step_sizes(argvals_s);
977 let ht = compute_step_sizes(argvals_t);
978
979 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
981 .map(|i| {
982 let mut ds = vec![0.0; ncol];
983 let mut dt = vec![0.0; ncol];
984 let mut dsdt = vec![0.0; ncol];
985
986 let get_val = |si: usize, ti: usize| -> f64 { data[(i, si + ti * m1)] };
987
988 for ti in 0..m2 {
989 for si in 0..m1 {
990 let idx = si + ti * m1;
991 let (ds_val, dt_val, dsdt_val) =
992 compute_2d_derivatives(get_val, si, ti, m1, m2, &hs, &ht);
993 ds[idx] = ds_val;
994 dt[idx] = dt_val;
995 dsdt[idx] = dsdt_val;
996 }
997 }
998
999 (ds, dt, dsdt)
1000 })
1001 .collect();
1002
1003 let (ds_vecs, (dt_vecs, dsdt_vecs)): (Vec<Vec<f64>>, (Vec<Vec<f64>>, Vec<Vec<f64>>)) =
1004 results.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
1005
1006 Some(Deriv2DResult {
1007 ds: reassemble_colmajor(&ds_vecs, n, ncol),
1008 dt: reassemble_colmajor(&dt_vecs, n, ncol),
1009 dsdt: reassemble_colmajor(&dsdt_vecs, n, ncol),
1010 })
1011}
1012
1013pub fn geometric_median_1d(
1023 data: &FdMatrix,
1024 argvals: &[f64],
1025 max_iter: usize,
1026 tol: f64,
1027) -> Vec<f64> {
1028 let (n, m) = data.shape();
1029 if n == 0 || m == 0 || argvals.len() != m {
1030 return Vec::new();
1031 }
1032
1033 let weights = simpsons_weights(argvals);
1034 weiszfeld_iteration(data, &weights, max_iter, tol)
1035}
1036
1037pub fn geometric_median_2d(
1048 data: &FdMatrix,
1049 argvals_s: &[f64],
1050 argvals_t: &[f64],
1051 max_iter: usize,
1052 tol: f64,
1053) -> Vec<f64> {
1054 let (n, m) = data.shape();
1055 let expected_cols = argvals_s.len() * argvals_t.len();
1056 if n == 0 || m == 0 || m != expected_cols {
1057 return Vec::new();
1058 }
1059
1060 let weights = simpsons_weights_2d(argvals_s, argvals_t);
1061 weiszfeld_iteration(data, &weights, max_iter, tol)
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use crate::test_helpers::uniform_grid;
1068 use std::f64::consts::PI;
1069
1070 #[test]
1073 fn test_mean_1d() {
1074 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();
1080 let mean = mean_1d(&mat);
1081 assert_eq!(mean, vec![2.0, 3.0, 4.0]);
1082 }
1083
1084 #[test]
1085 fn test_mean_1d_single_sample() {
1086 let data = vec![1.0, 2.0, 3.0];
1087 let mat = FdMatrix::from_column_major(data, 1, 3).unwrap();
1088 let mean = mean_1d(&mat);
1089 assert_eq!(mean, vec![1.0, 2.0, 3.0]);
1090 }
1091
1092 #[test]
1093 fn test_mean_1d_invalid() {
1094 assert!(mean_1d(&FdMatrix::zeros(0, 0)).is_empty());
1095 }
1096
1097 #[allow(deprecated)]
1098 #[test]
1099 fn test_mean_2d_delegates() {
1100 let data = vec![1.0, 3.0, 2.0, 4.0];
1101 let mat = FdMatrix::from_column_major(data, 2, 2).unwrap();
1102 let mean1d = mean_1d(&mat);
1103 let mean2d = mean_2d(&mat);
1104 assert_eq!(mean1d, mean2d);
1105 }
1106
1107 #[test]
1110 fn test_center_1d() {
1111 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();
1113 let centered = center_1d(&mat);
1114 assert_eq!(centered.as_slice(), &[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);
1116 }
1117
1118 #[test]
1119 fn test_center_1d_mean_zero() {
1120 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0];
1121 let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
1122 let centered = center_1d(&mat);
1123 let centered_mean = mean_1d(¢ered);
1124 for m in centered_mean {
1125 assert!(m.abs() < 1e-10, "Centered data should have zero mean");
1126 }
1127 }
1128
1129 #[test]
1130 fn test_center_1d_invalid() {
1131 let centered = center_1d(&FdMatrix::zeros(0, 0));
1132 assert!(centered.is_empty());
1133 }
1134
1135 #[test]
1138 fn test_norm_lp_1d_constant() {
1139 let argvals = uniform_grid(21);
1141 let data: Vec<f64> = vec![2.0; 21];
1142 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1143 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1144 assert_eq!(norms.len(), 1);
1145 assert!(
1146 (norms[0] - 2.0).abs() < 0.1,
1147 "L2 norm of constant 2 should be 2"
1148 );
1149 }
1150
1151 #[test]
1152 fn test_norm_lp_1d_sine() {
1153 let argvals = uniform_grid(101);
1155 let data: Vec<f64> = argvals.iter().map(|&x| (PI * x).sin()).collect();
1156 let mat = FdMatrix::from_column_major(data, 1, 101).unwrap();
1157 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1158 let expected = 0.5_f64.sqrt();
1159 assert!(
1160 (norms[0] - expected).abs() < 0.05,
1161 "Expected {}, got {}",
1162 expected,
1163 norms[0]
1164 );
1165 }
1166
1167 #[test]
1168 fn test_norm_lp_1d_invalid() {
1169 assert!(norm_lp_1d(&FdMatrix::zeros(0, 0), &[], 2.0).is_empty());
1170 }
1171
1172 #[test]
1175 fn test_deriv_1d_linear() {
1176 let argvals = uniform_grid(21);
1178 let data = argvals.clone();
1179 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1180 let deriv = deriv_1d(&mat, &argvals, 1);
1181 for j in 2..19 {
1183 assert!(
1184 (deriv[(0, j)] - 1.0).abs() < 0.1,
1185 "Derivative of x should be 1"
1186 );
1187 }
1188 }
1189
1190 #[test]
1191 fn test_deriv_1d_quadratic() {
1192 let argvals = uniform_grid(51);
1194 let data: Vec<f64> = argvals.iter().map(|&x| x * x).collect();
1195 let mat = FdMatrix::from_column_major(data, 1, 51).unwrap();
1196 let deriv = deriv_1d(&mat, &argvals, 1);
1197 for j in 5..45 {
1199 let expected = 2.0 * argvals[j];
1200 assert!(
1201 (deriv[(0, j)] - expected).abs() < 0.1,
1202 "Derivative of x^2 should be 2x"
1203 );
1204 }
1205 }
1206
1207 #[test]
1208 fn test_deriv_1d_invalid() {
1209 let result = deriv_1d(&FdMatrix::zeros(0, 0), &[], 1);
1210 assert!(result.is_empty() || result.as_slice().iter().all(|&x| x == 0.0));
1211 }
1212
1213 #[test]
1216 fn test_geometric_median_identical_curves() {
1217 let argvals = uniform_grid(21);
1219 let n = 5;
1220 let m = 21;
1221 let mut data = vec![0.0; n * m];
1222 for i in 0..n {
1223 for j in 0..m {
1224 data[i + j * n] = (2.0 * PI * argvals[j]).sin();
1225 }
1226 }
1227 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1228 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
1229 for j in 0..m {
1230 let expected = (2.0 * PI * argvals[j]).sin();
1231 assert!(
1232 (median[j] - expected).abs() < 0.01,
1233 "Median should equal all curves"
1234 );
1235 }
1236 }
1237
1238 #[test]
1239 fn test_geometric_median_converges() {
1240 let argvals = uniform_grid(21);
1241 let n = 10;
1242 let m = 21;
1243 let mut data = vec![0.0; n * m];
1244 for i in 0..n {
1245 for j in 0..m {
1246 data[i + j * n] = (i as f64 / n as f64) * argvals[j];
1247 }
1248 }
1249 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1250 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
1251 assert_eq!(median.len(), m);
1252 assert!(median.iter().all(|&x| x.is_finite()));
1253 }
1254
1255 #[test]
1256 fn test_geometric_median_invalid() {
1257 assert!(geometric_median_1d(&FdMatrix::zeros(0, 0), &[], 100, 1e-6).is_empty());
1258 }
1259
1260 #[test]
1263 fn test_deriv_2d_linear_surface() {
1264 let m1 = 11;
1267 let m2 = 11;
1268 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1269 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1270
1271 let n = 1; let ncol = m1 * m2;
1273 let mut data = vec![0.0; n * ncol];
1274
1275 for si in 0..m1 {
1276 for ti in 0..m2 {
1277 let s = argvals_s[si];
1278 let t = argvals_t[ti];
1279 let idx = si + ti * m1;
1280 data[idx] = 2.0 * s + 3.0 * t;
1281 }
1282 }
1283
1284 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1285 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
1286
1287 for si in 2..(m1 - 2) {
1289 for ti in 2..(m2 - 2) {
1290 let idx = si + ti * m1;
1291 assert!(
1292 (result.ds[(0, idx)] - 2.0).abs() < 0.2,
1293 "∂f/∂s at ({}, {}) = {}, expected 2",
1294 si,
1295 ti,
1296 result.ds[(0, idx)]
1297 );
1298 }
1299 }
1300
1301 for si in 2..(m1 - 2) {
1303 for ti in 2..(m2 - 2) {
1304 let idx = si + ti * m1;
1305 assert!(
1306 (result.dt[(0, idx)] - 3.0).abs() < 0.2,
1307 "∂f/∂t at ({}, {}) = {}, expected 3",
1308 si,
1309 ti,
1310 result.dt[(0, idx)]
1311 );
1312 }
1313 }
1314
1315 for si in 2..(m1 - 2) {
1317 for ti in 2..(m2 - 2) {
1318 let idx = si + ti * m1;
1319 assert!(
1320 result.dsdt[(0, idx)].abs() < 0.5,
1321 "∂²f/∂s∂t at ({}, {}) = {}, expected 0",
1322 si,
1323 ti,
1324 result.dsdt[(0, idx)]
1325 );
1326 }
1327 }
1328 }
1329
1330 #[test]
1331 fn test_deriv_2d_quadratic_surface() {
1332 let m1 = 21;
1335 let m2 = 21;
1336 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1337 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1338
1339 let n = 1;
1340 let ncol = m1 * m2;
1341 let mut data = vec![0.0; n * ncol];
1342
1343 for si in 0..m1 {
1344 for ti in 0..m2 {
1345 let s = argvals_s[si];
1346 let t = argvals_t[ti];
1347 let idx = si + ti * m1;
1348 data[idx] = s * t;
1349 }
1350 }
1351
1352 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1353 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
1354
1355 for si in 3..(m1 - 3) {
1357 for ti in 3..(m2 - 3) {
1358 let idx = si + ti * m1;
1359 let expected = argvals_t[ti];
1360 assert!(
1361 (result.ds[(0, idx)] - expected).abs() < 0.1,
1362 "∂f/∂s at ({}, {}) = {}, expected {}",
1363 si,
1364 ti,
1365 result.ds[(0, idx)],
1366 expected
1367 );
1368 }
1369 }
1370
1371 for si in 3..(m1 - 3) {
1373 for ti in 3..(m2 - 3) {
1374 let idx = si + ti * m1;
1375 let expected = argvals_s[si];
1376 assert!(
1377 (result.dt[(0, idx)] - expected).abs() < 0.1,
1378 "∂f/∂t at ({}, {}) = {}, expected {}",
1379 si,
1380 ti,
1381 result.dt[(0, idx)],
1382 expected
1383 );
1384 }
1385 }
1386
1387 for si in 3..(m1 - 3) {
1389 for ti in 3..(m2 - 3) {
1390 let idx = si + ti * m1;
1391 assert!(
1392 (result.dsdt[(0, idx)] - 1.0).abs() < 0.3,
1393 "∂²f/∂s∂t at ({}, {}) = {}, expected 1",
1394 si,
1395 ti,
1396 result.dsdt[(0, idx)]
1397 );
1398 }
1399 }
1400 }
1401
1402 #[test]
1403 fn test_deriv_2d_invalid_input() {
1404 let result = deriv_2d(&FdMatrix::zeros(0, 0), &[], &[], 0, 0);
1406 assert!(result.is_none());
1407
1408 let mat = FdMatrix::from_column_major(vec![1.0; 4], 1, 4).unwrap();
1410 let argvals = vec![0.0, 1.0];
1411 let result = deriv_2d(&mat, &argvals, &[0.0, 0.5, 1.0], 2, 2);
1412 assert!(result.is_none());
1413 }
1414
1415 #[test]
1418 fn test_geometric_median_2d_basic() {
1419 let m1 = 5;
1421 let m2 = 5;
1422 let m = m1 * m2;
1423 let n = 3;
1424 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1425 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1426
1427 let mut data = vec![0.0; n * m];
1428
1429 for i in 0..n {
1431 for si in 0..m1 {
1432 for ti in 0..m2 {
1433 let idx = si + ti * m1;
1434 let s = argvals_s[si];
1435 let t = argvals_t[ti];
1436 data[i + idx * n] = s + t;
1437 }
1438 }
1439 }
1440
1441 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1442 let median = geometric_median_2d(&mat, &argvals_s, &argvals_t, 100, 1e-6);
1443 assert_eq!(median.len(), m);
1444
1445 for si in 0..m1 {
1447 for ti in 0..m2 {
1448 let idx = si + ti * m1;
1449 let expected = argvals_s[si] + argvals_t[ti];
1450 assert!(
1451 (median[idx] - expected).abs() < 0.01,
1452 "Median at ({}, {}) = {}, expected {}",
1453 si,
1454 ti,
1455 median[idx],
1456 expected
1457 );
1458 }
1459 }
1460 }
1461
1462 #[test]
1465 fn functional_variance_equals_std_squared() {
1466 let data = FdMatrix::from_column_major(
1468 vec![
1469 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1470 ],
1471 3,
1472 4,
1473 )
1474 .unwrap();
1475 let var = functional_variance(&data).unwrap();
1476 let std = functional_std(&data).unwrap();
1477 for j in 0..4 {
1478 assert!(
1479 (std[j].powi(2) - var[j]).abs() < 1e-10,
1480 "at j={j}: std^2={} != var={}",
1481 std[j].powi(2),
1482 var[j]
1483 );
1484 }
1485 }
1486
1487 #[test]
1488 fn functional_covariance_diagonal_matches_variance() {
1489 let data = FdMatrix::from_column_major(
1490 vec![
1491 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1492 ],
1493 3,
1494 4,
1495 )
1496 .unwrap();
1497 let var = functional_variance(&data).unwrap();
1498 let cov = functional_covariance(&data).unwrap();
1499 for j in 0..4 {
1500 assert!(
1501 (cov[(j, j)] - var[j]).abs() < 1e-10,
1502 "at j={j}: cov[j,j]={} != var={}",
1503 cov[(j, j)],
1504 var[j]
1505 );
1506 }
1507 }
1508
1509 #[test]
1510 fn functional_variance_hand_computed() {
1511 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 4.0, 2.0], 2, 2).unwrap();
1518 let var = functional_variance(&data).unwrap();
1519 assert!((var[0] - 2.0).abs() < 1e-10, "var[0]={}", var[0]);
1520 assert!((var[1] - 2.0).abs() < 1e-10, "var[1]={}", var[1]);
1521 }
1522
1523 #[test]
1526 fn depth_based_median_argmax() {
1527 let n = 5;
1531 let m = 10;
1532 let mut data_vec = vec![0.0f64; n * m];
1533 for j in 0..m {
1539 data_vec[j * n] = 0.0;
1540 data_vec[1 + j * n] = 0.25;
1541 data_vec[2 + j * n] = 0.5;
1542 data_vec[3 + j * n] = 0.75;
1543 data_vec[4 + j * n] = 1.0;
1544 }
1545 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1546 let idx = depth_based_median(&data).unwrap();
1547 assert_eq!(idx, 2, "most central curve should be at index 2, got {idx}");
1548 }
1549
1550 #[test]
1551 fn trim_mean_alpha_zero_equals_mean() {
1552 let n = 5;
1553 let m = 4;
1554 let mut data_vec = vec![0.0f64; n * m];
1555 for i in 0..n {
1556 for j in 0..m {
1557 data_vec[i + j * n] = (i + 1) as f64 * (j + 1) as f64;
1558 }
1559 }
1560 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1561 let tm = trim_mean(&data, 0.0).unwrap();
1562 let mu = mean_1d(&data);
1563 for j in 0..m {
1564 assert!(
1565 (tm[j] - mu[j]).abs() < 1e-10,
1566 "at j={j}: trim_mean(alpha=0)={} != mean={}",
1567 tm[j],
1568 mu[j]
1569 );
1570 }
1571 }
1572
1573 #[test]
1574 fn trim_mean_rejects_bad_alpha() {
1575 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
1576 let result = trim_mean(&data, 1.0);
1578 assert!(
1579 matches!(
1580 result,
1581 Err(FdarError::InvalidParameter {
1582 parameter: "alpha",
1583 ..
1584 })
1585 ),
1586 "expected InvalidParameter for alpha=1.0, got {result:?}"
1587 );
1588 let result2 = trim_mean(&data, -0.1);
1590 assert!(
1591 matches!(
1592 result2,
1593 Err(FdarError::InvalidParameter {
1594 parameter: "alpha",
1595 ..
1596 })
1597 ),
1598 "expected InvalidParameter for alpha=-0.1, got {result2:?}"
1599 );
1600 }
1601
1602 #[test]
1605 fn functional_stats_input_validation() {
1606 use crate::error::FdarError;
1607
1608 let one_row = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
1610 assert!(
1611 matches!(
1612 functional_variance(&one_row),
1613 Err(FdarError::InvalidDimension {
1614 parameter: "data",
1615 ..
1616 })
1617 ),
1618 "functional_variance should reject n=1"
1619 );
1620 assert!(
1621 matches!(
1622 functional_std(&one_row),
1623 Err(FdarError::InvalidDimension {
1624 parameter: "data",
1625 ..
1626 })
1627 ),
1628 "functional_std should reject n=1"
1629 );
1630 assert!(
1631 matches!(
1632 functional_covariance(&one_row),
1633 Err(FdarError::InvalidDimension {
1634 parameter: "data",
1635 ..
1636 })
1637 ),
1638 "functional_covariance should reject n=1"
1639 );
1640
1641 let zero_rows = FdMatrix::zeros(0, 3);
1643 assert!(
1644 matches!(
1645 depth_based_median(&zero_rows),
1646 Err(FdarError::InvalidDimension {
1647 parameter: "data",
1648 ..
1649 })
1650 ),
1651 "depth_based_median should reject n=0"
1652 );
1653 assert!(
1654 matches!(
1655 trim_mean(&zero_rows, 0.0),
1656 Err(FdarError::InvalidDimension {
1657 parameter: "data",
1658 ..
1659 })
1660 ),
1661 "trim_mean should reject n=0"
1662 );
1663 }
1664
1665 #[test]
1666 fn test_nan_mean_no_panic() {
1667 let mut data_vec = vec![1.0; 6];
1668 data_vec[2] = f64::NAN;
1669 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1670 let m = mean_1d(&data);
1671 assert_eq!(m.len(), 3);
1672 }
1673
1674 #[test]
1675 fn test_nan_center_no_panic() {
1676 let mut data_vec = vec![1.0; 6];
1677 data_vec[2] = f64::NAN;
1678 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1679 let c = center_1d(&data);
1680 assert_eq!(c.nrows(), 2);
1681 }
1682
1683 #[test]
1684 fn test_nan_norm_no_panic() {
1685 let mut data_vec = vec![1.0; 6];
1686 data_vec[2] = f64::NAN;
1687 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1688 let argvals = vec![0.0, 0.5, 1.0];
1689 let norms = norm_lp_1d(&data, &argvals, 2.0);
1690 assert_eq!(norms.len(), 2);
1691 }
1692
1693 #[test]
1694 fn test_n1_norm() {
1695 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0], 1, 3).unwrap();
1696 let argvals = vec![0.0, 0.5, 1.0];
1697 let norms = norm_lp_1d(&data, &argvals, 2.0);
1698 assert_eq!(norms.len(), 1);
1699 assert!(norms[0] > 0.0);
1700 }
1701
1702 #[test]
1703 fn test_n2_center() {
1704 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0], 2, 2).unwrap();
1705 let centered = center_1d(&data);
1706 assert!((centered[(0, 0)] - (-1.0)).abs() < 1e-12);
1709 assert!((centered[(1, 0)] - 1.0).abs() < 1e-12);
1710 }
1711
1712 #[test]
1713 fn test_deriv_nderiv0() {
1714 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).unwrap();
1716 let argvals = vec![0.0, 0.5, 1.0];
1717 let result = deriv_1d(&data, &argvals, 0);
1718 assert_eq!(result.shape(), data.shape());
1719 for i in 0..2 {
1720 for j in 0..3 {
1721 assert!((result[(i, j)] - data[(i, j)]).abs() < 1e-12);
1722 }
1723 }
1724 }
1725
1726 #[test]
1729 fn test_normalize_autoscale() {
1730 let data = FdMatrix::from_column_major(
1732 vec![
1733 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1734 ],
1735 3,
1736 4,
1737 )
1738 .unwrap();
1739 let scaled = normalize(&data, NormalizationMethod::Autoscale);
1740 for j in 0..4 {
1742 let col = scaled.column(j);
1743 let mean = col.iter().sum::<f64>() / 3.0;
1744 assert!(
1745 mean.abs() < 1e-10,
1746 "column {j} mean should be 0, got {mean}"
1747 );
1748 let var = col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1749 assert!(
1750 (var - 1.0).abs() < 1e-10,
1751 "column {j} variance should be 1, got {var}"
1752 );
1753 }
1754 }
1755
1756 #[test]
1757 fn test_normalize_pareto() {
1758 let data =
1759 FdMatrix::from_column_major(vec![1.0, 5.0, 3.0, 10.0, 20.0, 30.0], 2, 3).unwrap();
1760 let scaled = normalize(&data, NormalizationMethod::Pareto);
1761 for j in 0..3 {
1763 let col = scaled.column(j);
1764 let mean = col.iter().sum::<f64>() / 2.0;
1765 assert!(mean.abs() < 1e-10, "column {j} mean should be 0");
1766 }
1767 }
1768
1769 #[test]
1770 fn test_normalize_range() {
1771 let data = FdMatrix::from_column_major(vec![0.0, 10.0, 2.0, 8.0], 2, 2).unwrap();
1772 let scaled = normalize(&data, NormalizationMethod::Range);
1773 assert!((scaled[(0, 0)] - (-0.5)).abs() < 1e-10);
1775 assert!((scaled[(1, 0)] - 0.5).abs() < 1e-10);
1776 }
1777
1778 #[test]
1779 fn test_normalize_curve_center() {
1780 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1781 let result = normalize(&data, NormalizationMethod::CurveCenter);
1782 assert!((result[(0, 0)] - (-2.0)).abs() < 1e-10);
1784 assert!((result[(0, 1)] - 0.0).abs() < 1e-10);
1785 assert!((result[(0, 2)] - 2.0).abs() < 1e-10);
1786 }
1787
1788 #[test]
1789 fn test_normalize_curve_standardize() {
1790 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1791 let result = normalize(&data, NormalizationMethod::CurveStandardize);
1792 for i in 0..2 {
1794 let row: Vec<f64> = (0..3).map(|j| result[(i, j)]).collect();
1795 let mean = row.iter().sum::<f64>() / 3.0;
1796 assert!(mean.abs() < 1e-10, "row {i} mean should be 0");
1797 let var = row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1798 assert!((var - 1.0).abs() < 1e-10, "row {i} variance should be 1");
1799 }
1800 }
1801
1802 #[test]
1803 fn test_normalize_curve_range() {
1804 let data =
1805 FdMatrix::from_column_major(vec![2.0, 10.0, 4.0, 20.0, 6.0, 30.0], 2, 3).unwrap();
1806 let result = normalize(&data, NormalizationMethod::CurveRange);
1807 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1809 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1810 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1811 }
1812
1813 #[test]
1814 fn test_normalize_center_matches_center_1d() {
1815 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0], 2, 3).unwrap();
1816 let a = center_1d(&data);
1817 let b = normalize(&data, NormalizationMethod::Center);
1818 assert_eq!(a.as_slice(), b.as_slice());
1819 }
1820
1821 #[test]
1822 fn test_normalize_curve_lp_l2() {
1823 let data = FdMatrix::from_column_major(vec![3.0, 0.0, 0.0, 4.0, 0.0, 0.0], 2, 3).unwrap();
1825 let t = vec![0.0, 0.5, 1.0];
1826 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1827 let norms = norm_lp_1d(&result, &t, 2.0);
1830 assert!(
1831 (norms[0] - 1.0).abs() < 0.1,
1832 "L2 norm after normalization should be ≈ 1, got {}",
1833 norms[0]
1834 );
1835 }
1836
1837 #[test]
1838 fn test_normalize_curve_lp_l1() {
1839 let data = FdMatrix::from_column_major(vec![2.0, 4.0, 6.0, 8.0], 2, 2).unwrap();
1840 let t = vec![0.0, 1.0];
1841 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(1.0));
1842 let norms = norm_lp_1d(&result, &t, 1.0);
1844 for (i, &norm) in norms.iter().enumerate() {
1845 assert!(
1846 (norm - 1.0).abs() < 0.1,
1847 "curve {i} L1 norm after normalization should be ≈ 1, got {norm}"
1848 );
1849 }
1850 }
1851
1852 #[test]
1853 fn test_normalize_curve_lp_linf() {
1854 let data =
1855 FdMatrix::from_column_major(vec![2.0, -5.0, 4.0, -10.0, 6.0, 15.0], 2, 3).unwrap();
1856 let t = vec![0.0, 0.5, 1.0];
1857 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(f64::INFINITY));
1858 for i in 0..2 {
1860 let max_abs: f64 = (0..3).map(|j| result[(i, j)].abs()).fold(0.0, f64::max);
1861 assert!(
1862 (max_abs - 1.0).abs() < 1e-10,
1863 "curve {i} max abs after L-inf normalization should be 1, got {max_abs}"
1864 );
1865 }
1866 }
1867
1868 #[test]
1869 fn test_normalize_curve_lp_zero_curve() {
1870 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0, 2.0], 2, 2).unwrap();
1872 let t = vec![0.0, 1.0];
1873 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1874 assert!((result[(0, 0)]).abs() < 1e-15);
1876 assert!((result[(0, 1)]).abs() < 1e-15);
1877 assert!(result[(1, 0)].abs() > 0.0);
1879 }
1880}