1use crate::algorithms::four_d::GraphNode4D;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone)]
5pub struct InfoNode {
6 pub id: u64,
7 pub entropy: f32,
8}
9
10#[derive(Debug, Clone)]
11pub struct InfoEdge {
12 pub src: u64,
13 pub dst: u64,
14 pub kl_uv: f32,
16 pub kl_vu: f32,
18 pub js_div: f32,
20 pub fisher_rao: f32,
22 pub entropy_src: f32,
23 pub entropy_dst: f32,
24}
25
26pub fn info_geometry(nodes: &[GraphNode4D], alpha: f32) -> (Vec<InfoNode>, Vec<InfoEdge>) {
35 let node_map: HashMap<u64, &GraphNode4D> = nodes.iter().map(|n| (n.id, n)).collect();
36
37 let info_nodes = nodes
38 .iter()
39 .map(|node| {
40 let mu = build_measure(&node_map, node.id, alpha);
41 InfoNode {
42 id: node.id,
43 entropy: entropy(&mu),
44 }
45 })
46 .collect();
47
48 let mut info_edges = Vec::new();
49 for node in nodes {
50 let u = node.id;
51 for edge in &node.successors {
52 let v = edge.dst;
53 if v <= u {
54 continue; }
56 let mu = build_measure(&node_map, u, alpha);
57 let nu = build_measure(&node_map, v, alpha);
58 info_edges.push(InfoEdge {
59 src: u,
60 dst: v,
61 kl_uv: kl_divergence(&mu, &nu),
62 kl_vu: kl_divergence(&nu, &mu),
63 js_div: js_divergence(&mu, &nu),
64 fisher_rao: fisher_rao_dist(&mu, &nu),
65 entropy_src: entropy(&mu),
66 entropy_dst: entropy(&nu),
67 });
68 }
69 }
70
71 (info_nodes, info_edges)
72}
73
74fn build_measure(node_map: &HashMap<u64, &GraphNode4D>, id: u64, alpha: f32) -> Vec<(u64, f32)> {
76 let node = match node_map.get(&id) {
77 Some(n) => n,
78 None => return vec![(id, 1.0)],
79 };
80 let deg = node.successors.len();
81 if deg == 0 {
82 return vec![(id, 1.0)];
83 }
84 let mut measure = Vec::with_capacity(deg + 1);
85 measure.push((id, alpha));
86 let w = (1.0 - alpha) / deg as f32;
87 for e in &node.successors {
88 measure.push((e.dst, w));
89 }
90 measure
91}
92
93fn entropy(mu: &[(u64, f32)]) -> f32 {
95 mu.iter()
96 .filter(|&&(_, p)| p > 0.0)
97 .map(|&(_, p)| -p * p.ln())
98 .sum()
99}
100
101fn kl_divergence(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
104 let q_map: HashMap<u64, f32> = q.iter().cloned().collect();
105 let mut kl = 0.0_f32;
106 for &(id, pi) in p {
107 if pi <= 0.0 {
108 continue;
109 }
110 let qi = q_map.get(&id).copied().unwrap_or(0.0);
111 if qi <= 0.0 {
112 return f32::INFINITY;
113 }
114 kl += pi * (pi / qi).ln();
115 }
116 kl
117}
118
119fn fisher_rao_dist(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
124 let q_map: HashMap<u64, f32> = q.iter().cloned().collect();
125 let bc: f32 = p
126 .iter()
127 .filter(|&&(_, pi)| pi > 0.0)
128 .map(|&(id, pi)| {
129 let qi = q_map.get(&id).copied().unwrap_or(0.0);
130 (pi * qi).sqrt()
131 })
132 .sum();
133 2.0 * bc.clamp(0.0, 1.0).acos()
134}
135
136fn js_divergence(p: &[(u64, f32)], q: &[(u64, f32)]) -> f32 {
138 let mut m_map: HashMap<u64, f32> = HashMap::new();
139 for &(id, pi) in p {
140 *m_map.entry(id).or_insert(0.0) += pi * 0.5;
141 }
142 for &(id, qi) in q {
143 *m_map.entry(id).or_insert(0.0) += qi * 0.5;
144 }
145 let m: Vec<(u64, f32)> = m_map.into_iter().collect();
146 (entropy(&m) - (entropy(p) + entropy(q)) * 0.5).max(0.0)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
153
154 fn node(id: u64, neighbors: &[u64]) -> GraphNode4D {
155 GraphNode4D {
156 id,
157 x: id as f32,
158 y: 0.0,
159 z: 0.0,
160 begin_ts: 0,
161 end_ts: 0,
162 properties: GraphProperties::default(),
163 successors: neighbors
164 .iter()
165 .map(|&dst| TemporalEdge {
166 dst,
167 weight: 1.0,
168 begin_ts: 0,
169 end_ts: 0,
170 })
171 .collect(),
172 }
173 }
174
175 #[test]
178 fn test_entropy_uniform_four_items() {
179 let mu = vec![(0u64, 0.25), (1, 0.25), (2, 0.25), (3, 0.25)];
181 let h = entropy(&mu);
182 let expected = (4.0_f32).ln();
183 assert!(
184 (h - expected).abs() < 1e-5,
185 "H(uniform4) expected {expected:.6}, got {h:.6}"
186 );
187 }
188
189 #[test]
190 fn test_entropy_point_mass_is_zero() {
191 let mu = vec![(0u64, 1.0)];
192 assert!(entropy(&mu).abs() < 1e-6, "H(point mass) must be 0");
193 }
194
195 #[test]
198 fn test_kl_identical_distributions_zero() {
199 let p = vec![(0u64, 0.5), (1, 0.3), (2, 0.2)];
200 assert!(kl_divergence(&p, &p).abs() < 1e-5, "D_KL(p‖p) must be 0");
201 }
202
203 #[test]
204 fn test_kl_orthogonal_distributions_infinite() {
205 let p = vec![(0u64, 0.5), (1, 0.5)];
206 let q = vec![(2u64, 0.5), (3, 0.5)];
207 assert!(
208 kl_divergence(&p, &q).is_infinite(),
209 "D_KL with disjoint support must be ∞"
210 );
211 }
212
213 #[test]
214 fn test_kl_triangle_measures_positive() {
215 let mu0 = vec![(0u64, 0.5), (1, 0.25), (2, 0.25)];
218 let mu1 = vec![(1u64, 0.5), (0, 0.25), (2, 0.25)];
219 let kl = kl_divergence(&mu0, &mu1);
220 let expected = 0.25 * 2.0_f32.ln(); assert!(
222 (kl - expected).abs() < 1e-4,
223 "D_KL(μ_0‖μ_1) for triangle expected {expected:.4}, got {kl:.4}"
224 );
225 }
226
227 #[test]
230 fn test_fisher_rao_identical_distributions_zero() {
231 let p = vec![(0u64, 0.5), (1, 0.5)];
232 assert!(fisher_rao_dist(&p, &p).abs() < 1e-5, "d_FR(p,p) must be 0");
233 }
234
235 #[test]
236 fn test_fisher_rao_orthogonal_distributions_pi() {
237 let p = vec![(0u64, 1.0)];
239 let q = vec![(1u64, 1.0)];
240 let d = fisher_rao_dist(&p, &q);
241 assert!(
242 (d - std::f32::consts::PI).abs() < 1e-5,
243 "d_FR with disjoint support expected π, got {d:.6}"
244 );
245 }
246
247 #[test]
250 fn test_js_divergence_symmetry() {
251 let p = vec![(0u64, 0.6), (1, 0.4)];
252 let q = vec![(0u64, 0.2), (1, 0.8)];
253 let jspq = js_divergence(&p, &q);
254 let jsqp = js_divergence(&q, &p);
255 assert!(
256 (jspq - jsqp).abs() < 1e-6,
257 "JSD(p,q) and JSD(q,p) must be equal; got {jspq:.6} vs {jsqp:.6}"
258 );
259 }
260
261 #[test]
264 fn test_info_geometry_two_node_identical_measures() {
265 let nodes = vec![node(0, &[1]), node(1, &[0])];
267 let (info_nodes, info_edges) = info_geometry(&nodes, 0.5);
268 assert_eq!(info_nodes.len(), 2);
269 assert_eq!(info_edges.len(), 1);
270 let e = &info_edges[0];
271 assert!(
272 e.kl_uv.abs() < 1e-5,
273 "identical measures → kl_uv=0, got {}",
274 e.kl_uv
275 );
276 assert!(
277 e.fisher_rao.abs() < 1e-5,
278 "identical measures → FR=0, got {}",
279 e.fisher_rao
280 );
281 assert!(
282 e.js_div.abs() < 1e-5,
283 "identical measures → JS=0, got {}",
284 e.js_div
285 );
286 }
287
288 #[test]
289 fn test_info_geometry_triangle_positive_divergences() {
290 let nodes = vec![node(0, &[1, 2]), node(1, &[0, 2]), node(2, &[0, 1])];
291 let (info_nodes, info_edges) = info_geometry(&nodes, 0.5);
292 assert_eq!(info_nodes.len(), 3);
293 assert_eq!(info_edges.len(), 3);
294 for e in &info_edges {
296 assert!(e.kl_uv > 0.0, "triangle kl_uv must be > 0");
297 assert!(e.fisher_rao > 0.0, "triangle fisher_rao must be > 0");
298 assert!(e.fisher_rao < std::f32::consts::PI, "fisher_rao < π");
299 assert!(e.js_div > 0.0, "triangle js_div must be > 0");
300 }
301 for n in &info_nodes {
303 let expected = 1.5 * 2.0_f32.ln();
304 assert!(
305 (n.entropy - expected).abs() < 1e-4,
306 "triangle node entropy expected {expected:.4}, got {:.4}",
307 n.entropy
308 );
309 }
310 }
311}