fdars_core/metric/lp.rs
1//! Lp distance metrics for 1D and 2D functional data.
2
3use crate::helpers::{simpsons_weights, simpsons_weights_2d};
4use crate::iter_maybe_parallel;
5use crate::matrix::FdMatrix;
6#[cfg(feature = "parallel")]
7use rayon::iter::ParallelIterator;
8
9use super::{lp_weighted_distance, merge_weights};
10
11/// Domain specification for the Lp metric dispatchers [`lp_self`] / [`lp_cross`].
12///
13/// - `OneD` selects 1D functional data on a single `argvals` grid.
14/// - `TwoD` selects 2D surface data on separate `argvals_s`/`argvals_t` grids.
15#[derive(Debug, Clone, PartialEq)]
16pub enum LpDomain<'a> {
17 /// 1D functional data on a single evaluation grid.
18 OneD {
19 /// Evaluation points for integration.
20 argvals: &'a [f64],
21 },
22 /// 2D surface data on a tensor-product grid.
23 TwoD {
24 /// Grid points in the s direction.
25 argvals_s: &'a [f64],
26 /// Grid points in the t direction.
27 argvals_t: &'a [f64],
28 },
29}
30
31/// Compute the Lp self-distance matrix (symmetric) for functional data.
32///
33/// A single dispatcher over the [`LpDomain`] grid enum: `OneD` routes to the 1D
34/// self-distance, `TwoD` to the 2D surface self-distance. Numeric output is identical to the
35/// former suffixed 1D/2D self-distance functions.
36///
37/// # Arguments
38/// * `data` - Functional data matrix
39/// * `domain` - Grid specification (1D or 2D)
40/// * `p` - Order of the norm
41/// * `user_weights` - Optional user weights (empty slice for none)
42#[must_use]
43pub fn lp_self(data: &FdMatrix, domain: LpDomain<'_>, p: f64, user_weights: &[f64]) -> FdMatrix {
44 match domain {
45 LpDomain::OneD { argvals } => lp_self_1d_impl(data, argvals, p, user_weights),
46 LpDomain::TwoD {
47 argvals_s,
48 argvals_t,
49 } => lp_self_2d_impl(data, argvals_s, argvals_t, p, user_weights),
50 }
51}
52
53/// Compute the Lp cross-distance matrix between two sets of functional data.
54///
55/// A single dispatcher over the [`LpDomain`] grid enum: `OneD` routes to the 1D
56/// cross-distance, `TwoD` to the 2D surface cross-distance. Numeric output is identical to the
57/// former suffixed 1D/2D cross-distance functions.
58///
59/// # Arguments
60/// * `data1` - First dataset matrix
61/// * `data2` - Second dataset matrix
62/// * `domain` - Grid specification (1D or 2D)
63/// * `p` - Order of the norm
64/// * `user_weights` - Optional user weights (empty slice for none)
65#[must_use]
66pub fn lp_cross(
67 data1: &FdMatrix,
68 data2: &FdMatrix,
69 domain: LpDomain<'_>,
70 p: f64,
71 user_weights: &[f64],
72) -> FdMatrix {
73 match domain {
74 LpDomain::OneD { argvals } => lp_cross_1d_impl(data1, data2, argvals, p, user_weights),
75 LpDomain::TwoD {
76 argvals_s,
77 argvals_t,
78 } => lp_cross_2d_impl(data1, data2, argvals_s, argvals_t, p, user_weights),
79 }
80}
81
82/// Compute Lp distance matrix between two sets of functional data.
83///
84/// # Arguments
85/// * `data1` - First dataset matrix (n1 rows x n_points columns)
86/// * `data2` - Second dataset matrix (n2 rows x n_points columns)
87/// * `argvals` - Evaluation points for integration
88/// * `p` - Order of the norm
89/// * `user_weights` - Optional user weights (empty slice for none)
90///
91/// # Returns
92/// Distance matrix (n1 rows x n2 columns)
93///
94/// # Examples
95///
96/// ```
97/// use fdars_core::matrix::FdMatrix;
98/// use fdars_core::metric::{lp_cross, LpDomain};
99///
100/// let argvals: Vec<f64> = (0..10).map(|i| i as f64 / 9.0).collect();
101/// let data1 = FdMatrix::from_column_major(
102/// (0..30).map(|i| (i as f64 * 0.1).sin()).collect(), 3, 10,
103/// ).unwrap();
104/// let data2 = FdMatrix::from_column_major(
105/// (0..20).map(|i| (i as f64 * 0.2).cos()).collect(), 2, 10,
106/// ).unwrap();
107/// let dist = lp_cross(&data1, &data2, LpDomain::OneD { argvals: &argvals }, 2.0, &[]);
108/// assert_eq!(dist.shape(), (3, 2));
109/// assert!(dist[(0, 0)] >= 0.0);
110/// ```
111fn lp_cross_1d_impl(
112 data1: &FdMatrix,
113 data2: &FdMatrix,
114 argvals: &[f64],
115 p: f64,
116 user_weights: &[f64],
117) -> FdMatrix {
118 let n1 = data1.nrows();
119 let n2 = data2.nrows();
120 let n_points = data1.ncols();
121
122 if n1 == 0 || n2 == 0 || n_points == 0 || argvals.len() != n_points || data2.ncols() != n_points
123 {
124 return FdMatrix::zeros(0, 0);
125 }
126
127 let weights = merge_weights(simpsons_weights(argvals), user_weights);
128 let vals: Vec<f64> = iter_maybe_parallel!(0..n1)
129 .flat_map(|i| {
130 (0..n2)
131 .map(|j| lp_weighted_distance(data1, i, data2, j, &weights, n_points, p))
132 .collect::<Vec<_>>()
133 })
134 .collect();
135 let mut dist = FdMatrix::zeros(n1, n2);
136 for i in 0..n1 {
137 for j in 0..n2 {
138 dist[(i, j)] = vals[i * n2 + j];
139 }
140 }
141 dist
142}
143
144/// Compute Lp distance matrix for self-distances (symmetric).
145///
146/// Returns symmetric distance matrix (n rows x n columns).
147///
148/// # Examples
149///
150/// ```
151/// use fdars_core::matrix::FdMatrix;
152/// use fdars_core::metric::{lp_self, LpDomain};
153///
154/// let argvals: Vec<f64> = (0..10).map(|i| i as f64 / 9.0).collect();
155/// let data = FdMatrix::from_column_major(
156/// (0..50).map(|i| (i as f64 * 0.1).sin()).collect(),
157/// 5, 10,
158/// ).unwrap();
159/// let dist = lp_self(&data, LpDomain::OneD { argvals: &argvals }, 2.0, &[]);
160/// assert_eq!(dist.shape(), (5, 5));
161/// // Diagonal should be zero, matrix should be symmetric
162/// assert!((dist[(0, 0)]).abs() < 1e-10);
163/// assert!((dist[(0, 1)] - dist[(1, 0)]).abs() < 1e-10);
164/// ```
165fn lp_self_1d_impl(data: &FdMatrix, argvals: &[f64], p: f64, user_weights: &[f64]) -> FdMatrix {
166 let n = data.nrows();
167 let n_points = data.ncols();
168
169 if n == 0 || n_points == 0 || argvals.len() != n_points {
170 return FdMatrix::zeros(0, 0);
171 }
172
173 let weights = merge_weights(simpsons_weights(argvals), user_weights);
174 // Inline the self-distance pattern rather than going through self_distance_matrix
175 // to ensure LLVM can fully optimize the tight p=2 inner loop.
176 let upper_vals: Vec<f64> = iter_maybe_parallel!(0..n)
177 .flat_map(|i| {
178 ((i + 1)..n)
179 .map(|j| lp_weighted_distance(data, i, data, j, &weights, n_points, p))
180 .collect::<Vec<_>>()
181 })
182 .collect();
183 let mut dist = FdMatrix::zeros(n, n);
184 let mut idx = 0;
185 for i in 0..n {
186 for j in (i + 1)..n {
187 let d = upper_vals[idx];
188 dist[(i, j)] = d;
189 dist[(j, i)] = d;
190 idx += 1;
191 }
192 }
193 dist
194}
195
196/// Compute Lp distance for 2D functional data (surfaces).
197fn lp_cross_2d_impl(
198 data1: &FdMatrix,
199 data2: &FdMatrix,
200 argvals_s: &[f64],
201 argvals_t: &[f64],
202 p: f64,
203 user_weights: &[f64],
204) -> FdMatrix {
205 let n1 = data1.nrows();
206 let n2 = data2.nrows();
207 let n_points = argvals_s.len() * argvals_t.len();
208 if n1 == 0 || n2 == 0 || n_points == 0 || data1.ncols() != n_points || data2.ncols() != n_points
209 {
210 return FdMatrix::zeros(0, 0);
211 }
212
213 let weights = merge_weights(simpsons_weights_2d(argvals_s, argvals_t), user_weights);
214 let vals: Vec<f64> = iter_maybe_parallel!(0..n1)
215 .flat_map(|i| {
216 (0..n2)
217 .map(|j| lp_weighted_distance(data1, i, data2, j, &weights, n_points, p))
218 .collect::<Vec<_>>()
219 })
220 .collect();
221 let mut dist = FdMatrix::zeros(n1, n2);
222 for i in 0..n1 {
223 for j in 0..n2 {
224 dist[(i, j)] = vals[i * n2 + j];
225 }
226 }
227 dist
228}
229
230/// Compute Lp self-distance matrix for 2D functional data (symmetric).
231fn lp_self_2d_impl(
232 data: &FdMatrix,
233 argvals_s: &[f64],
234 argvals_t: &[f64],
235 p: f64,
236 user_weights: &[f64],
237) -> FdMatrix {
238 let n = data.nrows();
239 let n_points = argvals_s.len() * argvals_t.len();
240 if n == 0 || n_points == 0 || data.ncols() != n_points {
241 return FdMatrix::zeros(0, 0);
242 }
243
244 let weights = merge_weights(simpsons_weights_2d(argvals_s, argvals_t), user_weights);
245 let upper_vals: Vec<f64> = iter_maybe_parallel!(0..n)
246 .flat_map(|i| {
247 ((i + 1)..n)
248 .map(|j| lp_weighted_distance(data, i, data, j, &weights, n_points, p))
249 .collect::<Vec<_>>()
250 })
251 .collect();
252 let mut dist = FdMatrix::zeros(n, n);
253 let mut idx = 0;
254 for i in 0..n {
255 for j in (i + 1)..n {
256 let d = upper_vals[idx];
257 dist[(i, j)] = d;
258 dist[(j, i)] = d;
259 idx += 1;
260 }
261 }
262 dist
263}