1use crate::error::FdarError;
4use crate::helpers::{simpsons_weights, simpsons_weights_2d, NUMERICAL_EPS};
5use crate::iter_maybe_parallel;
6use crate::matrix::FdMatrix;
7#[cfg(feature = "parallel")]
8use rayon::iter::ParallelIterator;
9
10fn finite_diff_1d(
15 values: impl Fn(usize) -> f64,
16 idx: usize,
17 n_points: usize,
18 step_sizes: &[f64],
19) -> f64 {
20 if idx == 0 {
21 (values(1) - values(0)) / step_sizes[0]
22 } else if idx == n_points - 1 {
23 (values(n_points - 1) - values(n_points - 2)) / step_sizes[n_points - 1]
24 } else {
25 (values(idx + 1) - values(idx - 1)) / step_sizes[idx]
26 }
27}
28
29fn compute_2d_derivatives(
33 get_val: impl Fn(usize, usize) -> f64,
34 si: usize,
35 ti: usize,
36 m1: usize,
37 m2: usize,
38 hs: &[f64],
39 ht: &[f64],
40) -> (f64, f64, f64) {
41 let ds = finite_diff_1d(|s| get_val(s, ti), si, m1, hs);
43
44 let dt = finite_diff_1d(|t| get_val(si, t), ti, m2, ht);
46
47 let denom = hs[si] * ht[ti];
49
50 let (s_lo, s_hi) = if si == 0 {
52 (0, 1)
53 } else if si == m1 - 1 {
54 (m1 - 2, m1 - 1)
55 } else {
56 (si - 1, si + 1)
57 };
58
59 let (t_lo, t_hi) = if ti == 0 {
60 (0, 1)
61 } else if ti == m2 - 1 {
62 (m2 - 2, m2 - 1)
63 } else {
64 (ti - 1, ti + 1)
65 };
66
67 let dsdt = (get_val(s_hi, t_hi) - get_val(s_lo, t_hi) - get_val(s_hi, t_lo)
68 + get_val(s_lo, t_lo))
69 / denom;
70
71 (ds, dt, dsdt)
72}
73
74fn weiszfeld_iteration(data: &FdMatrix, weights: &[f64], max_iter: usize, tol: f64) -> Vec<f64> {
78 let (n, m) = data.shape();
79
80 let mut median: Vec<f64> = (0..m)
82 .map(|j| {
83 let col = data.column(j);
84 col.iter().sum::<f64>() / n as f64
85 })
86 .collect();
87
88 for _ in 0..max_iter {
89 let distances: Vec<f64> = (0..n)
91 .map(|i| {
92 let mut dist_sq = 0.0;
93 for j in 0..m {
94 let diff = data[(i, j)] - median[j];
95 dist_sq += diff * diff * weights[j];
96 }
97 dist_sq.sqrt()
98 })
99 .collect();
100
101 let inv_distances: Vec<f64> = distances
103 .iter()
104 .map(|d| {
105 if *d > NUMERICAL_EPS {
106 1.0 / d
107 } else {
108 1.0 / NUMERICAL_EPS
109 }
110 })
111 .collect();
112
113 let sum_inv_dist: f64 = inv_distances.iter().sum();
114
115 let new_median: Vec<f64> = (0..m)
117 .map(|j| {
118 let mut weighted_sum = 0.0;
119 for i in 0..n {
120 weighted_sum += data[(i, j)] * inv_distances[i];
121 }
122 weighted_sum / sum_inv_dist
123 })
124 .collect();
125
126 let diff: f64 = median
128 .iter()
129 .zip(new_median.iter())
130 .map(|(a, b)| (a - b).abs())
131 .sum::<f64>()
132 / m as f64;
133
134 median = new_median;
135
136 if diff < tol {
137 break;
138 }
139 }
140
141 median
142}
143
144pub fn mean_1d(data: &FdMatrix) -> Vec<f64> {
168 let (n, m) = data.shape();
169 if n == 0 || m == 0 {
170 return Vec::new();
171 }
172
173 iter_maybe_parallel!(0..m)
174 .map(|j| {
175 let col = data.column(j);
176 col.iter().sum::<f64>() / n as f64
177 })
178 .collect()
179}
180
181pub fn mean_2d(data: &FdMatrix) -> Vec<f64> {
185 mean_1d(data)
187}
188
189pub fn center_1d(data: &FdMatrix) -> FdMatrix {
213 let (n, m) = data.shape();
214 if n == 0 || m == 0 {
215 return FdMatrix::zeros(0, 0);
216 }
217
218 let means: Vec<f64> = iter_maybe_parallel!(0..m)
220 .map(|j| {
221 let col = data.column(j);
222 col.iter().sum::<f64>() / n as f64
223 })
224 .collect();
225
226 let mut centered = FdMatrix::zeros(n, m);
228 for j in 0..m {
229 let col = centered.column_mut(j);
230 let src = data.column(j);
231 for i in 0..n {
232 col[i] = src[i] - means[j];
233 }
234 }
235
236 centered
237}
238
239pub fn functional_variance(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
269 let (n, m) = data.shape();
270 if n < 2 {
271 return Err(FdarError::InvalidDimension {
272 parameter: "data",
273 expected: ">= 2 rows".to_string(),
274 actual: n.to_string(),
275 });
276 }
277 let means = mean_1d(data);
278 let var: Vec<f64> = (0..m)
279 .map(|j| {
280 let col = data.column(j);
281 let mu = means[j];
282 col.iter().map(|&x| (x - mu).powi(2)).sum::<f64>() / (n - 1) as f64
283 })
284 .collect();
285 Ok(var)
286}
287
288pub fn functional_std(data: &FdMatrix) -> Result<Vec<f64>, FdarError> {
317 Ok(functional_variance(data)?
318 .iter()
319 .map(|v| v.sqrt())
320 .collect())
321}
322
323pub fn functional_covariance(data: &FdMatrix) -> Result<FdMatrix, FdarError> {
359 let (n, m) = data.shape();
360 if n < 2 {
361 return Err(FdarError::InvalidDimension {
362 parameter: "data",
363 expected: ">= 2 rows".to_string(),
364 actual: n.to_string(),
365 });
366 }
367 m.checked_mul(m)
369 .ok_or_else(|| FdarError::InvalidParameter {
370 parameter: "data",
371 message: format!(
372 "m={m} is too large: m*m would overflow usize (max {})",
373 usize::MAX
374 ),
375 })?;
376
377 let centered = center_1d(data);
378 let mut cov = FdMatrix::zeros(m, m);
379 let denom = (n - 1) as f64;
380 for j1 in 0..m {
381 let col1 = centered.column(j1);
382 for j2 in j1..m {
383 let col2 = centered.column(j2);
384 let val: f64 = col1
385 .iter()
386 .zip(col2.iter())
387 .map(|(&a, &b)| a * b)
388 .sum::<f64>()
389 / denom;
390 cov[(j1, j2)] = val;
391 cov[(j2, j1)] = val; }
393 }
394 Ok(cov)
395}
396
397pub fn depth_based_median(data: &FdMatrix) -> Result<usize, FdarError> {
429 let (n, _) = data.shape();
430 if n == 0 {
431 return Err(FdarError::InvalidDimension {
432 parameter: "data",
433 expected: ">= 1 row".to_string(),
434 actual: "0".to_string(),
435 });
436 }
437 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
438 depths
439 .iter()
440 .enumerate()
441 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
442 .map(|(i, _)| i)
443 .ok_or_else(|| FdarError::ComputationFailed {
444 operation: "depth_based_median",
445 detail: "depth vector is empty".to_string(),
446 })
447}
448
449pub fn trim_mean(data: &FdMatrix, alpha: f64) -> Result<Vec<f64>, FdarError> {
484 if !(0.0..1.0).contains(&alpha) {
485 return Err(FdarError::InvalidParameter {
486 parameter: "alpha",
487 message: format!("must be in [0, 1), got {alpha}"),
488 });
489 }
490 let (n, m) = data.shape();
491 if n == 0 {
492 return Err(FdarError::InvalidDimension {
493 parameter: "data",
494 expected: ">= 1 row".to_string(),
495 actual: "0".to_string(),
496 });
497 }
498
499 let depths = crate::depth::fraiman_muniz_1d(data, data, true);
501
502 let k = (alpha * n as f64).floor() as usize;
504
505 let mut indices: Vec<usize> = (0..n).collect();
507 indices.sort_unstable_by(|&a, &b| {
508 depths[b]
509 .partial_cmp(&depths[a])
510 .unwrap_or(std::cmp::Ordering::Equal)
511 });
512 let retained = &indices[..n - k];
513
514 let n_ret = retained.len() as f64;
516 let mean: Vec<f64> = (0..m)
517 .map(|j| retained.iter().map(|&i| data[(i, j)]).sum::<f64>() / n_ret)
518 .collect();
519
520 Ok(mean)
521}
522
523#[derive(Debug, Clone, Copy, PartialEq)]
525#[non_exhaustive]
526pub enum NormalizationMethod {
527 Center,
529 Autoscale,
531 Pareto,
533 Range,
535 CurveCenter,
537 CurveStandardize,
539 CurveRange,
541 CurveLp(f64),
547}
548
549pub fn normalize(data: &FdMatrix, method: NormalizationMethod) -> FdMatrix {
584 match method {
585 NormalizationMethod::CurveLp(_) => {
586 panic!("CurveLp requires argvals — use normalize_with_argvals()")
587 }
588 _ => {
589 let argvals: Vec<f64> = (0..data.ncols())
590 .map(|j| j as f64 / (data.ncols() - 1).max(1) as f64)
591 .collect();
592 normalize_with_argvals(data, &argvals, method)
593 }
594 }
595}
596
597pub fn normalize_with_argvals(
616 data: &FdMatrix,
617 argvals: &[f64],
618 method: NormalizationMethod,
619) -> FdMatrix {
620 let (n, m) = data.shape();
621 if n == 0 || m == 0 {
622 return FdMatrix::zeros(n, m);
623 }
624
625 match method {
626 NormalizationMethod::Center => center_1d(data),
627 NormalizationMethod::Autoscale => column_scale(data, n, m, ScaleKind::StdDev),
628 NormalizationMethod::Pareto => column_scale(data, n, m, ScaleKind::SqrtStdDev),
629 NormalizationMethod::Range => column_scale(data, n, m, ScaleKind::Range),
630 NormalizationMethod::CurveCenter => row_normalize(data, n, m, RowNorm::Center),
631 NormalizationMethod::CurveStandardize => row_normalize(data, n, m, RowNorm::Standardize),
632 NormalizationMethod::CurveRange => row_normalize(data, n, m, RowNorm::Range),
633 NormalizationMethod::CurveLp(p) => curve_lp_normalize(data, argvals, n, m, p),
634 }
635}
636
637#[derive(Clone, Copy)]
638enum ScaleKind {
639 StdDev,
640 SqrtStdDev,
641 Range,
642}
643
644fn column_scale(data: &FdMatrix, n: usize, m: usize, kind: ScaleKind) -> FdMatrix {
645 let mut result = FdMatrix::zeros(n, m);
646 for j in 0..m {
647 let col = data.column(j);
648 let mean = col.iter().sum::<f64>() / n as f64;
649 let scale = match kind {
650 ScaleKind::StdDev => {
651 let var =
652 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
653 var.sqrt()
654 }
655 ScaleKind::SqrtStdDev => {
656 let var =
657 col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / (n - 1).max(1) as f64;
658 var.sqrt().sqrt()
659 }
660 ScaleKind::Range => {
661 let min = col.iter().copied().fold(f64::INFINITY, f64::min);
662 let max = col.iter().copied().fold(f64::NEG_INFINITY, f64::max);
663 max - min
664 }
665 };
666 let out = result.column_mut(j);
667 let denom = if scale > 1e-15 { scale } else { 1.0 };
668 for i in 0..n {
669 out[i] = (col[i] - mean) / denom;
670 }
671 }
672 result
673}
674
675#[derive(Clone, Copy)]
676enum RowNorm {
677 Center,
678 Standardize,
679 Range,
680}
681
682fn row_normalize(data: &FdMatrix, n: usize, m: usize, kind: RowNorm) -> FdMatrix {
683 let mut result = FdMatrix::zeros(n, m);
684 for i in 0..n {
685 let row: Vec<f64> = (0..m).map(|j| data[(i, j)]).collect();
686 let mean = row.iter().sum::<f64>() / m as f64;
687 match kind {
688 RowNorm::Center => {
689 for j in 0..m {
690 result[(i, j)] = row[j] - mean;
691 }
692 }
693 RowNorm::Standardize => {
694 let std = (row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>()
695 / (m - 1).max(1) as f64)
696 .sqrt();
697 let denom = if std > 1e-15 { std } else { 1.0 };
698 for j in 0..m {
699 result[(i, j)] = (row[j] - mean) / denom;
700 }
701 }
702 RowNorm::Range => {
703 let min = row.iter().copied().fold(f64::INFINITY, f64::min);
704 let max = row.iter().copied().fold(f64::NEG_INFINITY, f64::max);
705 let range = max - min;
706 let denom = if range > 1e-15 { range } else { 1.0 };
707 for j in 0..m {
708 result[(i, j)] = (row[j] - min) / denom;
709 }
710 }
711 }
712 }
713 result
714}
715
716fn curve_lp_normalize(data: &FdMatrix, argvals: &[f64], n: usize, m: usize, p: f64) -> FdMatrix {
718 let mut result = FdMatrix::zeros(n, m);
719 if p.is_infinite() {
720 for i in 0..n {
722 let max_abs = (0..m).map(|j| data[(i, j)].abs()).fold(0.0f64, f64::max);
723 let denom = if max_abs > 1e-15 { max_abs } else { 1.0 };
724 for j in 0..m {
725 result[(i, j)] = data[(i, j)] / denom;
726 }
727 }
728 } else {
729 let norms = norm_lp_1d(data, argvals, p);
730 for i in 0..n {
731 let denom = if norms[i] > 1e-15 { norms[i] } else { 1.0 };
732 for j in 0..m {
733 result[(i, j)] = data[(i, j)] / denom;
734 }
735 }
736 }
737 result
738}
739
740pub fn norm_lp_1d(data: &FdMatrix, argvals: &[f64], p: f64) -> Vec<f64> {
750 let (n, m) = data.shape();
751 if n == 0 || m == 0 || argvals.len() != m {
752 return Vec::new();
753 }
754
755 let weights = simpsons_weights(argvals);
756
757 if (p - 2.0).abs() < 1e-14 {
758 iter_maybe_parallel!(0..n)
759 .map(|i| {
760 let mut integral = 0.0;
761 for j in 0..m {
762 let v = data[(i, j)];
763 integral += v * v * weights[j];
764 }
765 integral.sqrt()
766 })
767 .collect()
768 } else if (p - 1.0).abs() < 1e-14 {
769 iter_maybe_parallel!(0..n)
770 .map(|i| {
771 let mut integral = 0.0;
772 for j in 0..m {
773 integral += data[(i, j)].abs() * weights[j];
774 }
775 integral
776 })
777 .collect()
778 } else {
779 iter_maybe_parallel!(0..n)
780 .map(|i| {
781 let mut integral = 0.0;
782 for j in 0..m {
783 integral += data[(i, j)].abs().powf(p) * weights[j];
784 }
785 integral.powf(1.0 / p)
786 })
787 .collect()
788 }
789}
790
791fn deriv_1d_step(
817 current: &FdMatrix,
818 n: usize,
819 m: usize,
820 h0: f64,
821 hn: f64,
822 h_central: &[f64],
823) -> FdMatrix {
824 let mut next = FdMatrix::zeros(n, m);
825 let src_col0 = current.column(0);
827 let src_col1 = current.column(1);
828 let dst = next.column_mut(0);
829 for i in 0..n {
830 dst[i] = (src_col1[i] - src_col0[i]) / h0;
831 }
832 for j in 1..(m - 1) {
834 let src_prev = current.column(j - 1);
835 let src_next = current.column(j + 1);
836 let dst = next.column_mut(j);
837 let h = h_central[j - 1];
838 for i in 0..n {
839 dst[i] = (src_next[i] - src_prev[i]) / h;
840 }
841 }
842 let src_colm2 = current.column(m - 2);
844 let src_colm1 = current.column(m - 1);
845 let dst = next.column_mut(m - 1);
846 for i in 0..n {
847 dst[i] = (src_colm1[i] - src_colm2[i]) / hn;
848 }
849 next
850}
851
852pub fn deriv_1d(data: &FdMatrix, argvals: &[f64], nderiv: usize) -> FdMatrix {
853 let (n, m) = data.shape();
854 if n == 0 || m < 2 || argvals.len() != m {
855 return FdMatrix::zeros(n, m);
856 }
857 if nderiv == 0 {
858 return data.clone();
859 }
860
861 let mut current = data.clone();
862
863 let h0 = argvals[1] - argvals[0];
865 let hn = argvals[m - 1] - argvals[m - 2];
866 let h_central: Vec<f64> = (1..(m - 1))
867 .map(|j| argvals[j + 1] - argvals[j - 1])
868 .collect();
869
870 for _ in 0..nderiv {
871 current = deriv_1d_step(¤t, n, m, h0, hn, &h_central);
872 }
873
874 current
875}
876
877#[derive(Debug, Clone, PartialEq)]
879#[non_exhaustive]
880pub struct Deriv2DResult {
881 pub ds: FdMatrix,
883 pub dt: FdMatrix,
885 pub dsdt: FdMatrix,
887}
888
889fn compute_step_sizes(argvals: &[f64]) -> Vec<f64> {
893 let m = argvals.len();
894 if m < 2 {
895 return vec![1.0; m];
896 }
897 (0..m)
898 .map(|j| {
899 if j == 0 {
900 argvals[1] - argvals[0]
901 } else if j == m - 1 {
902 argvals[m - 1] - argvals[m - 2]
903 } else {
904 argvals[j + 1] - argvals[j - 1]
905 }
906 })
907 .collect()
908}
909
910fn reassemble_colmajor(rows: &[Vec<f64>], n: usize, ncol: usize) -> FdMatrix {
912 let mut mat = FdMatrix::zeros(n, ncol);
913 for i in 0..n {
914 for j in 0..ncol {
915 mat[(i, j)] = rows[i][j];
916 }
917 }
918 mat
919}
920
921pub fn deriv_2d(
935 data: &FdMatrix,
936 argvals_s: &[f64],
937 argvals_t: &[f64],
938 m1: usize,
939 m2: usize,
940) -> Option<Deriv2DResult> {
941 let n = data.nrows();
942 let ncol = m1 * m2;
943 if n == 0
944 || ncol == 0
945 || m1 < 2
946 || m2 < 2
947 || data.ncols() != ncol
948 || argvals_s.len() != m1
949 || argvals_t.len() != m2
950 {
951 return None;
952 }
953
954 let hs = compute_step_sizes(argvals_s);
955 let ht = compute_step_sizes(argvals_t);
956
957 let results: Vec<(Vec<f64>, Vec<f64>, Vec<f64>)> = iter_maybe_parallel!(0..n)
959 .map(|i| {
960 let mut ds = vec![0.0; ncol];
961 let mut dt = vec![0.0; ncol];
962 let mut dsdt = vec![0.0; ncol];
963
964 let get_val = |si: usize, ti: usize| -> f64 { data[(i, si + ti * m1)] };
965
966 for ti in 0..m2 {
967 for si in 0..m1 {
968 let idx = si + ti * m1;
969 let (ds_val, dt_val, dsdt_val) =
970 compute_2d_derivatives(get_val, si, ti, m1, m2, &hs, &ht);
971 ds[idx] = ds_val;
972 dt[idx] = dt_val;
973 dsdt[idx] = dsdt_val;
974 }
975 }
976
977 (ds, dt, dsdt)
978 })
979 .collect();
980
981 let (ds_vecs, (dt_vecs, dsdt_vecs)): (Vec<Vec<f64>>, (Vec<Vec<f64>>, Vec<Vec<f64>>)) =
982 results.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
983
984 Some(Deriv2DResult {
985 ds: reassemble_colmajor(&ds_vecs, n, ncol),
986 dt: reassemble_colmajor(&dt_vecs, n, ncol),
987 dsdt: reassemble_colmajor(&dsdt_vecs, n, ncol),
988 })
989}
990
991pub fn geometric_median_1d(
1001 data: &FdMatrix,
1002 argvals: &[f64],
1003 max_iter: usize,
1004 tol: f64,
1005) -> Vec<f64> {
1006 let (n, m) = data.shape();
1007 if n == 0 || m == 0 || argvals.len() != m {
1008 return Vec::new();
1009 }
1010
1011 let weights = simpsons_weights(argvals);
1012 weiszfeld_iteration(data, &weights, max_iter, tol)
1013}
1014
1015pub fn geometric_median_2d(
1026 data: &FdMatrix,
1027 argvals_s: &[f64],
1028 argvals_t: &[f64],
1029 max_iter: usize,
1030 tol: f64,
1031) -> Vec<f64> {
1032 let (n, m) = data.shape();
1033 let expected_cols = argvals_s.len() * argvals_t.len();
1034 if n == 0 || m == 0 || m != expected_cols {
1035 return Vec::new();
1036 }
1037
1038 let weights = simpsons_weights_2d(argvals_s, argvals_t);
1039 weiszfeld_iteration(data, &weights, max_iter, tol)
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045 use crate::test_helpers::uniform_grid;
1046 use std::f64::consts::PI;
1047
1048 #[test]
1051 fn test_mean_1d() {
1052 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();
1058 let mean = mean_1d(&mat);
1059 assert_eq!(mean, vec![2.0, 3.0, 4.0]);
1060 }
1061
1062 #[test]
1063 fn test_mean_1d_single_sample() {
1064 let data = vec![1.0, 2.0, 3.0];
1065 let mat = FdMatrix::from_column_major(data, 1, 3).unwrap();
1066 let mean = mean_1d(&mat);
1067 assert_eq!(mean, vec![1.0, 2.0, 3.0]);
1068 }
1069
1070 #[test]
1071 fn test_mean_1d_invalid() {
1072 assert!(mean_1d(&FdMatrix::zeros(0, 0)).is_empty());
1073 }
1074
1075 #[test]
1076 fn test_mean_2d_delegates() {
1077 let data = vec![1.0, 3.0, 2.0, 4.0];
1078 let mat = FdMatrix::from_column_major(data, 2, 2).unwrap();
1079 let mean1d = mean_1d(&mat);
1080 let mean2d = mean_2d(&mat);
1081 assert_eq!(mean1d, mean2d);
1082 }
1083
1084 #[test]
1087 fn test_center_1d() {
1088 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();
1090 let centered = center_1d(&mat);
1091 assert_eq!(centered.as_slice(), &[-1.0, 1.0, -1.0, 1.0, -1.0, 1.0]);
1093 }
1094
1095 #[test]
1096 fn test_center_1d_mean_zero() {
1097 let data = vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0];
1098 let mat = FdMatrix::from_column_major(data, 2, 3).unwrap();
1099 let centered = center_1d(&mat);
1100 let centered_mean = mean_1d(¢ered);
1101 for m in centered_mean {
1102 assert!(m.abs() < 1e-10, "Centered data should have zero mean");
1103 }
1104 }
1105
1106 #[test]
1107 fn test_center_1d_invalid() {
1108 let centered = center_1d(&FdMatrix::zeros(0, 0));
1109 assert!(centered.is_empty());
1110 }
1111
1112 #[test]
1115 fn test_norm_lp_1d_constant() {
1116 let argvals = uniform_grid(21);
1118 let data: Vec<f64> = vec![2.0; 21];
1119 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1120 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1121 assert_eq!(norms.len(), 1);
1122 assert!(
1123 (norms[0] - 2.0).abs() < 0.1,
1124 "L2 norm of constant 2 should be 2"
1125 );
1126 }
1127
1128 #[test]
1129 fn test_norm_lp_1d_sine() {
1130 let argvals = uniform_grid(101);
1132 let data: Vec<f64> = argvals.iter().map(|&x| (PI * x).sin()).collect();
1133 let mat = FdMatrix::from_column_major(data, 1, 101).unwrap();
1134 let norms = norm_lp_1d(&mat, &argvals, 2.0);
1135 let expected = 0.5_f64.sqrt();
1136 assert!(
1137 (norms[0] - expected).abs() < 0.05,
1138 "Expected {}, got {}",
1139 expected,
1140 norms[0]
1141 );
1142 }
1143
1144 #[test]
1145 fn test_norm_lp_1d_invalid() {
1146 assert!(norm_lp_1d(&FdMatrix::zeros(0, 0), &[], 2.0).is_empty());
1147 }
1148
1149 #[test]
1152 fn test_deriv_1d_linear() {
1153 let argvals = uniform_grid(21);
1155 let data = argvals.clone();
1156 let mat = FdMatrix::from_column_major(data, 1, 21).unwrap();
1157 let deriv = deriv_1d(&mat, &argvals, 1);
1158 for j in 2..19 {
1160 assert!(
1161 (deriv[(0, j)] - 1.0).abs() < 0.1,
1162 "Derivative of x should be 1"
1163 );
1164 }
1165 }
1166
1167 #[test]
1168 fn test_deriv_1d_quadratic() {
1169 let argvals = uniform_grid(51);
1171 let data: Vec<f64> = argvals.iter().map(|&x| x * x).collect();
1172 let mat = FdMatrix::from_column_major(data, 1, 51).unwrap();
1173 let deriv = deriv_1d(&mat, &argvals, 1);
1174 for j in 5..45 {
1176 let expected = 2.0 * argvals[j];
1177 assert!(
1178 (deriv[(0, j)] - expected).abs() < 0.1,
1179 "Derivative of x^2 should be 2x"
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn test_deriv_1d_invalid() {
1186 let result = deriv_1d(&FdMatrix::zeros(0, 0), &[], 1);
1187 assert!(result.is_empty() || result.as_slice().iter().all(|&x| x == 0.0));
1188 }
1189
1190 #[test]
1193 fn test_geometric_median_identical_curves() {
1194 let argvals = uniform_grid(21);
1196 let n = 5;
1197 let m = 21;
1198 let mut data = vec![0.0; n * m];
1199 for i in 0..n {
1200 for j in 0..m {
1201 data[i + j * n] = (2.0 * PI * argvals[j]).sin();
1202 }
1203 }
1204 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1205 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
1206 for j in 0..m {
1207 let expected = (2.0 * PI * argvals[j]).sin();
1208 assert!(
1209 (median[j] - expected).abs() < 0.01,
1210 "Median should equal all curves"
1211 );
1212 }
1213 }
1214
1215 #[test]
1216 fn test_geometric_median_converges() {
1217 let argvals = uniform_grid(21);
1218 let n = 10;
1219 let m = 21;
1220 let mut data = vec![0.0; n * m];
1221 for i in 0..n {
1222 for j in 0..m {
1223 data[i + j * n] = (i as f64 / n as f64) * argvals[j];
1224 }
1225 }
1226 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1227 let median = geometric_median_1d(&mat, &argvals, 100, 1e-6);
1228 assert_eq!(median.len(), m);
1229 assert!(median.iter().all(|&x| x.is_finite()));
1230 }
1231
1232 #[test]
1233 fn test_geometric_median_invalid() {
1234 assert!(geometric_median_1d(&FdMatrix::zeros(0, 0), &[], 100, 1e-6).is_empty());
1235 }
1236
1237 #[test]
1240 fn test_deriv_2d_linear_surface() {
1241 let m1 = 11;
1244 let m2 = 11;
1245 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1246 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1247
1248 let n = 1; let ncol = m1 * m2;
1250 let mut data = vec![0.0; n * ncol];
1251
1252 for si in 0..m1 {
1253 for ti in 0..m2 {
1254 let s = argvals_s[si];
1255 let t = argvals_t[ti];
1256 let idx = si + ti * m1;
1257 data[idx] = 2.0 * s + 3.0 * t;
1258 }
1259 }
1260
1261 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1262 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
1263
1264 for si in 2..(m1 - 2) {
1266 for ti in 2..(m2 - 2) {
1267 let idx = si + ti * m1;
1268 assert!(
1269 (result.ds[(0, idx)] - 2.0).abs() < 0.2,
1270 "∂f/∂s at ({}, {}) = {}, expected 2",
1271 si,
1272 ti,
1273 result.ds[(0, idx)]
1274 );
1275 }
1276 }
1277
1278 for si in 2..(m1 - 2) {
1280 for ti in 2..(m2 - 2) {
1281 let idx = si + ti * m1;
1282 assert!(
1283 (result.dt[(0, idx)] - 3.0).abs() < 0.2,
1284 "∂f/∂t at ({}, {}) = {}, expected 3",
1285 si,
1286 ti,
1287 result.dt[(0, idx)]
1288 );
1289 }
1290 }
1291
1292 for si in 2..(m1 - 2) {
1294 for ti in 2..(m2 - 2) {
1295 let idx = si + ti * m1;
1296 assert!(
1297 result.dsdt[(0, idx)].abs() < 0.5,
1298 "∂²f/∂s∂t at ({}, {}) = {}, expected 0",
1299 si,
1300 ti,
1301 result.dsdt[(0, idx)]
1302 );
1303 }
1304 }
1305 }
1306
1307 #[test]
1308 fn test_deriv_2d_quadratic_surface() {
1309 let m1 = 21;
1312 let m2 = 21;
1313 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1314 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1315
1316 let n = 1;
1317 let ncol = m1 * m2;
1318 let mut data = vec![0.0; n * ncol];
1319
1320 for si in 0..m1 {
1321 for ti in 0..m2 {
1322 let s = argvals_s[si];
1323 let t = argvals_t[ti];
1324 let idx = si + ti * m1;
1325 data[idx] = s * t;
1326 }
1327 }
1328
1329 let mat = FdMatrix::from_column_major(data, n, ncol).unwrap();
1330 let result = deriv_2d(&mat, &argvals_s, &argvals_t, m1, m2).unwrap();
1331
1332 for si in 3..(m1 - 3) {
1334 for ti in 3..(m2 - 3) {
1335 let idx = si + ti * m1;
1336 let expected = argvals_t[ti];
1337 assert!(
1338 (result.ds[(0, idx)] - expected).abs() < 0.1,
1339 "∂f/∂s at ({}, {}) = {}, expected {}",
1340 si,
1341 ti,
1342 result.ds[(0, idx)],
1343 expected
1344 );
1345 }
1346 }
1347
1348 for si in 3..(m1 - 3) {
1350 for ti in 3..(m2 - 3) {
1351 let idx = si + ti * m1;
1352 let expected = argvals_s[si];
1353 assert!(
1354 (result.dt[(0, idx)] - expected).abs() < 0.1,
1355 "∂f/∂t at ({}, {}) = {}, expected {}",
1356 si,
1357 ti,
1358 result.dt[(0, idx)],
1359 expected
1360 );
1361 }
1362 }
1363
1364 for si in 3..(m1 - 3) {
1366 for ti in 3..(m2 - 3) {
1367 let idx = si + ti * m1;
1368 assert!(
1369 (result.dsdt[(0, idx)] - 1.0).abs() < 0.3,
1370 "∂²f/∂s∂t at ({}, {}) = {}, expected 1",
1371 si,
1372 ti,
1373 result.dsdt[(0, idx)]
1374 );
1375 }
1376 }
1377 }
1378
1379 #[test]
1380 fn test_deriv_2d_invalid_input() {
1381 let result = deriv_2d(&FdMatrix::zeros(0, 0), &[], &[], 0, 0);
1383 assert!(result.is_none());
1384
1385 let mat = FdMatrix::from_column_major(vec![1.0; 4], 1, 4).unwrap();
1387 let argvals = vec![0.0, 1.0];
1388 let result = deriv_2d(&mat, &argvals, &[0.0, 0.5, 1.0], 2, 2);
1389 assert!(result.is_none());
1390 }
1391
1392 #[test]
1395 fn test_geometric_median_2d_basic() {
1396 let m1 = 5;
1398 let m2 = 5;
1399 let m = m1 * m2;
1400 let n = 3;
1401 let argvals_s: Vec<f64> = (0..m1).map(|i| i as f64 / (m1 - 1) as f64).collect();
1402 let argvals_t: Vec<f64> = (0..m2).map(|i| i as f64 / (m2 - 1) as f64).collect();
1403
1404 let mut data = vec![0.0; n * m];
1405
1406 for i in 0..n {
1408 for si in 0..m1 {
1409 for ti in 0..m2 {
1410 let idx = si + ti * m1;
1411 let s = argvals_s[si];
1412 let t = argvals_t[ti];
1413 data[i + idx * n] = s + t;
1414 }
1415 }
1416 }
1417
1418 let mat = FdMatrix::from_column_major(data, n, m).unwrap();
1419 let median = geometric_median_2d(&mat, &argvals_s, &argvals_t, 100, 1e-6);
1420 assert_eq!(median.len(), m);
1421
1422 for si in 0..m1 {
1424 for ti in 0..m2 {
1425 let idx = si + ti * m1;
1426 let expected = argvals_s[si] + argvals_t[ti];
1427 assert!(
1428 (median[idx] - expected).abs() < 0.01,
1429 "Median at ({}, {}) = {}, expected {}",
1430 si,
1431 ti,
1432 median[idx],
1433 expected
1434 );
1435 }
1436 }
1437 }
1438
1439 #[test]
1442 fn functional_variance_equals_std_squared() {
1443 let data = FdMatrix::from_column_major(
1445 vec![
1446 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1447 ],
1448 3,
1449 4,
1450 )
1451 .unwrap();
1452 let var = functional_variance(&data).unwrap();
1453 let std = functional_std(&data).unwrap();
1454 for j in 0..4 {
1455 assert!(
1456 (std[j].powi(2) - var[j]).abs() < 1e-10,
1457 "at j={j}: std^2={} != var={}",
1458 std[j].powi(2),
1459 var[j]
1460 );
1461 }
1462 }
1463
1464 #[test]
1465 fn functional_covariance_diagonal_matches_variance() {
1466 let data = FdMatrix::from_column_major(
1467 vec![
1468 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1469 ],
1470 3,
1471 4,
1472 )
1473 .unwrap();
1474 let var = functional_variance(&data).unwrap();
1475 let cov = functional_covariance(&data).unwrap();
1476 for j in 0..4 {
1477 assert!(
1478 (cov[(j, j)] - var[j]).abs() < 1e-10,
1479 "at j={j}: cov[j,j]={} != var={}",
1480 cov[(j, j)],
1481 var[j]
1482 );
1483 }
1484 }
1485
1486 #[test]
1487 fn functional_variance_hand_computed() {
1488 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 4.0, 2.0], 2, 2).unwrap();
1495 let var = functional_variance(&data).unwrap();
1496 assert!((var[0] - 2.0).abs() < 1e-10, "var[0]={}", var[0]);
1497 assert!((var[1] - 2.0).abs() < 1e-10, "var[1]={}", var[1]);
1498 }
1499
1500 #[test]
1503 fn depth_based_median_argmax() {
1504 let n = 5;
1508 let m = 10;
1509 let mut data_vec = vec![0.0f64; n * m];
1510 for j in 0..m {
1516 data_vec[j * n] = 0.0;
1517 data_vec[1 + j * n] = 0.25;
1518 data_vec[2 + j * n] = 0.5;
1519 data_vec[3 + j * n] = 0.75;
1520 data_vec[4 + j * n] = 1.0;
1521 }
1522 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1523 let idx = depth_based_median(&data).unwrap();
1524 assert_eq!(idx, 2, "most central curve should be at index 2, got {idx}");
1525 }
1526
1527 #[test]
1528 fn trim_mean_alpha_zero_equals_mean() {
1529 let n = 5;
1530 let m = 4;
1531 let mut data_vec = vec![0.0f64; n * m];
1532 for i in 0..n {
1533 for j in 0..m {
1534 data_vec[i + j * n] = (i + 1) as f64 * (j + 1) as f64;
1535 }
1536 }
1537 let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
1538 let tm = trim_mean(&data, 0.0).unwrap();
1539 let mu = mean_1d(&data);
1540 for j in 0..m {
1541 assert!(
1542 (tm[j] - mu[j]).abs() < 1e-10,
1543 "at j={j}: trim_mean(alpha=0)={} != mean={}",
1544 tm[j],
1545 mu[j]
1546 );
1547 }
1548 }
1549
1550 #[test]
1551 fn trim_mean_rejects_bad_alpha() {
1552 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
1553 let result = trim_mean(&data, 1.0);
1555 assert!(
1556 matches!(
1557 result,
1558 Err(FdarError::InvalidParameter {
1559 parameter: "alpha",
1560 ..
1561 })
1562 ),
1563 "expected InvalidParameter for alpha=1.0, got {result:?}"
1564 );
1565 let result2 = trim_mean(&data, -0.1);
1567 assert!(
1568 matches!(
1569 result2,
1570 Err(FdarError::InvalidParameter {
1571 parameter: "alpha",
1572 ..
1573 })
1574 ),
1575 "expected InvalidParameter for alpha=-0.1, got {result2:?}"
1576 );
1577 }
1578
1579 #[test]
1582 fn functional_stats_input_validation() {
1583 use crate::error::FdarError;
1584
1585 let one_row = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
1587 assert!(
1588 matches!(
1589 functional_variance(&one_row),
1590 Err(FdarError::InvalidDimension {
1591 parameter: "data",
1592 ..
1593 })
1594 ),
1595 "functional_variance should reject n=1"
1596 );
1597 assert!(
1598 matches!(
1599 functional_std(&one_row),
1600 Err(FdarError::InvalidDimension {
1601 parameter: "data",
1602 ..
1603 })
1604 ),
1605 "functional_std should reject n=1"
1606 );
1607 assert!(
1608 matches!(
1609 functional_covariance(&one_row),
1610 Err(FdarError::InvalidDimension {
1611 parameter: "data",
1612 ..
1613 })
1614 ),
1615 "functional_covariance should reject n=1"
1616 );
1617
1618 let zero_rows = FdMatrix::zeros(0, 3);
1620 assert!(
1621 matches!(
1622 depth_based_median(&zero_rows),
1623 Err(FdarError::InvalidDimension {
1624 parameter: "data",
1625 ..
1626 })
1627 ),
1628 "depth_based_median should reject n=0"
1629 );
1630 assert!(
1631 matches!(
1632 trim_mean(&zero_rows, 0.0),
1633 Err(FdarError::InvalidDimension {
1634 parameter: "data",
1635 ..
1636 })
1637 ),
1638 "trim_mean should reject n=0"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_nan_mean_no_panic() {
1644 let mut data_vec = vec![1.0; 6];
1645 data_vec[2] = f64::NAN;
1646 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1647 let m = mean_1d(&data);
1648 assert_eq!(m.len(), 3);
1649 }
1650
1651 #[test]
1652 fn test_nan_center_no_panic() {
1653 let mut data_vec = vec![1.0; 6];
1654 data_vec[2] = f64::NAN;
1655 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1656 let c = center_1d(&data);
1657 assert_eq!(c.nrows(), 2);
1658 }
1659
1660 #[test]
1661 fn test_nan_norm_no_panic() {
1662 let mut data_vec = vec![1.0; 6];
1663 data_vec[2] = f64::NAN;
1664 let data = FdMatrix::from_column_major(data_vec, 2, 3).unwrap();
1665 let argvals = vec![0.0, 0.5, 1.0];
1666 let norms = norm_lp_1d(&data, &argvals, 2.0);
1667 assert_eq!(norms.len(), 2);
1668 }
1669
1670 #[test]
1671 fn test_n1_norm() {
1672 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0], 1, 3).unwrap();
1673 let argvals = vec![0.0, 0.5, 1.0];
1674 let norms = norm_lp_1d(&data, &argvals, 2.0);
1675 assert_eq!(norms.len(), 1);
1676 assert!(norms[0] > 0.0);
1677 }
1678
1679 #[test]
1680 fn test_n2_center() {
1681 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0], 2, 2).unwrap();
1682 let centered = center_1d(&data);
1683 assert!((centered[(0, 0)] - (-1.0)).abs() < 1e-12);
1686 assert!((centered[(1, 0)] - 1.0).abs() < 1e-12);
1687 }
1688
1689 #[test]
1690 fn test_deriv_nderiv0() {
1691 let data = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).unwrap();
1693 let argvals = vec![0.0, 0.5, 1.0];
1694 let result = deriv_1d(&data, &argvals, 0);
1695 assert_eq!(result.shape(), data.shape());
1696 for i in 0..2 {
1697 for j in 0..3 {
1698 assert!((result[(i, j)] - data[(i, j)]).abs() < 1e-12);
1699 }
1700 }
1701 }
1702
1703 #[test]
1706 fn test_normalize_autoscale() {
1707 let data = FdMatrix::from_column_major(
1709 vec![
1710 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
1711 ],
1712 3,
1713 4,
1714 )
1715 .unwrap();
1716 let scaled = normalize(&data, NormalizationMethod::Autoscale);
1717 for j in 0..4 {
1719 let col = scaled.column(j);
1720 let mean = col.iter().sum::<f64>() / 3.0;
1721 assert!(
1722 mean.abs() < 1e-10,
1723 "column {j} mean should be 0, got {mean}"
1724 );
1725 let var = col.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1726 assert!(
1727 (var - 1.0).abs() < 1e-10,
1728 "column {j} variance should be 1, got {var}"
1729 );
1730 }
1731 }
1732
1733 #[test]
1734 fn test_normalize_pareto() {
1735 let data =
1736 FdMatrix::from_column_major(vec![1.0, 5.0, 3.0, 10.0, 20.0, 30.0], 2, 3).unwrap();
1737 let scaled = normalize(&data, NormalizationMethod::Pareto);
1738 for j in 0..3 {
1740 let col = scaled.column(j);
1741 let mean = col.iter().sum::<f64>() / 2.0;
1742 assert!(mean.abs() < 1e-10, "column {j} mean should be 0");
1743 }
1744 }
1745
1746 #[test]
1747 fn test_normalize_range() {
1748 let data = FdMatrix::from_column_major(vec![0.0, 10.0, 2.0, 8.0], 2, 2).unwrap();
1749 let scaled = normalize(&data, NormalizationMethod::Range);
1750 assert!((scaled[(0, 0)] - (-0.5)).abs() < 1e-10);
1752 assert!((scaled[(1, 0)] - 0.5).abs() < 1e-10);
1753 }
1754
1755 #[test]
1756 fn test_normalize_curve_center() {
1757 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1758 let result = normalize(&data, NormalizationMethod::CurveCenter);
1759 assert!((result[(0, 0)] - (-2.0)).abs() < 1e-10);
1761 assert!((result[(0, 1)] - 0.0).abs() < 1e-10);
1762 assert!((result[(0, 2)] - 2.0).abs() < 1e-10);
1763 }
1764
1765 #[test]
1766 fn test_normalize_curve_standardize() {
1767 let data = FdMatrix::from_column_major(vec![1.0, 4.0, 3.0, 6.0, 5.0, 8.0], 2, 3).unwrap();
1768 let result = normalize(&data, NormalizationMethod::CurveStandardize);
1769 for i in 0..2 {
1771 let row: Vec<f64> = (0..3).map(|j| result[(i, j)]).collect();
1772 let mean = row.iter().sum::<f64>() / 3.0;
1773 assert!(mean.abs() < 1e-10, "row {i} mean should be 0");
1774 let var = row.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1775 assert!((var - 1.0).abs() < 1e-10, "row {i} variance should be 1");
1776 }
1777 }
1778
1779 #[test]
1780 fn test_normalize_curve_range() {
1781 let data =
1782 FdMatrix::from_column_major(vec![2.0, 10.0, 4.0, 20.0, 6.0, 30.0], 2, 3).unwrap();
1783 let result = normalize(&data, NormalizationMethod::CurveRange);
1784 assert!((result[(0, 0)] - 0.0).abs() < 1e-10);
1786 assert!((result[(0, 1)] - 0.5).abs() < 1e-10);
1787 assert!((result[(0, 2)] - 1.0).abs() < 1e-10);
1788 }
1789
1790 #[test]
1791 fn test_normalize_center_matches_center_1d() {
1792 let data = FdMatrix::from_column_major(vec![1.0, 3.0, 2.0, 4.0, 3.0, 5.0], 2, 3).unwrap();
1793 let a = center_1d(&data);
1794 let b = normalize(&data, NormalizationMethod::Center);
1795 assert_eq!(a.as_slice(), b.as_slice());
1796 }
1797
1798 #[test]
1799 fn test_normalize_curve_lp_l2() {
1800 let data = FdMatrix::from_column_major(vec![3.0, 0.0, 0.0, 4.0, 0.0, 0.0], 2, 3).unwrap();
1802 let t = vec![0.0, 0.5, 1.0];
1803 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1804 let norms = norm_lp_1d(&result, &t, 2.0);
1807 assert!(
1808 (norms[0] - 1.0).abs() < 0.1,
1809 "L2 norm after normalization should be ≈ 1, got {}",
1810 norms[0]
1811 );
1812 }
1813
1814 #[test]
1815 fn test_normalize_curve_lp_l1() {
1816 let data = FdMatrix::from_column_major(vec![2.0, 4.0, 6.0, 8.0], 2, 2).unwrap();
1817 let t = vec![0.0, 1.0];
1818 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(1.0));
1819 let norms = norm_lp_1d(&result, &t, 1.0);
1821 for (i, &norm) in norms.iter().enumerate() {
1822 assert!(
1823 (norm - 1.0).abs() < 0.1,
1824 "curve {i} L1 norm after normalization should be ≈ 1, got {norm}"
1825 );
1826 }
1827 }
1828
1829 #[test]
1830 fn test_normalize_curve_lp_linf() {
1831 let data =
1832 FdMatrix::from_column_major(vec![2.0, -5.0, 4.0, -10.0, 6.0, 15.0], 2, 3).unwrap();
1833 let t = vec![0.0, 0.5, 1.0];
1834 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(f64::INFINITY));
1835 for i in 0..2 {
1837 let max_abs: f64 = (0..3).map(|j| result[(i, j)].abs()).fold(0.0, f64::max);
1838 assert!(
1839 (max_abs - 1.0).abs() < 1e-10,
1840 "curve {i} max abs after L-inf normalization should be 1, got {max_abs}"
1841 );
1842 }
1843 }
1844
1845 #[test]
1846 fn test_normalize_curve_lp_zero_curve() {
1847 let data = FdMatrix::from_column_major(vec![0.0, 1.0, 0.0, 2.0], 2, 2).unwrap();
1849 let t = vec![0.0, 1.0];
1850 let result = normalize_with_argvals(&data, &t, NormalizationMethod::CurveLp(2.0));
1851 assert!((result[(0, 0)]).abs() < 1e-15);
1853 assert!((result[(0, 1)]).abs() < 1e-15);
1854 assert!(result[(1, 0)].abs() > 0.0);
1856 }
1857}