fdars_core/depth/rpd.rs
1//! Random Projection with Derivatives (RPD) depth.
2//!
3//! Enriches random projection depth by incorporating derivative information.
4//! Each curve is augmented with its finite-difference derivatives up to order
5//! `nderiv`, and standard random projection depth is computed on the
6//! augmented representation.
7
8use crate::matrix::FdMatrix;
9
10/// Compute RPD depth for 1D functional data.
11///
12/// Projects curves augmented with derivatives to scalars using random
13/// projections and computes average univariate depth.
14///
15/// # Arguments
16/// * `data_obj` - Data to compute depth for (n_obj x m)
17/// * `data_ori` - Reference data (n_ori x m)
18/// * `argvals` - Evaluation points (length m), used for finite-difference spacing
19/// * `nproj` - Number of random projections
20/// * `nderiv` - Maximum derivative order to include (0 = no derivatives, same as RP depth)
21///
22/// # Examples
23///
24/// ```
25/// use fdars_core::matrix::FdMatrix;
26/// use fdars_core::depth::rpd_depth_1d;
27///
28/// let data = FdMatrix::from_column_major(
29/// (0..50).map(|i| (i as f64 * 0.1).sin()).collect(),
30/// 5, 10,
31/// ).unwrap();
32/// let argvals: Vec<f64> = (0..10).map(|i| i as f64 / 9.0).collect();
33/// let depths = rpd_depth_1d(&data, &data, &argvals, 50, 1);
34/// assert_eq!(depths.len(), 5);
35/// assert!(depths.iter().all(|&d| d >= 0.0));
36/// ```
37#[must_use = "expensive computation whose result should not be discarded"]
38pub fn rpd_depth_1d(
39 data_obj: &FdMatrix,
40 data_ori: &FdMatrix,
41 argvals: &[f64],
42 nproj: usize,
43 nderiv: usize,
44) -> Vec<f64> {
45 rpd_depth_1d_seeded(data_obj, data_ori, argvals, nproj, nderiv, None)
46}
47
48/// Compute RPD depth with optional seed for reproducibility.
49///
50/// See [`rpd_depth_1d`] for details. When `seed` is `Some`, the random
51/// projections are generated deterministically.
52#[must_use = "expensive computation whose result should not be discarded"]
53pub fn rpd_depth_1d_seeded(
54 data_obj: &FdMatrix,
55 data_ori: &FdMatrix,
56 argvals: &[f64],
57 nproj: usize,
58 nderiv: usize,
59 seed: Option<u64>,
60) -> Vec<f64> {
61 let m = data_obj.ncols();
62
63 // Edge case: empty data
64 if data_obj.nrows() == 0 || data_ori.nrows() == 0 || m == 0 || nproj == 0 {
65 return Vec::new();
66 }
67
68 // Edge case: too few grid points for requested derivatives
69 if m <= nderiv {
70 return vec![0.0; data_obj.nrows()];
71 }
72
73 // When nderiv == 0, skip augmentation and delegate directly
74 if nderiv == 0 {
75 return super::random_depth_core(
76 data_obj,
77 data_ori,
78 nproj,
79 seed,
80 0.0,
81 |acc, d| acc + d,
82 |acc, n| acc / n as f64,
83 );
84 }
85
86 // Compute augmented dimension: m + (m-1) + (m-2) + ... + (m-nderiv)
87 let aug_dim: usize = (0..=nderiv).map(|k| m - k).sum();
88
89 let aug_obj = build_augmented(data_obj, argvals, nderiv, aug_dim);
90 let aug_ori = build_augmented(data_ori, argvals, nderiv, aug_dim);
91
92 super::random_depth_core(
93 &aug_obj,
94 &aug_ori,
95 nproj,
96 seed,
97 0.0,
98 |acc, d| acc + d,
99 |acc, n| acc / n as f64,
100 )
101}
102
103/// Build augmented matrix by concatenating function values with finite-difference
104/// derivatives up to order `nderiv`.
105///
106/// For each curve, the augmented row is:
107/// `[f(t_1), ..., f(t_m), f'(t_1), ..., f'(t_{m-1}), f''(t_1), ..., f''(t_{m-2}), ...]`
108fn build_augmented(data: &FdMatrix, argvals: &[f64], nderiv: usize, aug_dim: usize) -> FdMatrix {
109 let n = data.nrows();
110 let m = data.ncols();
111
112 // We'll work with row-major intermediate storage, then convert to column-major.
113 // aug_data is column-major: aug_data[i + col * n]
114 let mut aug_data = vec![0.0; n * aug_dim];
115
116 // Copy original values (derivative order 0)
117 for j in 0..m {
118 for i in 0..n {
119 aug_data[i + j * n] = data[(i, j)];
120 }
121 }
122
123 // Compute derivatives iteratively.
124 // We keep a buffer of the "previous derivative" values per curve.
125 // prev_deriv[i * prev_len + j] = d^(k-1)/dt^(k-1) f_i(t_j)
126 let mut prev_len = m;
127 let mut prev_deriv: Vec<f64> = (0..n * m)
128 .map(|idx| {
129 let i = idx % n;
130 let j = idx / n;
131 data[(i, j)]
132 })
133 .collect();
134
135 let mut col_offset = m; // where to start writing in augmented matrix
136
137 for k in 1..=nderiv {
138 let new_len = m - k;
139 let mut new_deriv = vec![0.0; n * new_len];
140
141 // The argvals spacing for derivative k uses the midpoints from the
142 // previous level. For simplicity, we use the spacing from the
143 // original grid points that are k apart:
144 // d^k f / dt^k ≈ (d^{k-1}f(t_{j+1}) - d^{k-1}f(t_j)) / (t_{j+1} - t_j)
145 //
146 // For the k-th derivative, the denominator uses argvals[j+1] - argvals[j]
147 // at the level of the (k-1)-th derivative's grid.
148 // After the first derivative, the grid points shift. We approximate using
149 // the original grid spacing: argvals[j+k] - argvals[j+k-1] for derivative
150 // level, but for finite differences we use the spacing between consecutive
151 // points of the previous derivative's implicit grid.
152 //
153 // The standard approach: the k-th finite difference uses
154 // (prev[j+1] - prev[j]) / (argvals[j+1] - argvals[j]) where argvals
155 // here refers to the spacing appropriate for this derivative level.
156 // For the first derivative: dt_j = argvals[j+1] - argvals[j]
157 // For the second derivative operating on first-derivative values at
158 // midpoints (argvals[j] + argvals[j+1])/2:
159 // dt_j = midpoint[j+1] - midpoint[j] = (argvals[j+2] - argvals[j]) / 2
160 //
161 // For simplicity and consistency with R's depth.RPD, we use the spacing
162 // of consecutive original grid points offset by (k-1):
163 // dt_j = argvals[j + 1 + (k-1)] - argvals[j + (k-1)]
164 // = argvals[j + k] - argvals[j + k - 1]
165 // but actually the simplest correct approach for iterated differences is
166 // just: (prev[j+1] - prev[j]) / (argvals[j+1] - argvals[j]) at each level.
167 // However, the "argvals" for derivative level k are the midpoints from
168 // level k-1. Let's just use a constant dt approach: we divide by the
169 // spacing of the *previous level's* implicit grid.
170
171 for j in 0..new_len {
172 // Spacing: use the difference of the original argvals indices
173 // that correspond to the endpoints of this finite difference.
174 // For iterated forward differences, the j-th value at level k
175 // spans original indices [j, j+k], so we use:
176 // dt = argvals[j + k] - argvals[j + k - 1]
177 // But more precisely, for the simple forward-difference iteration,
178 // the denominator at each level should be the spacing of the
179 // previous derivative's grid. Since the previous derivative's j-th
180 // value corresponds to the interval [argvals[j + k-1], argvals[j + k]],
181 // we approximate its "position" as the midpoint, and the spacing
182 // between consecutive positions is:
183 // (argvals[j+k] + argvals[j+k-1])/2 - (argvals[j+k-1] + argvals[j+k-2])/2
184 // = (argvals[j+k] - argvals[j+k-2]) / 2
185 //
186 // For simplicity and numerical stability, we just use:
187 // dt = argvals[j+1] - argvals[j] (for k=1)
188 // dt = argvals[j+2] - argvals[j+1] (for k=2, operating on first-deriv values)
189 // i.e., argvals[j + k] - argvals[j + k - 1]
190 let dt = argvals[j + k] - argvals[j + k - 1];
191 let inv_dt = if dt.abs() < 1e-30 { 0.0 } else { 1.0 / dt };
192
193 for i in 0..n {
194 let val = (prev_deriv[i + (j + 1) * n] - prev_deriv[i + j * n]) * inv_dt;
195 new_deriv[i + j * n] = val;
196 aug_data[i + (col_offset + j) * n] = val;
197 }
198 }
199
200 col_offset += new_len;
201 prev_deriv = new_deriv;
202 prev_len = new_len;
203 }
204
205 // Suppress unused variable warning
206 let _ = prev_len;
207
208 FdMatrix::from_column_major(aug_data, n, aug_dim).unwrap()
209}