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