Skip to main content

fdars_core/
depth.rs

1//! Depth measures for functional data.
2//!
3//! This module provides various depth measures for assessing the centrality
4//! of functional observations within a reference sample.
5
6use crate::helpers::simpsons_weights;
7use crate::iter_maybe_parallel;
8use crate::matrix::FdMatrix;
9use rand::prelude::*;
10use rand_distr::StandardNormal;
11#[cfg(feature = "parallel")]
12use rayon::iter::ParallelIterator;
13
14/// Compute Fraiman-Muniz depth for 1D functional data.
15///
16/// Uses the FM1 formula: d = 1 - |0.5 - Fn(x)|
17/// With scale=true: d = 2 * min(Fn(x), 1-Fn(x))
18///
19/// # Arguments
20/// * `data_obj` - Data to compute depth for (nobj x n_points)
21/// * `data_ori` - Reference data (nori x n_points)
22/// * `scale` - Whether to scale the depth values
23pub fn fraiman_muniz_1d(data_obj: &FdMatrix, data_ori: &FdMatrix, scale: bool) -> Vec<f64> {
24    let nobj = data_obj.nrows();
25    let nori = data_ori.nrows();
26    let n_points = data_obj.ncols();
27
28    if nobj == 0 || nori == 0 || n_points == 0 {
29        return Vec::new();
30    }
31
32    let scale_factor = if scale { 2.0 } else { 1.0 };
33
34    iter_maybe_parallel!(0..nobj)
35        .map(|i| {
36            let mut depth_sum = 0.0;
37
38            for t in 0..n_points {
39                let x_t = data_obj[(i, t)];
40                let mut le_count = 0;
41
42                for j in 0..nori {
43                    let y_t = data_ori[(j, t)];
44                    if y_t <= x_t {
45                        le_count += 1;
46                    }
47                }
48
49                let fn_x = le_count as f64 / nori as f64;
50                let univariate_depth = fn_x.min(1.0 - fn_x) * scale_factor;
51                depth_sum += univariate_depth;
52            }
53
54            depth_sum / n_points as f64
55        })
56        .collect()
57}
58
59/// Compute Fraiman-Muniz depth for 2D functional data (surfaces).
60pub fn fraiman_muniz_2d(data_obj: &FdMatrix, data_ori: &FdMatrix, scale: bool) -> Vec<f64> {
61    // Same implementation as 1D - iterate over all grid points
62    fraiman_muniz_1d(data_obj, data_ori, scale)
63}
64
65/// Compute modal depth for 1D functional data.
66///
67/// Uses a Gaussian kernel to measure density around each curve.
68///
69/// # Arguments
70/// * `data_obj` - Data to compute depth for
71/// * `data_ori` - Reference data
72/// * `h` - Bandwidth parameter
73pub fn modal_1d(data_obj: &FdMatrix, data_ori: &FdMatrix, h: f64) -> Vec<f64> {
74    let nobj = data_obj.nrows();
75    let nori = data_ori.nrows();
76    let n_points = data_obj.ncols();
77
78    if nobj == 0 || nori == 0 || n_points == 0 {
79        return Vec::new();
80    }
81
82    iter_maybe_parallel!(0..nobj)
83        .map(|i| {
84            let mut depth = 0.0;
85
86            for j in 0..nori {
87                let mut dist_sq = 0.0;
88                for t in 0..n_points {
89                    let diff = data_obj[(i, t)] - data_ori[(j, t)];
90                    dist_sq += diff * diff;
91                }
92                let dist = (dist_sq / n_points as f64).sqrt();
93                let kernel_val = (-0.5 * (dist / h).powi(2)).exp();
94                depth += kernel_val;
95            }
96
97            depth / nori as f64
98        })
99        .collect()
100}
101
102/// Compute modal depth for 2D functional data.
103pub fn modal_2d(data_obj: &FdMatrix, data_ori: &FdMatrix, h: f64) -> Vec<f64> {
104    modal_1d(data_obj, data_ori, h)
105}
106
107/// Compute random projection depth for 1D functional data.
108///
109/// Projects curves to scalars using random projections and computes
110/// average univariate depth.
111///
112/// # Arguments
113/// * `data_obj` - Data to compute depth for
114/// * `data_ori` - Reference data
115/// * `nproj` - Number of random projections
116pub fn random_projection_1d(data_obj: &FdMatrix, data_ori: &FdMatrix, nproj: usize) -> Vec<f64> {
117    let nobj = data_obj.nrows();
118    let nori = data_ori.nrows();
119    let n_points = data_obj.ncols();
120
121    if nobj == 0 || nori == 0 || n_points == 0 || nproj == 0 {
122        return Vec::new();
123    }
124
125    let mut rng = rand::thread_rng();
126    let projections: Vec<Vec<f64>> = (0..nproj)
127        .map(|_| {
128            let mut proj: Vec<f64> = (0..n_points).map(|_| rng.sample(StandardNormal)).collect();
129            let norm: f64 = proj.iter().map(|x| x * x).sum::<f64>().sqrt();
130            proj.iter_mut().for_each(|x| *x /= norm);
131            proj
132        })
133        .collect();
134
135    iter_maybe_parallel!(0..nobj)
136        .map(|i| {
137            let mut total_depth = 0.0;
138
139            for proj in &projections {
140                let mut proj_i = 0.0;
141                for t in 0..n_points {
142                    proj_i += data_obj[(i, t)] * proj[t];
143                }
144
145                let mut proj_ori: Vec<f64> = (0..nori)
146                    .map(|j| {
147                        let mut p = 0.0;
148                        for t in 0..n_points {
149                            p += data_ori[(j, t)] * proj[t];
150                        }
151                        p
152                    })
153                    .collect();
154
155                proj_ori.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
156
157                let below = proj_ori.iter().filter(|&&x| x < proj_i).count();
158                let above = proj_ori.iter().filter(|&&x| x > proj_i).count();
159                let depth = (below.min(above) as f64 + 1.0) / (nori as f64 + 1.0);
160
161                total_depth += depth;
162            }
163
164            total_depth / nproj as f64
165        })
166        .collect()
167}
168
169/// Compute random projection depth for 2D functional data.
170pub fn random_projection_2d(data_obj: &FdMatrix, data_ori: &FdMatrix, nproj: usize) -> Vec<f64> {
171    random_projection_1d(data_obj, data_ori, nproj)
172}
173
174/// Compute random Tukey depth for 1D functional data.
175///
176/// Takes the minimum over all random projections (more conservative than RP depth).
177pub fn random_tukey_1d(data_obj: &FdMatrix, data_ori: &FdMatrix, nproj: usize) -> Vec<f64> {
178    let nobj = data_obj.nrows();
179    let nori = data_ori.nrows();
180    let n_points = data_obj.ncols();
181
182    if nobj == 0 || nori == 0 || n_points == 0 || nproj == 0 {
183        return Vec::new();
184    }
185
186    let mut rng = rand::thread_rng();
187    let projections: Vec<Vec<f64>> = (0..nproj)
188        .map(|_| {
189            let mut proj: Vec<f64> = (0..n_points).map(|_| rng.sample(StandardNormal)).collect();
190            let norm: f64 = proj.iter().map(|x| x * x).sum::<f64>().sqrt();
191            proj.iter_mut().for_each(|x| *x /= norm);
192            proj
193        })
194        .collect();
195
196    iter_maybe_parallel!(0..nobj)
197        .map(|i| {
198            let mut min_depth = f64::INFINITY;
199
200            for proj in &projections {
201                let mut proj_i = 0.0;
202                for t in 0..n_points {
203                    proj_i += data_obj[(i, t)] * proj[t];
204                }
205
206                let proj_ori: Vec<f64> = (0..nori)
207                    .map(|j| {
208                        let mut p = 0.0;
209                        for t in 0..n_points {
210                            p += data_ori[(j, t)] * proj[t];
211                        }
212                        p
213                    })
214                    .collect();
215
216                let below = proj_ori.iter().filter(|&&x| x < proj_i).count();
217                let above = proj_ori.iter().filter(|&&x| x > proj_i).count();
218                let depth = (below.min(above) as f64 + 1.0) / (nori as f64 + 1.0);
219
220                min_depth = min_depth.min(depth);
221            }
222
223            min_depth
224        })
225        .collect()
226}
227
228/// Compute random Tukey depth for 2D functional data.
229pub fn random_tukey_2d(data_obj: &FdMatrix, data_ori: &FdMatrix, nproj: usize) -> Vec<f64> {
230    random_tukey_1d(data_obj, data_ori, nproj)
231}
232
233/// Compute Functional Spatial Depth for 1D functional data.
234pub fn functional_spatial_1d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Vec<f64> {
235    let nobj = data_obj.nrows();
236    let nori = data_ori.nrows();
237    let n_points = data_obj.ncols();
238
239    if nobj == 0 || nori == 0 || n_points == 0 {
240        return Vec::new();
241    }
242
243    iter_maybe_parallel!(0..nobj)
244        .map(|i| {
245            let mut sum_unit = vec![0.0; n_points];
246
247            for j in 0..nori {
248                let mut direction = vec![0.0; n_points];
249                let mut norm_sq = 0.0;
250
251                for t in 0..n_points {
252                    direction[t] = data_ori[(j, t)] - data_obj[(i, t)];
253                    norm_sq += direction[t] * direction[t];
254                }
255
256                let norm = norm_sq.sqrt();
257                if norm > 1e-10 {
258                    for t in 0..n_points {
259                        sum_unit[t] += direction[t] / norm;
260                    }
261                }
262            }
263
264            let mut avg_norm_sq = 0.0;
265            for t in 0..n_points {
266                let avg = sum_unit[t] / nori as f64;
267                avg_norm_sq += avg * avg;
268            }
269
270            1.0 - avg_norm_sq.sqrt()
271        })
272        .collect()
273}
274
275/// Compute Functional Spatial Depth for 2D functional data.
276pub fn functional_spatial_2d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Vec<f64> {
277    functional_spatial_1d(data_obj, data_ori)
278}
279
280/// Compute kernel distance contribution for a single (j,k) pair.
281fn kernel_pair_contribution(j: usize, k: usize, m1: &[Vec<f64>], m2: &[f64]) -> Option<f64> {
282    let denom_j_sq = 2.0 - 2.0 * m2[j];
283    if denom_j_sq < 1e-20 {
284        return None;
285    }
286    let denom_k_sq = 2.0 - 2.0 * m2[k];
287    if denom_k_sq < 1e-20 {
288        return None;
289    }
290    let denom = denom_j_sq.sqrt() * denom_k_sq.sqrt();
291    if denom <= 1e-20 {
292        return None;
293    }
294    let m_ijk = (1.0 + m1[j][k] - m2[j] - m2[k]) / denom;
295    if m_ijk.is_finite() {
296        Some(m_ijk)
297    } else {
298        None
299    }
300}
301
302/// Accumulate the kernel spatial depth statistic for a single observation.
303/// Returns (total_sum, valid_count) from the double sum over reference pairs.
304fn kfsd_accumulate(m2: &[f64], m1: &[Vec<f64>], nori: usize) -> (f64, usize) {
305    let mut total_sum = 0.0;
306    let mut valid_count = 0;
307
308    for j in 0..nori {
309        for k in 0..nori {
310            if let Some(val) = kernel_pair_contribution(j, k, m1, m2) {
311                total_sum += val;
312                valid_count += 1;
313            }
314        }
315    }
316
317    (total_sum, valid_count)
318}
319
320/// Shared implementation for kernel functional spatial depth.
321/// Uses weighted L2 norm: sum_t weights[t] * (f(t) - g(t))^2.
322fn kfsd_weighted(data_obj: &FdMatrix, data_ori: &FdMatrix, h: f64, weights: &[f64]) -> Vec<f64> {
323    let nobj = data_obj.nrows();
324    let nori = data_ori.nrows();
325    let n_points = data_obj.ncols();
326    let h_sq = h * h;
327
328    // Pre-compute M1[j,k] = K(X_j, X_k) for reference data
329    let m1_upper: Vec<(usize, usize, f64)> = iter_maybe_parallel!(0..nori)
330        .flat_map(|j| {
331            ((j + 1)..nori)
332                .map(|k| {
333                    let mut sum = 0.0;
334                    for t in 0..n_points {
335                        let diff = data_ori[(j, t)] - data_ori[(k, t)];
336                        sum += weights[t] * diff * diff;
337                    }
338                    (j, k, (-sum / h_sq).exp())
339                })
340                .collect::<Vec<_>>()
341        })
342        .collect();
343
344    let mut m1 = vec![vec![0.0; nori]; nori];
345    for j in 0..nori {
346        m1[j][j] = 1.0;
347    }
348    for (j, k, kval) in m1_upper {
349        m1[j][k] = kval;
350        m1[k][j] = kval;
351    }
352
353    let nori_f64 = nori as f64;
354
355    iter_maybe_parallel!(0..nobj)
356        .map(|i| {
357            let m2: Vec<f64> = (0..nori)
358                .map(|j| {
359                    let mut sum = 0.0;
360                    for t in 0..n_points {
361                        let diff = data_obj[(i, t)] - data_ori[(j, t)];
362                        sum += weights[t] * diff * diff;
363                    }
364                    (-sum / h_sq).exp()
365                })
366                .collect();
367
368            let (total_sum, valid_count) = kfsd_accumulate(&m2, &m1, nori);
369
370            if valid_count > 0 && total_sum >= 0.0 {
371                1.0 - total_sum.sqrt() / nori_f64
372            } else if total_sum < 0.0 {
373                1.0
374            } else {
375                0.0
376            }
377        })
378        .collect()
379}
380
381/// Check if curve i is entirely inside the band formed by reference curves j and k.
382fn curve_inside_band(
383    data_obj: &FdMatrix,
384    data_ori: &FdMatrix,
385    i: usize,
386    j: usize,
387    k: usize,
388) -> bool {
389    let n_points = data_obj.ncols();
390    for t in 0..n_points {
391        let x_t = data_obj[(i, t)];
392        let y_j_t = data_ori[(j, t)];
393        let y_k_t = data_ori[(k, t)];
394        if x_t < y_j_t.min(y_k_t) || x_t > y_j_t.max(y_k_t) {
395            return false;
396        }
397    }
398    true
399}
400
401/// Proportion of time points where curve i is inside the band formed by curves j and k.
402fn proportion_inside_band(
403    data_obj: &FdMatrix,
404    data_ori: &FdMatrix,
405    i: usize,
406    j: usize,
407    k: usize,
408) -> f64 {
409    let n_points = data_obj.ncols();
410    let mut count = 0usize;
411    for t in 0..n_points {
412        let x_t = data_obj[(i, t)];
413        let y_j_t = data_ori[(j, t)];
414        let y_k_t = data_ori[(k, t)];
415        if x_t >= y_j_t.min(y_k_t) && x_t <= y_j_t.max(y_k_t) {
416            count += 1;
417        }
418    }
419    count as f64 / n_points as f64
420}
421
422/// Compute Kernel Functional Spatial Depth (KFSD) for 1D functional data.
423///
424/// Implements the RKHS-based formulation.
425pub fn kernel_functional_spatial_1d(
426    data_obj: &FdMatrix,
427    data_ori: &FdMatrix,
428    argvals: &[f64],
429    h: f64,
430) -> Vec<f64> {
431    let nobj = data_obj.nrows();
432    let nori = data_ori.nrows();
433    let n_points = data_obj.ncols();
434
435    if nobj == 0 || nori == 0 || n_points == 0 {
436        return Vec::new();
437    }
438
439    let weights = simpsons_weights(argvals);
440    kfsd_weighted(data_obj, data_ori, h, &weights)
441}
442
443/// Compute Kernel Functional Spatial Depth (KFSD) for 2D functional data.
444pub fn kernel_functional_spatial_2d(data_obj: &FdMatrix, data_ori: &FdMatrix, h: f64) -> Vec<f64> {
445    let nobj = data_obj.nrows();
446    let nori = data_ori.nrows();
447    let n_points = data_obj.ncols();
448
449    if nobj == 0 || nori == 0 || n_points == 0 {
450        return Vec::new();
451    }
452
453    let weights = vec![1.0; n_points];
454    kfsd_weighted(data_obj, data_ori, h, &weights)
455}
456
457/// Compute Band Depth (BD) for 1D functional data.
458///
459/// BD(x) = proportion of pairs (i,j) where x lies within the band formed by curves i and j.
460pub fn band_1d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Vec<f64> {
461    let nobj = data_obj.nrows();
462    let nori = data_ori.nrows();
463    let n_points = data_obj.ncols();
464
465    if nobj == 0 || nori < 2 || n_points == 0 {
466        return Vec::new();
467    }
468
469    let n_pairs = (nori * (nori - 1)) / 2;
470
471    iter_maybe_parallel!(0..nobj)
472        .map(|i| {
473            let mut count_in_band = 0usize;
474
475            for j in 0..nori {
476                for k in (j + 1)..nori {
477                    if curve_inside_band(data_obj, data_ori, i, j, k) {
478                        count_in_band += 1;
479                    }
480                }
481            }
482
483            count_in_band as f64 / n_pairs as f64
484        })
485        .collect()
486}
487
488/// Compute Modified Band Depth (MBD) for 1D functional data.
489///
490/// MBD(x) = average over pairs of the proportion of the domain where x is inside the band.
491pub fn modified_band_1d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Vec<f64> {
492    let nobj = data_obj.nrows();
493    let nori = data_ori.nrows();
494    let n_points = data_obj.ncols();
495
496    if nobj == 0 || nori < 2 || n_points == 0 {
497        return Vec::new();
498    }
499
500    let n_pairs = (nori * (nori - 1)) / 2;
501
502    iter_maybe_parallel!(0..nobj)
503        .map(|i| {
504            let mut total_proportion = 0.0;
505
506            for j in 0..nori {
507                for k in (j + 1)..nori {
508                    total_proportion += proportion_inside_band(data_obj, data_ori, i, j, k);
509                }
510            }
511
512            total_proportion / n_pairs as f64
513        })
514        .collect()
515}
516
517/// Compute Modified Epigraph Index (MEI) for 1D functional data.
518///
519/// MEI measures the proportion of time a curve is below other curves.
520pub fn modified_epigraph_index_1d(data_obj: &FdMatrix, data_ori: &FdMatrix) -> Vec<f64> {
521    let nobj = data_obj.nrows();
522    let nori = data_ori.nrows();
523    let n_points = data_obj.ncols();
524
525    if nobj == 0 || nori == 0 || n_points == 0 {
526        return Vec::new();
527    }
528
529    iter_maybe_parallel!(0..nobj)
530        .map(|i| {
531            let mut total = 0.0;
532
533            for j in 0..nori {
534                let mut count = 0.0;
535
536                for t in 0..n_points {
537                    let xi = data_obj[(i, t)];
538                    let xj = data_ori[(j, t)];
539
540                    if xi < xj {
541                        count += 1.0;
542                    } else if (xi - xj).abs() < 1e-12 {
543                        count += 0.5;
544                    }
545                }
546
547                total += count / n_points as f64;
548            }
549
550            total / nori as f64
551        })
552        .collect()
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use std::f64::consts::PI;
559
560    fn uniform_grid(n: usize) -> Vec<f64> {
561        (0..n).map(|i| i as f64 / (n - 1) as f64).collect()
562    }
563
564    fn generate_centered_data(n: usize, m: usize) -> FdMatrix {
565        let argvals = uniform_grid(m);
566        let mut data = vec![0.0; n * m];
567        for i in 0..n {
568            let offset = (i as f64 - n as f64 / 2.0) / (n as f64);
569            for j in 0..m {
570                data[i + j * n] = (2.0 * PI * argvals[j]).sin() + offset;
571            }
572        }
573        FdMatrix::from_column_major(data, n, m).unwrap()
574    }
575
576    // ============== Fraiman-Muniz tests ==============
577
578    #[test]
579    fn test_fraiman_muniz() {
580        // Simple test: identical data should give maximum depth
581        let data = FdMatrix::from_column_major(vec![1.0, 1.0, 2.0, 2.0], 2, 2).unwrap(); // 2 identical curves, 2 points each
582        let depths = fraiman_muniz_1d(&data, &data, true);
583        assert_eq!(depths.len(), 2);
584    }
585
586    #[test]
587    fn test_fraiman_muniz_central_deeper() {
588        let n = 20;
589        let m = 30;
590        let data = generate_centered_data(n, m);
591        let depths = fraiman_muniz_1d(&data, &data, true);
592
593        // Central curve (index n/2) should have higher depth than extreme curves
594        let central_depth = depths[n / 2];
595        let edge_depth = depths[0];
596        assert!(
597            central_depth > edge_depth,
598            "Central curve should be deeper: {} > {}",
599            central_depth,
600            edge_depth
601        );
602    }
603
604    #[test]
605    fn test_fraiman_muniz_range() {
606        let n = 15;
607        let m = 20;
608        let data = generate_centered_data(n, m);
609        let depths = fraiman_muniz_1d(&data, &data, true);
610
611        for d in &depths {
612            assert!(*d >= 0.0 && *d <= 1.0, "Depth should be in [0, 1]");
613        }
614    }
615
616    #[test]
617    fn test_fraiman_muniz_invalid() {
618        let empty = FdMatrix::zeros(0, 0);
619        assert!(fraiman_muniz_1d(&empty, &empty, true).is_empty());
620    }
621
622    // ============== Modal depth tests ==============
623
624    #[test]
625    fn test_modal_central_deeper() {
626        let n = 20;
627        let m = 30;
628        let data = generate_centered_data(n, m);
629        let depths = modal_1d(&data, &data, 0.5);
630
631        let central_depth = depths[n / 2];
632        let edge_depth = depths[0];
633        assert!(central_depth > edge_depth, "Central curve should be deeper");
634    }
635
636    #[test]
637    fn test_modal_positive() {
638        let n = 10;
639        let m = 20;
640        let data = generate_centered_data(n, m);
641        let depths = modal_1d(&data, &data, 0.5);
642
643        for d in &depths {
644            assert!(*d > 0.0, "Modal depth should be positive");
645        }
646    }
647
648    #[test]
649    fn test_modal_invalid() {
650        let empty = FdMatrix::zeros(0, 0);
651        assert!(modal_1d(&empty, &empty, 0.5).is_empty());
652    }
653
654    // ============== Random projection depth tests ==============
655
656    #[test]
657    fn test_rp_depth_range() {
658        let n = 15;
659        let m = 20;
660        let data = generate_centered_data(n, m);
661        let depths = random_projection_1d(&data, &data, 50);
662
663        for d in &depths {
664            assert!(*d >= 0.0 && *d <= 1.0, "RP depth should be in [0, 1]");
665        }
666    }
667
668    #[test]
669    fn test_rp_depth_invalid() {
670        let empty = FdMatrix::zeros(0, 0);
671        assert!(random_projection_1d(&empty, &empty, 10).is_empty());
672    }
673
674    // ============== Random Tukey depth tests ==============
675
676    #[test]
677    fn test_random_tukey_range() {
678        let n = 15;
679        let m = 20;
680        let data = generate_centered_data(n, m);
681        let depths = random_tukey_1d(&data, &data, 50);
682
683        for d in &depths {
684            assert!(*d >= 0.0 && *d <= 1.0, "Tukey depth should be in [0, 1]");
685        }
686    }
687
688    #[test]
689    fn test_random_tukey_invalid() {
690        let empty = FdMatrix::zeros(0, 0);
691        assert!(random_tukey_1d(&empty, &empty, 10).is_empty());
692    }
693
694    // ============== Functional spatial depth tests ==============
695
696    #[test]
697    fn test_functional_spatial_range() {
698        let n = 15;
699        let m = 20;
700        let data = generate_centered_data(n, m);
701        let depths = functional_spatial_1d(&data, &data);
702
703        for d in &depths {
704            assert!(*d >= 0.0 && *d <= 1.0, "Spatial depth should be in [0, 1]");
705        }
706    }
707
708    #[test]
709    fn test_functional_spatial_invalid() {
710        let empty = FdMatrix::zeros(0, 0);
711        assert!(functional_spatial_1d(&empty, &empty).is_empty());
712    }
713
714    // ============== Band depth tests ==============
715
716    #[test]
717    fn test_band_depth_central_deeper() {
718        let n = 10;
719        let m = 20;
720        let data = generate_centered_data(n, m);
721        let depths = band_1d(&data, &data);
722
723        // Central curve should be in more bands
724        let central_depth = depths[n / 2];
725        let edge_depth = depths[0];
726        assert!(
727            central_depth >= edge_depth,
728            "Central curve should have higher band depth"
729        );
730    }
731
732    #[test]
733    fn test_band_depth_range() {
734        let n = 10;
735        let m = 20;
736        let data = generate_centered_data(n, m);
737        let depths = band_1d(&data, &data);
738
739        for d in &depths {
740            assert!(*d >= 0.0 && *d <= 1.0, "Band depth should be in [0, 1]");
741        }
742    }
743
744    #[test]
745    fn test_band_depth_invalid() {
746        let empty = FdMatrix::zeros(0, 0);
747        assert!(band_1d(&empty, &empty).is_empty());
748        let single = FdMatrix::from_column_major(vec![1.0, 2.0], 1, 2).unwrap();
749        assert!(band_1d(&single, &single).is_empty()); // need at least 2 ref curves
750    }
751
752    // ============== Modified band depth tests ==============
753
754    #[test]
755    fn test_modified_band_depth_range() {
756        let n = 10;
757        let m = 20;
758        let data = generate_centered_data(n, m);
759        let depths = modified_band_1d(&data, &data);
760
761        for d in &depths {
762            assert!(*d >= 0.0 && *d <= 1.0, "MBD should be in [0, 1]");
763        }
764    }
765
766    #[test]
767    fn test_modified_band_depth_invalid() {
768        let empty = FdMatrix::zeros(0, 0);
769        assert!(modified_band_1d(&empty, &empty).is_empty());
770    }
771
772    // ============== Modified epigraph index tests ==============
773
774    #[test]
775    fn test_mei_range() {
776        let n = 15;
777        let m = 20;
778        let data = generate_centered_data(n, m);
779        let mei = modified_epigraph_index_1d(&data, &data);
780
781        for d in &mei {
782            assert!(*d >= 0.0 && *d <= 1.0, "MEI should be in [0, 1]");
783        }
784    }
785
786    #[test]
787    fn test_mei_invalid() {
788        let empty = FdMatrix::zeros(0, 0);
789        assert!(modified_epigraph_index_1d(&empty, &empty).is_empty());
790    }
791
792    // ============== KFSD 1D tests ==============
793
794    #[test]
795    fn test_kfsd_1d_range() {
796        let n = 10;
797        let m = 20;
798        let argvals = uniform_grid(m);
799        let data = generate_centered_data(n, m);
800        let depths = kernel_functional_spatial_1d(&data, &data, &argvals, 0.5);
801
802        assert_eq!(depths.len(), n);
803        for d in &depths {
804            assert!(
805                *d >= -0.01 && *d <= 1.01,
806                "KFSD depth should be near [0, 1], got {}",
807                d
808            );
809            assert!(d.is_finite(), "KFSD depth should be finite");
810        }
811
812        // Central curve should have higher depth
813        let central_depth = depths[n / 2];
814        let edge_depth = depths[0];
815        assert!(
816            central_depth > edge_depth,
817            "Central KFSD depth {} should be > edge depth {}",
818            central_depth,
819            edge_depth
820        );
821    }
822
823    #[test]
824    fn test_kfsd_1d_identical() {
825        // All identical curves should exercise the denom_j_sq < 1e-20 path
826        let n = 5;
827        let m = 10;
828        let argvals = uniform_grid(m);
829        let data_vec: Vec<f64> = (0..n * m)
830            .map(|i| (2.0 * PI * (i % m) as f64 / (m - 1) as f64).sin())
831            .collect();
832        let data = FdMatrix::from_column_major(data_vec, n, m).unwrap();
833
834        // When all curves are identical, kernel distances are all 1.0
835        // and denom_j_sq = K(x,x) + K(y,y) - 2*K(x,y) = 1 + 1 - 2*1 = 0
836        let depths = kernel_functional_spatial_1d(&data, &data, &argvals, 0.5);
837
838        assert_eq!(depths.len(), n);
839        for d in &depths {
840            assert!(
841                d.is_finite(),
842                "KFSD depth should be finite for identical curves"
843            );
844        }
845    }
846
847    #[test]
848    fn test_kfsd_1d_invalid() {
849        let argvals = uniform_grid(10);
850        let empty = FdMatrix::zeros(0, 0);
851        assert!(kernel_functional_spatial_1d(&empty, &empty, &argvals, 0.5).is_empty());
852        let empty_obj = FdMatrix::zeros(0, 0);
853        assert!(kernel_functional_spatial_1d(&empty_obj, &empty_obj, &argvals, 0.5).is_empty());
854    }
855
856    // ============== KFSD 2D tests ==============
857
858    #[test]
859    fn test_kfsd_2d_range() {
860        let n = 8;
861        let m = 15;
862        let data = generate_centered_data(n, m);
863        let depths = kernel_functional_spatial_2d(&data, &data, 0.5);
864
865        assert_eq!(depths.len(), n);
866        for d in &depths {
867            assert!(
868                *d >= -0.01 && *d <= 1.01,
869                "KFSD 2D depth should be near [0, 1], got {}",
870                d
871            );
872            assert!(d.is_finite(), "KFSD 2D depth should be finite");
873        }
874    }
875
876    // ============== 2D delegation tests ==============
877
878    #[test]
879    fn test_fraiman_muniz_2d_delegates() {
880        let n = 10;
881        let m = 15;
882        let data = generate_centered_data(n, m);
883        let depths_1d = fraiman_muniz_1d(&data, &data, true);
884        let depths_2d = fraiman_muniz_2d(&data, &data, true);
885        assert_eq!(depths_1d, depths_2d);
886    }
887
888    #[test]
889    fn test_modal_2d_delegates() {
890        let n = 10;
891        let m = 15;
892        let data = generate_centered_data(n, m);
893        let depths_1d = modal_1d(&data, &data, 0.5);
894        let depths_2d = modal_2d(&data, &data, 0.5);
895        assert_eq!(depths_1d, depths_2d);
896    }
897
898    #[test]
899    fn test_functional_spatial_2d_delegates() {
900        let n = 10;
901        let m = 15;
902        let data = generate_centered_data(n, m);
903        let depths_1d = functional_spatial_1d(&data, &data);
904        let depths_2d = functional_spatial_2d(&data, &data);
905        assert_eq!(depths_1d, depths_2d);
906    }
907
908    #[test]
909    fn test_random_projection_2d_returns_valid() {
910        let n = 10;
911        let m = 15;
912        let data = generate_centered_data(n, m);
913        let depths = random_projection_2d(&data, &data, 20);
914        assert_eq!(depths.len(), n);
915        for d in &depths {
916            assert!(*d >= 0.0 && *d <= 1.0, "RP 2D depth should be in [0, 1]");
917        }
918    }
919}