use crate::algorithms::four_d::GraphNode4D;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct InfoNode {
pub id: u64,
pub entropy: f32,
}
#[derive(Debug, Clone)]
pub struct InfoEdge {
pub src: u64,
pub dst: u64,
pub kl_uv: f32,
pub kl_vu: f32,
pub js_div: f32,
pub fisher_rao: f32,
pub entropy_src: f32,
pub entropy_dst: f32,
}
pub fn info_geometry(nodes: &[GraphNode4D], alpha: f32) -> (Vec<InfoNode>, Vec<InfoEdge>) {
let node_map: HashMap<u64, &GraphNode4D> = nodes.iter().map(|n| (n.id, n)).collect();
let info_nodes = nodes
.iter()
.map(|node| {
let mu = build_measure(&node_map, node.id, alpha);
InfoNode {
id: node.id,
entropy: entropy(&mu),
}
})
.collect();
let mut info_edges = Vec::new();
for node in nodes {
let u = node.id;
for edge in &node.successors {
let v = edge.dst;
if v <= u {
continue; }
let mu = build_measure(&node_map, u, alpha);
let nu = build_measure(&node_map, v, alpha);
info_edges.push(InfoEdge {
src: u,
dst: v,
kl_uv: kl_divergence(&mu, &nu),
kl_vu: kl_divergence(&nu, &mu),
js_div: js_divergence(&mu, &nu),
fisher_rao: fisher_rao_dist(&mu, &nu),
entropy_src: entropy(&mu),
entropy_dst: entropy(&nu),
});
}
}
(info_nodes, info_edges)
}
fn build_measure(node_map: &HashMap<u64, &GraphNode4D>, id: u64, alpha: f32) -> Vec<(u64, f32)> {
let node = match node_map.get(&id) {
Some(n) => n,
None => return vec![(id, 1.0)],
};
let deg = node.successors.len();
if deg == 0 {
return vec![(id, 1.0)];
}
let mut measure = Vec::with_capacity(deg + 1);
measure.push((id, alpha));
let w = (1.0 - alpha) / deg as f32;
for e in &node.successors {
measure.push((e.dst, w));
}
measure
}
fn entropy(mu: &[(u64, f32)]) -> f32 {
mu.iter()
.filter(|&&(_, p)| p > 0.0)
.map(|&(_, p)| -p * p.ln())
.sum()
}
fn kl_divergence(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
let q_map: HashMap<u64, f32> = q.iter().cloned().collect();
let mut kl = 0.0_f32;
for &(id, pi) in p {
if pi <= 0.0 {
continue;
}
let qi = q_map.get(&id).copied().unwrap_or(0.0);
if qi <= 0.0 {
return f32::INFINITY;
}
kl += pi * (pi / qi).ln();
}
kl
}
fn fisher_rao_dist(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
let q_map: HashMap<u64, f32> = q.iter().cloned().collect();
let bc: f32 = p
.iter()
.filter(|&&(_, pi)| pi > 0.0)
.map(|&(id, pi)| {
let qi = q_map.get(&id).copied().unwrap_or(0.0);
(pi * qi).sqrt()
})
.sum();
2.0 * bc.clamp(0.0, 1.0).acos()
}
fn js_divergence(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
let mut m_map: HashMap<u64, f32> = HashMap::new();
for &(id, pi) in p {
*m_map.entry(id).or_insert(0.0) += pi * 0.5;
}
for &(id, qi) in q {
*m_map.entry(id).or_insert(0.0) += qi * 0.5;
}
let m: Vec<(u64, f32)> = m_map.into_iter().collect();
(entropy(&m) - (entropy(p) + entropy(q)) * 0.5).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
fn node(id: u64, neighbors: &[u64]) -> GraphNode4D {
GraphNode4D {
id,
x: id as f32,
y: 0.0,
z: 0.0,
begin_ts: 0,
end_ts: 0,
properties: GraphProperties::default(),
successors: neighbors
.iter()
.map(|&dst| TemporalEdge {
dst,
weight: 1.0,
begin_ts: 0,
end_ts: 0,
})
.collect(),
}
}
#[test]
fn test_entropy_uniform_four_items() {
let mu = vec![(0u64, 0.25), (1, 0.25), (2, 0.25), (3, 0.25)];
let h = entropy(&mu);
let expected = (4.0_f32).ln();
assert!(
(h - expected).abs() < 1e-5,
"H(uniform4) expected {expected:.6}, got {h:.6}"
);
}
#[test]
fn test_entropy_point_mass_is_zero() {
let mu = vec![(0u64, 1.0)];
assert!(entropy(&mu).abs() < 1e-6, "H(point mass) must be 0");
}
#[test]
fn test_kl_identical_distributions_zero() {
let p = vec![(0u64, 0.5), (1, 0.3), (2, 0.2)];
assert!(kl_divergence(&p, &p).abs() < 1e-5, "D_KL(p‖p) must be 0");
}
#[test]
fn test_kl_orthogonal_distributions_infinite() {
let p = vec![(0u64, 0.5), (1, 0.5)];
let q = vec![(2u64, 0.5), (3, 0.5)];
assert!(
kl_divergence(&p, &q).is_infinite(),
"D_KL with disjoint support must be ∞"
);
}
#[test]
fn test_kl_triangle_measures_positive() {
let mu0 = vec![(0u64, 0.5), (1, 0.25), (2, 0.25)];
let mu1 = vec![(1u64, 0.5), (0, 0.25), (2, 0.25)];
let kl = kl_divergence(&mu0, &mu1);
let expected = 0.25 * 2.0_f32.ln(); assert!(
(kl - expected).abs() < 1e-4,
"D_KL(μ_0‖μ_1) for triangle expected {expected:.4}, got {kl:.4}"
);
}
#[test]
fn test_fisher_rao_identical_distributions_zero() {
let p = vec![(0u64, 0.5), (1, 0.5)];
assert!(fisher_rao_dist(&p, &p).abs() < 1e-5, "d_FR(p,p) must be 0");
}
#[test]
fn test_fisher_rao_orthogonal_distributions_pi() {
let p = vec![(0u64, 1.0)];
let q = vec![(1u64, 1.0)];
let d = fisher_rao_dist(&p, &q);
assert!(
(d - std::f32::consts::PI).abs() < 1e-5,
"d_FR with disjoint support expected π, got {d:.6}"
);
}
#[test]
fn test_js_divergence_symmetry() {
let p = vec![(0u64, 0.6), (1, 0.4)];
let q = vec![(0u64, 0.2), (1, 0.8)];
let jspq = js_divergence(&p, &q);
let jsqp = js_divergence(&q, &p);
assert!(
(jspq - jsqp).abs() < 1e-6,
"JSD(p,q) and JSD(q,p) must be equal; got {jspq:.6} vs {jsqp:.6}"
);
}
#[test]
fn test_info_geometry_two_node_identical_measures() {
let nodes = vec![node(0, &[1]), node(1, &[0])];
let (info_nodes, info_edges) = info_geometry(&nodes, 0.5);
assert_eq!(info_nodes.len(), 2);
assert_eq!(info_edges.len(), 1);
let e = &info_edges[0];
assert!(
e.kl_uv.abs() < 1e-5,
"identical measures → kl_uv=0, got {}",
e.kl_uv
);
assert!(
e.fisher_rao.abs() < 1e-5,
"identical measures → FR=0, got {}",
e.fisher_rao
);
assert!(
e.js_div.abs() < 1e-5,
"identical measures → JS=0, got {}",
e.js_div
);
}
#[test]
fn test_info_geometry_triangle_positive_divergences() {
let nodes = vec![node(0, &[1, 2]), node(1, &[0, 2]), node(2, &[0, 1])];
let (info_nodes, info_edges) = info_geometry(&nodes, 0.5);
assert_eq!(info_nodes.len(), 3);
assert_eq!(info_edges.len(), 3);
for e in &info_edges {
assert!(e.kl_uv > 0.0, "triangle kl_uv must be > 0");
assert!(e.fisher_rao > 0.0, "triangle fisher_rao must be > 0");
assert!(e.fisher_rao < std::f32::consts::PI, "fisher_rao < π");
assert!(e.js_div > 0.0, "triangle js_div must be > 0");
}
for n in &info_nodes {
let expected = 1.5 * 2.0_f32.ln();
assert!(
(n.entropy - expected).abs() < 1e-4,
"triangle node entropy expected {expected:.4}, got {:.4}",
n.entropy
);
}
}
}