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