use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MsmConfig {
pub c: f64,
}
impl MsmConfig {
#[inline]
pub fn new(c: f64) -> Self {
assert!(
c >= 0.0,
"Split/merge cost c must be non-negative, got {}",
c
);
Self { c }
}
#[inline]
pub fn default_cost() -> Self {
Self::new(1.0)
}
pub fn distance(&self, x: &[f64], y: &[f64]) -> f64 {
let m = x.len();
let n = y.len();
if m == 0 && n == 0 {
return 0.0;
}
if m == 0 || n == 0 {
return f64::INFINITY;
}
let mut cost = vec![vec![f64::INFINITY; n + 1]; m + 1];
cost[1][1] = (x[0] - y[0]).abs();
for i in 2..=m {
cost[i][1] = cost[i - 1][1] + self.c_func(x[i - 1], x[i - 2], y[0]);
}
for j in 2..=n {
cost[1][j] = cost[1][j - 1] + self.c_func(y[j - 1], x[0], y[j - 2]);
}
for i in 2..=m {
for j in 2..=n {
let move_cost = cost[i - 1][j - 1] + (x[i - 1] - y[j - 1]).abs();
let merge_cost = cost[i - 1][j] + self.c_func(x[i - 1], x[i - 2], y[j - 1]);
let split_cost = cost[i][j - 1] + self.c_func(y[j - 1], x[i - 1], y[j - 2]);
cost[i][j] = move_cost.min(merge_cost).min(split_cost);
}
}
cost[m][n]
}
pub fn distance_optimized(&self, x: &[f64], y: &[f64]) -> f64 {
if x.len() < y.len() {
return self.distance_optimized(y, x);
}
let m = x.len();
let n = y.len();
if m == 0 && n == 0 {
return 0.0;
}
if n == 0 {
return f64::INFINITY;
}
let mut prev = vec![f64::INFINITY; n + 1];
let mut curr = vec![f64::INFINITY; n + 1];
prev[1] = (x[0] - y[0]).abs();
for j in 2..=n {
prev[j] = prev[j - 1] + self.c_func(y[j - 1], x[0], y[j - 2]);
}
for i in 2..=m {
curr[1] = prev[1] + self.c_func(x[i - 1], x[i - 2], y[0]);
for j in 2..=n {
let move_cost = prev[j - 1] + (x[i - 1] - y[j - 1]).abs();
let merge_cost = prev[j] + self.c_func(x[i - 1], x[i - 2], y[j - 1]);
let split_cost = curr[j - 1] + self.c_func(y[j - 1], x[i - 1], y[j - 2]);
curr[j] = move_cost.min(merge_cost).min(split_cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[n]
}
pub fn distance_with_matrix(&self, x: &[f64], y: &[f64]) -> MsmResult {
let m = x.len();
let n = y.len();
if m == 0 && n == 0 {
return MsmResult {
distance: 0.0,
matrix: vec![vec![0.0]],
};
}
if m == 0 || n == 0 {
return MsmResult {
distance: f64::INFINITY,
matrix: vec![vec![f64::INFINITY; n + 1]; m + 1],
};
}
let mut cost = vec![vec![f64::INFINITY; n + 1]; m + 1];
cost[1][1] = (x[0] - y[0]).abs();
for i in 2..=m {
cost[i][1] = cost[i - 1][1] + self.c_func(x[i - 1], x[i - 2], y[0]);
}
for j in 2..=n {
cost[1][j] = cost[1][j - 1] + self.c_func(y[j - 1], x[0], y[j - 2]);
}
for i in 2..=m {
for j in 2..=n {
let move_cost = cost[i - 1][j - 1] + (x[i - 1] - y[j - 1]).abs();
let merge_cost = cost[i - 1][j] + self.c_func(x[i - 1], x[i - 2], y[j - 1]);
let split_cost = cost[i][j - 1] + self.c_func(y[j - 1], x[i - 1], y[j - 2]);
cost[i][j] = move_cost.min(merge_cost).min(split_cost);
}
}
MsmResult {
distance: cost[m][n],
matrix: cost,
}
}
#[inline]
pub fn c_func(&self, a: f64, b: f64, c: f64) -> f64 {
let between_bc = (b <= a && a <= c) || (b >= a && a >= c);
if between_bc {
self.c
} else {
self.c + (a - b).abs().min((a - c).abs())
}
}
}
impl Default for MsmConfig {
fn default() -> Self {
Self::default_cost()
}
}
#[derive(Debug, Clone)]
pub struct MsmResult {
pub distance: f64,
pub matrix: Vec<Vec<f64>>,
}
impl fmt::Display for MsmResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "MSM Distance: {}", self.distance)?;
writeln!(f, "DP Matrix:")?;
for (i, row) in self.matrix.iter().enumerate() {
write!(f, " [{:2}] ", i)?;
for cell in row {
if cell.is_infinite() {
write!(f, " inf ")?;
} else {
write!(f, "{:5.2} ", cell)?;
}
}
writeln!(f)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-10;
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < EPSILON
}
#[test]
fn test_identical_series() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
assert!(approx_eq(config.distance(&x, &x), 0.0));
}
#[test]
fn test_single_element_move() {
let config = MsmConfig::new(1.0);
assert!(approx_eq(config.distance(&[1.0], &[2.0]), 1.0));
assert!(approx_eq(config.distance(&[0.0], &[5.0]), 5.0));
}
#[test]
fn test_empty_series() {
let config = MsmConfig::new(1.0);
assert!(approx_eq(config.distance(&[], &[]), 0.0));
assert!(config.distance(&[], &[1.0]).is_infinite());
assert!(config.distance(&[1.0], &[]).is_infinite());
}
#[test]
fn test_c_function_between() {
let config = MsmConfig::new(1.0);
assert!(approx_eq(config.c_func(2.0, 1.0, 3.0), 1.0)); assert!(approx_eq(config.c_func(2.0, 3.0, 1.0), 1.0)); assert!(approx_eq(config.c_func(1.0, 1.0, 1.0), 1.0)); }
#[test]
fn test_c_function_outside() {
let config = MsmConfig::new(1.0);
assert!(approx_eq(config.c_func(0.0, 1.0, 3.0), 2.0));
assert!(approx_eq(config.c_func(5.0, 1.0, 3.0), 3.0));
}
#[test]
fn test_symmetry() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0, 3.0, 2.0, 1.0];
let y = vec![1.0, 3.0, 2.0];
let d_xy = config.distance(&x, &y);
let d_yx = config.distance(&y, &x);
assert!(approx_eq(d_xy, d_yx), "d(x,y)={} != d(y,x)={}", d_xy, d_yx);
}
#[test]
fn test_triangle_inequality() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.5, 2.5, 3.5];
let z = vec![2.0, 3.0, 4.0];
let d_xz = config.distance(&x, &z);
let d_xy = config.distance(&x, &y);
let d_yz = config.distance(&y, &z);
assert!(
d_xz <= d_xy + d_yz + EPSILON,
"Triangle inequality violated: d(x,z)={} > d(x,y)+d(y,z)={}",
d_xz,
d_xy + d_yz
);
}
#[test]
fn test_optimized_matches_standard() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.5, 2.5, 3.5, 4.5];
let d1 = config.distance(&x, &y);
let d2 = config.distance_optimized(&x, &y);
assert!(
approx_eq(d1, d2),
"Standard distance {} != optimized distance {}",
d1,
d2
);
}
#[test]
fn test_with_matrix() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0, 3.0];
let y = vec![1.0, 2.0, 3.0];
let result = config.distance_with_matrix(&x, &y);
assert!(approx_eq(result.distance, 0.0));
assert_eq!(result.matrix.len(), 4); assert_eq!(result.matrix[0].len(), 4); }
#[test]
fn test_different_c_values() {
let x = vec![1.0, 1.0, 2.0];
let y = vec![1.0, 2.0];
let config1 = MsmConfig::new(0.5);
let d1 = config1.distance(&x, &y);
let config2 = MsmConfig::new(2.0);
let d2 = config2.distance(&x, &y);
assert!(d1 <= d2 || approx_eq(d1, d2));
}
#[test]
#[should_panic(expected = "must be non-negative")]
fn test_negative_c_panics() {
MsmConfig::new(-1.0);
}
#[test]
fn test_paper_example() {
let config = MsmConfig::new(1.0);
let x = vec![1.0, 2.0];
let y = vec![1.0, 2.0];
assert!(approx_eq(config.distance(&x, &y), 0.0));
let x = vec![1.0, 2.0, 3.0];
let y = vec![2.0, 3.0, 4.0];
assert!(approx_eq(config.distance(&x, &y), 3.0));
}
}