Skip to main content

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}