use hyperball::PoincareBall;
use ndarray::{array, Array1};
fn main() {
println!("Tree Embedding in Hyperbolic Space");
println!("===================================\n");
let ball = PoincareBall::<f64>::new(1.0);
println!("Tree structure:");
println!(" root");
println!(" / \\");
println!(" A B");
println!(" / \\ / \\");
println!(" A1 A2 B1 B2\n");
let root = array![0.0, 0.0];
let a = array![0.4, 0.3]; let b = array![0.4, -0.3];
let a1 = array![0.7, 0.4];
let a2 = array![0.7, 0.2];
let b1 = array![0.7, -0.2];
let b2 = array![0.7, -0.4];
let nodes = vec![
("root", &root),
("A", &a),
("B", &b),
("A1", &a1),
("A2", &a2),
("B1", &b1),
("B2", &b2),
];
println!("1. Node Positions (Euclidean coordinates)");
for (name, pos) in &nodes {
let dot_product: f64 = pos.dot(*pos);
let norm = dot_product.sqrt();
let d_from_root = ball.distance(&root.view(), &pos.view());
println!(
" {}: {:?}, ||x||={:.2}, d(root)={:.2}",
name,
pos.to_vec(),
norm,
d_from_root
);
}
println!("\n2. Structural Distances");
println!(" Parent-child relationships:");
println!(
" d(root, A) = {:.3}",
ball.distance(&root.view(), &a.view())
);
println!(
" d(root, B) = {:.3}",
ball.distance(&root.view(), &b.view())
);
println!(
" d(A, A1) = {:.3}",
ball.distance(&a.view(), &a1.view())
);
println!(
" d(A, A2) = {:.3}",
ball.distance(&a.view(), &a2.view())
);
println!(
" d(B, B1) = {:.3}",
ball.distance(&b.view(), &b1.view())
);
println!(
" d(B, B2) = {:.3}",
ball.distance(&b.view(), &b2.view())
);
println!("\n Sibling distances:");
println!(
" d(A, B) = {:.3} (level 1 siblings)",
ball.distance(&a.view(), &b.view())
);
println!(
" d(A1, A2) = {:.3} (level 2 siblings, same parent)",
ball.distance(&a1.view(), &a2.view())
);
println!(
" d(B1, B2) = {:.3} (level 2 siblings, same parent)",
ball.distance(&b1.view(), &b2.view())
);
println!("\n Cross-branch distances:");
println!(
" d(A1, B1) = {:.3} (different branches)",
ball.distance(&a1.view(), &b1.view())
);
println!(
" d(A2, B2) = {:.3} (different branches)",
ball.distance(&a2.view(), &b2.view())
);
println!("\n3. Key Property: Paths Through Ancestor");
println!(" In hyperbolic space, shortest paths between nodes in different");
println!(" branches tend to go 'up' through their common ancestor.\n");
let d_a1_b1_direct = ball.distance(&a1.view(), &b1.view());
let d_a1_root = ball.distance(&a1.view(), &root.view());
let d_root_b1 = ball.distance(&root.view(), &b1.view());
println!(" Direct d(A1, B1) = {:.3}", d_a1_b1_direct);
println!(
" Via root: d(A1, root) + d(root, B1) = {:.3} + {:.3} = {:.3}",
d_a1_root,
d_root_b1,
d_a1_root + d_root_b1
);
println!("\n4. Why Low Dimensions Work for Trees");
println!(" A binary tree of depth d has 2^d leaves.");
println!(" In Euclidean space, you'd need ~d dimensions.");
println!(" In hyperbolic space, 2D is often sufficient!\n");
println!(" This is because hyperbolic space has exponential volume growth:");
println!(" Area at radius r grows like sinh(r), not r^2.");
println!("\n5. Euclidean vs Hyperbolic Comparison");
println!(" (Same coordinates, different metrics. See note in source.)");
let euclidean_dist = |a: &Array1<f64>, b: &Array1<f64>| {
let diff = a - b;
diff.dot(&diff).sqrt()
};
println!(" Distance | Euclidean | Hyperbolic | Ratio");
println!(" ---------|-----------|------------|-------");
let pairs = [
("d(root,A)", &root, &a),
("d(root,A1)", &root, &a1),
("d(A,A1)", &a, &a1),
("d(A1,B1)", &a1, &b1),
];
for (name, p1, p2) in pairs {
let d_euc = euclidean_dist(p1, p2);
let d_hyp = ball.distance(&p1.view(), &p2.view());
println!(
" {:10} | {:9.3} | {:10.3} | {:6.2}x",
name,
d_euc,
d_hyp,
d_hyp / d_euc
);
}
println!("\n Notice: hyperbolic distances grow faster for points near boundary.");
println!(" This naturally encodes the tree hierarchy!");
println!("\nDone!");
}