Skip to main content

fdars_core/depth/
linf.rs

1//! L∞ (sup-norm) depth.
2//!
3//! L∞ depth inverts a curve's *average sup-norm distance* to the reference sample:
4//! curves that are close (in max-pointwise-deviation) to all others are deep, far
5//! ones are shallow.
6
7use crate::dim::Dim;
8use crate::error::FdarError;
9use crate::iter_maybe_parallel;
10use crate::matrix::FdMatrix;
11#[cfg(feature = "parallel")]
12use rayon::iter::ParallelIterator;
13
14/// Compute the L∞ (sup-norm) Depth for 1D functional data.
15///
16/// For each object curve `X_i`, the depth is the inverse of one plus its mean
17/// sup-norm distance to the reference curves:
18///
19/// ```text
20/// L∞_depth(X_i) = 1 / (1 + (1/N) · Σ_j max_t |X_i(t) − X_j(t)|)     ∈ (0, 1]
21/// ```
22///
23/// Note this is the average sup-norm distance to **all** reference curves, not the
24/// distance to the pointwise median. Depth is monotonically decreasing in that
25/// mean distance: the curve closest to the sample is deepest; a far magnitude
26/// outlier is shallow. [CITED: fdaoutlier::linfinity_depth]
27///
28/// # Errors
29///
30/// Returns [`FdarError::InvalidDimension`] if either matrix is empty or if the two
31/// grids differ. A single reference curve (`nori = 1`) is valid — the self-distance
32/// is 0, giving depth 1.0.
33#[must_use = "expensive computation whose result should not be discarded"]
34pub(crate) fn linfinity_depth_1d(
35    data_obj: &FdMatrix,
36    data_ori: &FdMatrix,
37) -> Result<Vec<f64>, FdarError> {
38    let (nobj, nori, m) = (data_obj.nrows(), data_ori.nrows(), data_obj.ncols());
39    if nobj == 0 || nori == 0 || m == 0 {
40        return Err(FdarError::InvalidDimension {
41            parameter: "data_obj",
42            expected: "non-empty matrices".to_string(),
43            actual: format!("{nobj}x{m}"),
44        });
45    }
46    if data_ori.ncols() != m {
47        return Err(FdarError::InvalidDimension {
48            parameter: "data_ori",
49            expected: format!("same number of columns as data_obj ({m})"),
50            actual: format!("{}", data_ori.ncols()),
51        });
52    }
53
54    let depths: Vec<f64> = iter_maybe_parallel!(0..nobj)
55        .map(|i| {
56            let mut total = 0.0_f64;
57            for j in 0..nori {
58                let mut sup = 0.0_f64;
59                for t in 0..m {
60                    let d = (data_obj[(i, t)] - data_ori[(j, t)]).abs();
61                    if d > sup {
62                        sup = d;
63                    }
64                }
65                total += sup;
66            }
67            let mean_dist = total / nori as f64;
68            1.0 / (1.0 + mean_dist)
69        })
70        .collect();
71
72    Ok(depths)
73}
74
75/// Compute L-infinity depth for 1D or 2D functional data via a unified [`Dim`] dispatch.
76///
77/// The 2D path never diverged from the 1D one, so both [`Dim`] arms forward to
78/// [`linfinity_depth_1d`]. The `dim` argument makes caller intent explicit.
79#[must_use = "expensive computation whose result should not be discarded"]
80pub fn linfinity_depth(
81    data_obj: &FdMatrix,
82    data_ori: &FdMatrix,
83    dim: Dim,
84) -> Result<Vec<f64>, FdarError> {
85    match dim {
86        Dim::One | Dim::Two => linfinity_depth_1d(data_obj, data_ori),
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::depth::dispatch::{functional_depth, DepthMethod};
94
95    fn sample(n: usize, m: usize) -> FdMatrix {
96        let mut col_major = vec![0.0; n * m];
97        for i in 0..n {
98            for t in 0..m {
99                let x = t as f64 / (m as f64 - 1.0);
100                col_major[i + t * n] = (x * std::f64::consts::PI).sin() + 0.05 * i as f64;
101            }
102        }
103        FdMatrix::from_column_major(col_major, n, m).unwrap()
104    }
105
106    fn sample_with_outlier(n: usize, m: usize, outlier_idx: usize) -> FdMatrix {
107        let mut col_major = vec![0.0; n * m];
108        for i in 0..n {
109            for t in 0..m {
110                let x = t as f64 / (m as f64 - 1.0);
111                let base = (x * std::f64::consts::PI).sin();
112                let val = if i == outlier_idx {
113                    base + 100.0
114                } else {
115                    base + 0.01 * i as f64
116                };
117                col_major[i + t * n] = val;
118            }
119        }
120        FdMatrix::from_column_major(col_major, n, m).unwrap()
121    }
122
123    #[test]
124    fn depths_in_unit_interval() {
125        let data = sample(8, 20);
126        let ld = linfinity_depth_1d(&data, &data).unwrap();
127        assert_eq!(ld.len(), 8);
128        for &v in &ld {
129            assert!(v > 0.0 && v <= 1.0 + 1e-12, "L∞ depth out of range: {v}");
130        }
131    }
132
133    #[test]
134    fn closest_to_sample_is_deepest_and_outlier_shallow() {
135        let outlier_idx = 3usize;
136        let data = sample_with_outlier(8, 20, outlier_idx);
137        let ld = linfinity_depth_1d(&data, &data).unwrap();
138        // The far magnitude outlier has the largest mean sup-norm distance → shallowest.
139        let mut shallowest = 0usize;
140        let mut deepest = 0usize;
141        for i in 1..ld.len() {
142            if ld[i] < ld[shallowest] {
143                shallowest = i;
144            }
145            if ld[i] > ld[deepest] {
146                deepest = i;
147            }
148        }
149        assert_eq!(
150            shallowest, outlier_idx,
151            "outlier should be shallowest by L∞ depth"
152        );
153        assert_ne!(deepest, outlier_idx);
154        assert!(ld[deepest] > ld[outlier_idx]);
155    }
156
157    #[test]
158    fn monotone_decreasing_in_mean_distance() {
159        // On the stacked no-outlier sample the central curve is closest to all others.
160        let n = 9usize;
161        let data = sample(n, 25);
162        let ld = linfinity_depth_1d(&data, &data).unwrap();
163        let mut deepest = 0usize;
164        for i in 1..n {
165            if ld[i] > ld[deepest] {
166                deepest = i;
167            }
168        }
169        assert_eq!(
170            deepest,
171            n / 2,
172            "central curve should be deepest by L∞ depth"
173        );
174    }
175
176    #[test]
177    fn single_curve_self_depth_is_one() {
178        let one = sample(1, 8);
179        let ld = linfinity_depth_1d(&one, &one).unwrap();
180        assert_eq!(ld.len(), 1);
181        assert!(
182            (ld[0] - 1.0).abs() < 1e-12,
183            "n=1 self-depth should be 1.0, got {}",
184            ld[0]
185        );
186    }
187
188    #[test]
189    fn dispatch_round_trip_and_empty_err() {
190        let data = sample(6, 12);
191        let got = functional_depth(&data, DepthMethod::LInfinity).unwrap();
192        assert_eq!(got, linfinity_depth_1d(&data, &data).unwrap());
193        let empty = FdMatrix::from_column_major(vec![], 0, 0).unwrap();
194        assert!(linfinity_depth_1d(&empty, &empty).is_err());
195    }
196}