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