pub trait Ultrametric<T> {
fn ultrametric_distance(&self, a: &T, b: &T) -> f64;
fn ultrametric_ball(&self, center: &T, radius: f64) -> Vec<T>
where
T: Clone;
}
#[derive(Debug, Clone)]
pub struct UltrametricTree {
parents: Vec<i32>,
heights: Vec<f64>,
num_leaves: usize,
}
impl UltrametricTree {
pub fn from_merges(merges: &[(usize, usize, f64, usize)], n_leaves: usize) -> Self {
let n_internal = merges.len();
let n_total = n_leaves + n_internal;
let mut parents = vec![-1i32; n_total];
let mut heights = vec![0.0f64; n_total];
for (i, &(a, b, h, _)) in merges.iter().enumerate() {
let internal_id = n_leaves + i;
parents[a] = internal_id as i32;
parents[b] = internal_id as i32;
heights[internal_id] = h;
}
Self {
parents,
heights,
num_leaves: n_leaves,
}
}
pub fn lca(&self, a: usize, b: usize) -> usize {
let mut ancestors_a = std::collections::HashSet::new();
let mut current = a;
while current < self.parents.len() {
let _ = ancestors_a.insert(current);
let parent = self.parents[current];
if parent < 0 {
break;
}
current = parent as usize;
}
let _ = ancestors_a.insert(current);
current = b;
while !ancestors_a.contains(¤t) {
let parent = self.parents[current];
if parent < 0 {
break;
}
current = parent as usize;
}
current
}
pub fn distance(&self, a: usize, b: usize) -> f64 {
if a == b {
return 0.0;
}
let lca = self.lca(a, b);
self.heights[lca]
}
pub fn height(&self, node: usize) -> f64 {
self.heights.get(node).copied().unwrap_or(0.0)
}
pub fn num_leaves(&self) -> usize {
self.num_leaves
}
#[cfg(test)]
fn verify_ultrametric(&self) -> bool {
for i in 0..self.num_leaves {
for j in 0..self.num_leaves {
for k in 0..self.num_leaves {
let d_ik = self.distance(i, k);
let d_ij = self.distance(i, j);
let d_jk = self.distance(j, k);
if d_ik > d_ij.max(d_jk) + 1e-10 {
return false;
}
}
}
}
true
}
}
pub fn subdominant_ultrametric(distances: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = distances.len();
let mut u = distances.to_vec();
for k in 0..n {
for i in 0..n {
for j in 0..n {
let path_through_k = u[i][k].max(u[k][j]);
if path_through_k < u[i][j] {
u[i][j] = path_through_k;
}
}
}
}
u
}
pub fn is_ultrametric(distances: &[Vec<f64>], tolerance: f64) -> bool {
let n = distances.len();
for i in 0..n {
for j in 0..n {
for k in 0..n {
let d_ik = distances[i][k];
let d_ij = distances[i][j];
let d_jk = distances[j][k];
if d_ik > d_ij.max(d_jk) + tolerance {
return false;
}
}
}
}
true
}
pub fn embedding_distortion(original: &[Vec<f64>], embedded: &[Vec<f64>]) -> f64 {
let n = original.len();
let mut max_expansion = 1.0f64;
let mut max_contraction = 1.0f64;
for i in 0..n {
for j in (i + 1)..n {
let d_orig = original[i][j];
let d_embed = embedded[i][j];
if d_orig > 1e-10 {
max_expansion = max_expansion.max(d_embed / d_orig);
}
if d_embed > 1e-10 {
max_contraction = max_contraction.max(d_orig / d_embed);
}
}
}
max_expansion.max(max_contraction)
}
pub fn gromov_hyperbolicity(distances: &[Vec<f64>]) -> f64 {
let n = distances.len();
let mut max_delta = 0.0f64;
for x in 0..n {
for y in 0..n {
for z in 0..n {
for w in 0..n {
let sum_xy_zw = distances[x][y] + distances[z][w];
let sum_xz_yw = distances[x][z] + distances[y][w];
let sum_xw_yz = distances[x][w] + distances[y][z];
let max_other = sum_xz_yw.max(sum_xw_yz);
let delta = (sum_xy_zw - max_other) / 2.0;
max_delta = max_delta.max(delta);
}
}
}
}
max_delta
}
#[cfg(test)]
#[allow(clippy::unwrap_used, unused_results, clippy::needless_range_loop)]
mod tests {
use super::*;
#[test]
fn test_ultrametric_tree_basic() {
let merges = vec![
(0, 1, 1.0, 2), (3, 2, 2.0, 3), ];
let tree = UltrametricTree::from_merges(&merges, 3);
assert!((tree.distance(0, 1) - 1.0).abs() < 1e-10); assert!((tree.distance(0, 2) - 2.0).abs() < 1e-10); assert!((tree.distance(1, 2) - 2.0).abs() < 1e-10);
assert!(tree.verify_ultrametric());
}
#[test]
fn test_subdominant_ultrametric() {
let distances = vec![
vec![0.0, 1.0, 2.0],
vec![1.0, 0.0, 1.5],
vec![2.0, 1.5, 0.0],
];
let u = subdominant_ultrametric(&distances);
assert!(is_ultrametric(&u, 1e-10));
for i in 0..3 {
for j in 0..3 {
assert!(u[i][j] <= distances[i][j] + 1e-10);
}
}
}
#[test]
fn test_gromov_hyperbolicity_tree() {
let tree =
UltrametricTree::from_merges(&[(0, 1, 1.0, 2), (2, 3, 1.0, 2), (4, 5, 2.0, 4)], 4);
let mut distances = vec![vec![0.0; 4]; 4];
for i in 0..4 {
for j in 0..4 {
distances[i][j] = tree.distance(i, j);
}
}
let delta = gromov_hyperbolicity(&distances);
assert!(delta < 1e-10, "Tree should be 0-hyperbolic, got {}", delta);
}
#[test]
fn test_is_ultrametric() {
let ultra = vec![
vec![0.0, 1.0, 2.0],
vec![1.0, 0.0, 2.0],
vec![2.0, 2.0, 0.0],
];
assert!(is_ultrametric(&ultra, 1e-10));
let non_ultra = vec![
vec![0.0, 1.0, 3.0],
vec![1.0, 0.0, 2.0],
vec![3.0, 2.0, 0.0],
];
assert!(!is_ultrametric(&non_ultra, 1e-10));
}
}