use super::msm::MsmConfig;
pub fn euclidean_lb(x: &[f64], y: &[f64]) -> f64 {
if x.is_empty() || y.is_empty() {
if x.is_empty() && y.is_empty() {
return 0.0;
}
return f64::INFINITY;
}
let min_len = x.len().min(y.len());
let sum_sq: f64 = x[..min_len]
.iter()
.zip(y[..min_len].iter())
.map(|(&xi, &yi)| (xi - yi).powi(2))
.sum();
sum_sq.sqrt()
}
pub fn length_lb(x: &[f64], y: &[f64], c: f64) -> f64 {
if x.is_empty() && y.is_empty() {
return 0.0;
}
if x.is_empty() || y.is_empty() {
return f64::INFINITY;
}
let len_diff = (x.len() as isize - y.len() as isize).unsigned_abs();
len_diff as f64 * c
}
pub fn combined_lb(x: &[f64], y: &[f64], c: f64) -> f64 {
euclidean_lb(x, y).max(length_lb(x, y, c))
}
pub fn l1_lb(x: &[f64], y: &[f64]) -> f64 {
if x.is_empty() || y.is_empty() {
if x.is_empty() && y.is_empty() {
return 0.0;
}
return f64::INFINITY;
}
let min_len = x.len().min(y.len());
x[..min_len]
.iter()
.zip(y[..min_len].iter())
.map(|(&xi, &yi)| (xi - yi).abs())
.sum()
}
#[derive(Debug, Clone, Copy)]
pub struct LowerBoundConfig {
pub c: f64,
pub bounds: LowerBoundType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LowerBoundType {
LengthOnly,
EuclideanOnly,
L1Only,
Combined,
}
impl LowerBoundType {
#[inline]
pub fn is_proven_safe_for_pruning(self) -> bool {
matches!(self, LowerBoundType::LengthOnly)
}
}
impl LowerBoundConfig {
pub fn new(c: f64) -> Self {
Self {
c,
bounds: LowerBoundType::LengthOnly,
}
}
pub fn lower_bound(&self, x: &[f64], y: &[f64]) -> f64 {
match self.bounds {
LowerBoundType::LengthOnly => length_lb(x, y, self.c),
LowerBoundType::EuclideanOnly => euclidean_lb(x, y),
LowerBoundType::L1Only => l1_lb(x, y),
LowerBoundType::Combined => euclidean_lb(x, y)
.max(length_lb(x, y, self.c))
.max(l1_lb(x, y)),
}
}
}
pub fn filter_by_lower_bound<'a, V: Clone + 'a>(
query: &'a [f64],
candidates: impl Iterator<Item = (V, &'a [f64])> + 'a,
threshold: f64,
lb_config: LowerBoundConfig,
) -> impl Iterator<Item = (V, &'a [f64])> + 'a {
candidates.filter(move |(_, series)| lb_config.lower_bound(query, series) <= threshold)
}
pub fn search_with_lb<V: Clone>(
query: &[f64],
database: &[(V, Vec<f64>)],
threshold: f64,
msm_config: &MsmConfig,
) -> Vec<(V, f64)> {
let lb_config = LowerBoundConfig::new(msm_config.c);
let mut results: Vec<(V, f64)> = database
.iter()
.filter_map(|(value, series)| {
if lb_config.lower_bound(query, series) > threshold {
return None;
}
let dist = msm_config.distance(query, series);
if dist <= threshold + 1e-9 {
Some((value.clone(), dist))
} else {
None
}
})
.collect();
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
results
}
#[cfg(feature = "rayon")]
pub fn search_with_lb_parallel<V: Clone + Send + Sync>(
query: &[f64],
database: &[(V, Vec<f64>)],
threshold: f64,
msm_config: &MsmConfig,
) -> Vec<(V, f64)> {
use rayon::prelude::*;
let lb_config = LowerBoundConfig::new(msm_config.c);
let mut results: Vec<(V, f64)> = database
.par_iter()
.filter_map(|(value, series)| {
if lb_config.lower_bound(query, series) > threshold {
return None;
}
let dist = msm_config.distance(query, series);
if dist <= threshold + 1e-9 {
Some((value.clone(), dist))
} else {
None
}
})
.collect();
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
results
}
#[derive(Debug, Clone)]
pub struct LowerBoundStats {
pub total_candidates: usize,
pub pruned_by_lb: usize,
pub passed_lb: usize,
pub passed_exact: usize,
pub pruning_rate: f64,
pub false_positive_rate: f64,
}
pub fn search_with_lb_stats<V: Clone>(
query: &[f64],
database: &[(V, Vec<f64>)],
threshold: f64,
msm_config: &MsmConfig,
) -> (Vec<(V, f64)>, LowerBoundStats) {
let lb_config = LowerBoundConfig::new(msm_config.c);
let total_candidates = database.len();
let mut pruned_by_lb = 0;
let mut passed_lb = 0;
let mut passed_exact = 0;
let mut results: Vec<(V, f64)> = Vec::new();
for (value, series) in database {
if lb_config.lower_bound(query, series) > threshold {
pruned_by_lb += 1;
continue;
}
passed_lb += 1;
let dist = msm_config.distance(query, series);
if dist <= threshold + 1e-9 {
passed_exact += 1;
results.push((value.clone(), dist));
}
}
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let stats = LowerBoundStats {
total_candidates,
pruned_by_lb,
passed_lb,
passed_exact,
pruning_rate: if total_candidates > 0 {
pruned_by_lb as f64 / total_candidates as f64
} else {
0.0
},
false_positive_rate: if passed_lb > 0 {
(passed_lb - passed_exact) as f64 / passed_lb as f64
} else {
0.0
},
};
(results, stats)
}
impl std::fmt::Display for LowerBoundStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Lower Bound Pruning Statistics:")?;
writeln!(f, " Total candidates: {}", self.total_candidates)?;
writeln!(f, " Pruned by safe bound: {}", self.pruned_by_lb)?;
writeln!(f, " Passed safe bound: {}", self.passed_lb)?;
writeln!(f, " Passed exact: {}", self.passed_exact)?;
writeln!(f, " Pruning rate: {:.1}%", self.pruning_rate * 100.0)?;
writeln!(
f,
" False positive rate: {:.1}%",
self.false_positive_rate * 100.0
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-9;
fn approx_eq(a: f64, b: f64) -> bool {
if a.is_infinite() && b.is_infinite() {
return a.signum() == b.signum();
}
(a - b).abs() < EPSILON
}
#[test]
fn test_euclidean_lb_identical() {
let x = vec![1.0, 2.0, 3.0, 4.0];
assert!(approx_eq(euclidean_lb(&x, &x), 0.0));
}
#[test]
fn test_euclidean_lb_simple() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![2.0, 3.0, 4.0];
let lb = euclidean_lb(&x, &y);
assert!((lb - 3.0_f64.sqrt()).abs() < 0.01);
}
#[test]
fn test_euclidean_lb_empty() {
let x: Vec<f64> = vec![];
let y = vec![1.0, 2.0, 3.0];
assert!(euclidean_lb(&x, &y).is_infinite());
assert!(euclidean_lb(&y, &x).is_infinite());
assert!(approx_eq(euclidean_lb(&x, &x), 0.0));
}
#[test]
fn test_euclidean_lb_different_lengths() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![1.0, 2.0];
let lb = euclidean_lb(&x, &y);
assert!(approx_eq(lb, 0.0));
}
#[test]
fn test_length_lb_same_length() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![4.0, 5.0, 6.0];
assert!(approx_eq(length_lb(&x, &y, 1.0), 0.0));
}
#[test]
fn test_length_lb_different_lengths() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.0, 2.0, 3.0];
let c = 1.0;
assert!(approx_eq(length_lb(&x, &y, c), 2.0));
}
#[test]
fn test_length_lb_different_c() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![1.0];
assert!(approx_eq(length_lb(&x, &y, 1.0), 3.0));
assert!(approx_eq(length_lb(&x, &y, 2.0), 6.0));
assert!(approx_eq(length_lb(&x, &y, 0.5), 1.5));
}
#[test]
fn test_l1_lb_identical() {
let x = vec![1.0, 2.0, 3.0];
assert!(approx_eq(l1_lb(&x, &x), 0.0));
}
#[test]
fn test_l1_lb_simple() {
let x = vec![1.0, 2.0, 3.0];
let y = vec![2.0, 3.0, 4.0];
assert!(approx_eq(l1_lb(&x, &y), 3.0));
}
#[test]
fn test_combined_lb() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![1.0, 2.0];
let c = 1.0;
let combined = combined_lb(&x, &y, c);
let euclidean = euclidean_lb(&x, &y);
let length = length_lb(&x, &y, c);
assert!(approx_eq(combined, euclidean.max(length)));
}
#[test]
fn test_heuristic_bounds_are_not_general_msm_lower_bounds() {
let config = MsmConfig::new(1.0);
let x = vec![0.0, 100.0];
let y = vec![0.0, 0.0, 100.0];
let msm = config.distance(&x, &y);
assert!(approx_eq(msm, 1.0));
assert!(euclidean_lb(&x, &y) > msm);
assert!(l1_lb(&x, &y) > msm);
assert!(combined_lb(&x, &y, config.c) > msm);
assert!(length_lb(&x, &y, config.c) <= msm + EPSILON);
}
#[test]
fn test_search_with_lb() {
let config = MsmConfig::new(1.0);
let database: Vec<(usize, Vec<f64>)> = vec![
(0, vec![1.0, 2.0, 3.0]),
(1, vec![1.1, 2.1, 3.1]),
(2, vec![10.0, 20.0, 30.0]),
(3, vec![1.0, 2.0]),
(4, vec![1.0, 2.0, 3.0, 4.0]),
];
let query = vec![1.0, 2.0, 3.0];
let results = search_with_lb(&query, &database, 1.0, &config);
let found_ids: Vec<usize> = results.iter().map(|(id, _)| *id).collect();
assert!(found_ids.contains(&0));
assert!(found_ids.contains(&1));
assert!(!found_ids.contains(&2)); }
#[test]
fn test_search_with_lb_stats() {
let config = MsmConfig::new(1.0);
let database: Vec<(usize, Vec<f64>)> = vec![
(0, vec![1.0, 2.0, 3.0]),
(1, vec![100.0, 200.0, 300.0, 400.0, 500.0, 600.0]), (2, vec![1.5, 2.5, 3.5]),
(3, vec![50.0, 60.0, 70.0]),
];
let query = vec![1.0, 2.0, 3.0];
let (results, stats) = search_with_lb_stats(&query, &database, 2.0, &config);
assert_eq!(stats.total_candidates, 4);
assert!(stats.pruned_by_lb >= 1); assert!(results.len() >= 1);
}
#[test]
fn test_lower_bound_config() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![1.0, 2.0];
let config = LowerBoundConfig {
c: 1.0,
bounds: LowerBoundType::LengthOnly,
};
let lb_length = config.lower_bound(&x, &y);
assert!(approx_eq(lb_length, 2.0));
let config = LowerBoundConfig {
c: 1.0,
bounds: LowerBoundType::EuclideanOnly,
};
let lb_euclidean = config.lower_bound(&x, &y);
assert!(approx_eq(lb_euclidean, 0.0));
let config = LowerBoundConfig {
c: 1.0,
bounds: LowerBoundType::Combined,
};
let lb_combined = config.lower_bound(&x, &y);
assert!(approx_eq(lb_combined, 2.0)); }
#[test]
fn test_filter_by_lower_bound() {
let query = vec![1.0, 2.0, 3.0];
let candidates: Vec<(usize, Vec<f64>)> = vec![
(0, vec![1.0, 2.0, 3.0]), (1, vec![100.0, 200.0, 300.0]), (2, vec![1.5, 2.5, 3.5]), ];
let lb_config = LowerBoundConfig {
c: 1.0,
bounds: LowerBoundType::Combined,
};
let candidate_refs: Vec<(usize, &[f64])> = candidates
.iter()
.map(|(id, series)| (*id, series.as_slice()))
.collect();
let filtered: Vec<_> =
filter_by_lower_bound(&query, candidate_refs.into_iter(), 5.0, lb_config).collect();
let found_ids: Vec<usize> = filtered.iter().map(|(id, _)| *id).collect();
assert!(found_ids.contains(&0));
assert!(!found_ids.contains(&1)); assert!(found_ids.contains(&2));
}
#[test]
fn test_l1_tighter_than_euclidean() {
let x = vec![1.0, 2.0, 3.0, 4.0];
let y = vec![2.0, 3.0, 4.0, 5.0];
let l1 = l1_lb(&x, &y); let euclidean = euclidean_lb(&x, &y);
assert!(l1 > euclidean);
}
}