#[cfg(any(feature = "machine_learning", feature = "utils"))]
use crate::{Deserialize, Serialize};
use ndarray::{ArrayBase, ArrayView1, Data, Ix1, Zip};
#[inline]
pub fn squared_euclidean_distance_row<S1, S2>(
x1: &ArrayBase<S1, Ix1>,
x2: &ArrayBase<S2, Ix1>,
) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
let mut sum = 0.0;
Zip::from(x1).and(x2).for_each(|&a, &b| {
let d = a - b;
sum += d * d;
});
sum
}
#[inline]
pub fn manhattan_distance_row<S1, S2>(x1: &ArrayBase<S1, Ix1>, x2: &ArrayBase<S2, Ix1>) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
let mut sum = 0.0;
Zip::from(x1)
.and(x2)
.for_each(|&a, &b| sum += (a - b).abs());
sum
}
#[inline]
pub fn minkowski_distance_row<S1, S2>(
x1: &ArrayBase<S1, Ix1>,
x2: &ArrayBase<S2, Ix1>,
p: f64,
) -> f64
where
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
{
if p < 1.0 || p.is_nan() {
panic!("invalid parameter `p`: Minkowski order must be at least 1.0, got {p}");
}
let mut sum = 0.0;
Zip::from(x1)
.and(x2)
.for_each(|&a, &b| sum += (a - b).abs().powf(p));
sum.powf(1.0 / p)
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[cfg_attr(
any(feature = "machine_learning", feature = "utils"),
derive(Deserialize, Serialize)
)]
pub enum DistanceCalculationMetric {
#[default]
Euclidean,
Manhattan,
Minkowski(f64),
}
impl DistanceCalculationMetric {
#[inline]
pub fn distance(&self, a: ArrayView1<f64>, b: ArrayView1<f64>) -> f64 {
match *self {
DistanceCalculationMetric::Euclidean => squared_euclidean_distance_row(&a, &b).sqrt(),
DistanceCalculationMetric::Manhattan => manhattan_distance_row(&a, &b),
DistanceCalculationMetric::Minkowski(p) => minkowski_distance_row(&a, &b, p),
}
}
#[inline]
pub fn within(&self, a: ArrayView1<f64>, b: ArrayView1<f64>, threshold: f64) -> bool {
self.comparable_distance(a, b) <= self.comparable_scalar(threshold)
}
pub(crate) fn comparable_scalar(&self, t: f64) -> f64 {
match *self {
DistanceCalculationMetric::Euclidean => t * t,
DistanceCalculationMetric::Manhattan => t,
DistanceCalculationMetric::Minkowski(p) => t.powf(p),
}
}
pub(crate) fn comparable_distance(&self, a: ArrayView1<f64>, b: ArrayView1<f64>) -> f64 {
match *self {
DistanceCalculationMetric::Euclidean => squared_euclidean_distance_row(&a, &b),
DistanceCalculationMetric::Manhattan => manhattan_distance_row(&a, &b),
DistanceCalculationMetric::Minkowski(p) => a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).abs().powf(p))
.sum(),
}
}
#[cfg(feature = "machine_learning")]
pub(crate) fn distance_from_comparable(&self, c: f64) -> f64 {
match *self {
DistanceCalculationMetric::Euclidean => c.sqrt(),
DistanceCalculationMetric::Manhattan => c,
DistanceCalculationMetric::Minkowski(p) => c.powf(1.0 / p),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use ndarray::array;
#[test]
fn distance_euclidean_345_triangle() {
let metric = DistanceCalculationMetric::Euclidean;
let a = array![0.0_f64, 0.0];
let b = array![3.0_f64, 4.0];
assert_abs_diff_eq!(metric.distance(a.view(), b.view()), 5.0, epsilon = 1e-6);
}
#[test]
fn distance_manhattan_345() {
let metric = DistanceCalculationMetric::Manhattan;
let a = array![0.0_f64, 0.0];
let b = array![3.0_f64, 4.0];
assert_abs_diff_eq!(metric.distance(a.view(), b.view()), 7.0, epsilon = 1e-6);
}
#[test]
fn distance_minkowski_p3() {
let metric = DistanceCalculationMetric::Minkowski(3.0);
let a = array![0.0_f64, 0.0];
let b = array![3.0_f64, 4.0];
let expected = 91.0_f64.powf(1.0 / 3.0); assert_abs_diff_eq!(
metric.distance(a.view(), b.view()),
expected,
epsilon = 1e-6
);
}
#[test]
fn distance_euclidean_symmetry() {
let metric = DistanceCalculationMetric::Euclidean;
let a = array![1.0_f64, 2.0, 3.0];
let b = array![4.0_f64, 6.0, 8.0];
assert_abs_diff_eq!(
metric.distance(a.view(), b.view()),
metric.distance(b.view(), a.view()),
epsilon = 1e-10
);
}
#[test]
fn distance_euclidean_identical_vectors() {
let metric = DistanceCalculationMetric::Euclidean;
let a = array![1.0_f64, 2.0];
assert_abs_diff_eq!(metric.distance(a.view(), a.view()), 0.0, epsilon = 1e-6);
}
#[test]
fn distance_manhattan_symmetry() {
let metric = DistanceCalculationMetric::Manhattan;
let a = array![1.0_f64, 5.0];
let b = array![3.0_f64, 2.0];
assert_abs_diff_eq!(
metric.distance(a.view(), b.view()),
metric.distance(b.view(), a.view()),
epsilon = 1e-10
);
}
}