1use crate::algorithms::four_d::GraphNode4D;
2use std::collections::{HashMap, HashSet, VecDeque};
3
4#[derive(Debug, Clone)]
6pub struct CausalInterval {
7 pub src: u64,
8 pub dst: u64,
9 pub volume: usize,
11 pub proper_time: u32,
13}
14
15#[derive(Debug, Clone)]
17pub struct CausalStats {
18 pub n_nodes: usize,
19 pub n_related_pairs: usize,
20 pub mm_dimension: f32,
23 pub mean_volume: f32,
24 pub mean_proper_time: f32,
25}
26
27fn build_causal_adj(nodes: &[GraphNode4D]) -> (HashMap<u64, Vec<u64>>, HashMap<u64, Vec<u64>>) {
29 let mut fwd: HashMap<u64, Vec<u64>> = HashMap::with_capacity(nodes.len());
30 let mut rev: HashMap<u64, Vec<u64>> = HashMap::with_capacity(nodes.len());
31 for node in nodes {
32 fwd.entry(node.id).or_default();
33 rev.entry(node.id).or_default();
34 for edge in &node.successors {
35 if edge.end_ts > edge.begin_ts {
36 fwd.entry(node.id).or_default().push(edge.dst);
37 rev.entry(edge.dst).or_default().push(node.id);
38 }
39 }
40 }
41 (fwd, rev)
42}
43
44fn reachable(adj: &HashMap<u64, Vec<u64>>, start: u64) -> HashSet<u64> {
46 if !adj.contains_key(&start) {
47 return HashSet::new();
48 }
49 let mut visited = HashSet::new();
50 let mut queue = VecDeque::from([start]);
51 while let Some(cur) = queue.pop_front() {
52 if !visited.insert(cur) {
53 continue;
54 }
55 if let Some(neighbors) = adj.get(&cur) {
56 for &nb in neighbors {
57 if !visited.contains(&nb) {
58 queue.push_back(nb);
59 }
60 }
61 }
62 }
63 visited
64}
65
66fn longest_path(
69 node_map: &HashMap<u64, &GraphNode4D>,
70 fwd_adj: &HashMap<u64, Vec<u64>>,
71 interval: &HashSet<u64>,
72 src: u64,
73 dst: u64,
74) -> u32 {
75 if src == dst {
76 return 0;
77 }
78 let mut topo: Vec<u64> = interval.iter().cloned().collect();
79 topo.sort_by_key(|&id| node_map.get(&id).map(|n| n.begin_ts).unwrap_or(0));
80
81 let mut dist: HashMap<u64, u32> = HashMap::new();
82 dist.insert(src, 0);
83
84 for &u in &topo {
85 let d_u = match dist.get(&u) {
86 Some(&d) => d,
87 None => continue,
88 };
89 if let Some(neighbors) = fwd_adj.get(&u) {
90 for &v in neighbors {
91 if interval.contains(&v) {
92 let e = dist.entry(v).or_insert(0);
93 if d_u + 1 > *e {
94 *e = d_u + 1;
95 }
96 }
97 }
98 }
99 }
100
101 dist.get(&dst).copied().unwrap_or(0)
102}
103
104pub fn causal_intervals(nodes: &[GraphNode4D]) -> Vec<CausalInterval> {
110 let node_map: HashMap<u64, &GraphNode4D> = nodes.iter().map(|n| (n.id, n)).collect();
111 let (fwd_adj, rev_adj) = build_causal_adj(nodes);
112
113 let fwd_reach: HashMap<u64, HashSet<u64>> = nodes
114 .iter()
115 .map(|n| (n.id, reachable(&fwd_adj, n.id)))
116 .collect();
117 let bwd_reach: HashMap<u64, HashSet<u64>> = nodes
118 .iter()
119 .map(|n| (n.id, reachable(&rev_adj, n.id)))
120 .collect();
121
122 let mut result = Vec::new();
123 for node in nodes {
124 let u = node.id;
125 let Some(fwd_u) = fwd_reach.get(&u) else {
126 continue;
127 };
128 for &v in fwd_u {
129 if v == u {
130 continue;
131 }
132 let Some(bwd_v) = bwd_reach.get(&v) else {
133 continue;
134 };
135 let interval: HashSet<u64> = fwd_u.intersection(bwd_v).cloned().collect();
136 let volume = interval.len();
137 let tau = longest_path(&node_map, &fwd_adj, &interval, u, v);
138 result.push(CausalInterval {
139 src: u,
140 dst: v,
141 volume,
142 proper_time: tau,
143 });
144 }
145 }
146 result
147}
148
149pub fn causal_stats(nodes: &[GraphNode4D]) -> CausalStats {
151 let intervals = causal_intervals(nodes);
152 let n_nodes = nodes.len();
153 let n_related_pairs = intervals.len();
154
155 if intervals.is_empty() {
156 return CausalStats {
157 n_nodes,
158 n_related_pairs: 0,
159 mm_dimension: f32::NAN,
160 mean_volume: 0.0,
161 mean_proper_time: 0.0,
162 };
163 }
164
165 let mean_volume =
166 intervals.iter().map(|i| i.volume as f32).sum::<f32>() / n_related_pairs as f32;
167 let mean_proper_time =
168 intervals.iter().map(|i| i.proper_time as f32).sum::<f32>() / n_related_pairs as f32;
169
170 let long_pairs: Vec<(f32, f32)> = intervals
173 .iter()
174 .filter(|i| i.proper_time >= 2)
175 .map(|i| ((i.proper_time as f32).ln(), (i.volume as f32).ln()))
176 .collect();
177
178 let mm_dimension = if long_pairs.len() < 2 {
179 f32::NAN
180 } else {
181 let n = long_pairs.len() as f32;
182 let mean_x = long_pairs.iter().map(|(x, _)| x).sum::<f32>() / n;
183 let mean_y = long_pairs.iter().map(|(_, y)| y).sum::<f32>() / n;
184 let cov = long_pairs
185 .iter()
186 .map(|(x, y)| (x - mean_x) * (y - mean_y))
187 .sum::<f32>();
188 let var_x = long_pairs
189 .iter()
190 .map(|(x, _)| (x - mean_x).powi(2))
191 .sum::<f32>();
192 if var_x < 1e-10 {
193 f32::NAN
194 } else {
195 cov / var_x
196 }
197 };
198
199 CausalStats {
200 n_nodes,
201 n_related_pairs,
202 mm_dimension,
203 mean_volume,
204 mean_proper_time,
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
212 use crate::algorithms::tnet4d::build_tnet4d;
213
214 fn temporal_edge(dst: u64, begin_ts: u64, end_ts: u64) -> TemporalEdge {
215 TemporalEdge {
216 dst,
217 weight: 1.0,
218 begin_ts,
219 end_ts,
220 }
221 }
222
223 fn spatial_edge(dst: u64, ts: u64) -> TemporalEdge {
224 TemporalEdge {
225 dst,
226 weight: 1.0,
227 begin_ts: ts,
228 end_ts: ts,
229 }
230 }
231
232 fn node(id: u64, t: u64, succs: Vec<TemporalEdge>) -> GraphNode4D {
233 GraphNode4D {
234 id,
235 x: id as f32,
236 y: 0.0,
237 z: 0.0,
238 begin_ts: t,
239 end_ts: t + 1,
240 properties: GraphProperties::default(),
241 successors: succs,
242 }
243 }
244
245 fn make_chain(n: usize) -> Vec<GraphNode4D> {
246 (0..n as u64)
247 .map(|i| {
248 let succs = if i + 1 < n as u64 {
249 vec![temporal_edge(i + 1, i, i + 1)]
250 } else {
251 vec![]
252 };
253 node(i, i, succs)
254 })
255 .collect()
256 }
257
258 #[test]
261 fn test_reachable_single_chain() {
262 let nodes = make_chain(4);
264 let (fwd, _) = build_causal_adj(&nodes);
265 let reach = reachable(&fwd, 0);
266 assert!(reach.contains(&1), "0 must reach 1");
267 assert!(reach.contains(&2), "0 must reach 2");
268 assert!(reach.contains(&3), "0 must reach 3");
269 assert!(!reach.contains(&4), "4 does not exist");
270 }
271
272 #[test]
273 fn test_reachable_missing_start_is_empty() {
274 let nodes: Vec<GraphNode4D> = vec![];
276 let (fwd, _) = build_causal_adj(&nodes);
277 let reach = reachable(&fwd, 42);
278 assert!(reach.is_empty(), "missing start → empty reachable set");
279 }
280
281 #[test]
284 fn test_interval_chain_inclusive() {
285 let nodes = make_chain(4);
287 let intervals = causal_intervals(&nodes);
288 let e = intervals
289 .iter()
290 .find(|i| i.src == 0 && i.dst == 3)
291 .expect("interval (0,3) must exist");
292 assert_eq!(e.volume, 4, "I[0,3] volume must be 4");
293 assert_eq!(e.proper_time, 3, "proper_time(0,3) must be 3");
294 }
295
296 #[test]
297 fn test_interval_direct_edge() {
298 let nodes = make_chain(2);
300 let intervals = causal_intervals(&nodes);
301 assert_eq!(intervals.len(), 1);
302 assert_eq!(intervals[0].volume, 2);
303 assert_eq!(intervals[0].proper_time, 1);
304 }
305
306 #[test]
307 fn test_interval_diamond() {
308 let nodes = vec![
310 node(0, 0, vec![temporal_edge(1, 0, 1), temporal_edge(2, 0, 1)]),
311 node(1, 1, vec![temporal_edge(3, 1, 2)]),
312 node(2, 1, vec![temporal_edge(3, 1, 2)]),
313 node(3, 2, vec![]),
314 ];
315 let intervals = causal_intervals(&nodes);
316 let e = intervals
317 .iter()
318 .find(|i| i.src == 0 && i.dst == 3)
319 .expect("interval (0,3) must exist");
320 assert_eq!(e.volume, 4, "I[0,3] volume must be 4");
321 assert_eq!(e.proper_time, 2, "proper_time(0,3) = 2 hops");
322 }
323
324 #[test]
325 fn test_spatial_edges_ignored() {
326 let nodes = vec![
328 node(0, 0, vec![spatial_edge(1, 0)]),
329 node(1, 0, vec![spatial_edge(0, 0)]),
330 ];
331 let intervals = causal_intervals(&nodes);
332 assert!(
333 intervals.is_empty(),
334 "spatial-only graph must produce no causal intervals"
335 );
336 }
337
338 #[test]
339 fn test_causal_intervals_count_chain() {
340 let nodes = make_chain(5);
342 let intervals = causal_intervals(&nodes);
343 assert_eq!(
344 intervals.len(),
345 10,
346 "5-node chain must yield 10 causal intervals"
347 );
348 }
349
350 #[test]
353 fn test_mm_dimension_chain_approx_one() {
354 let nodes = make_chain(10);
356 let stats = causal_stats(&nodes);
357 assert!(
358 stats.mm_dimension >= 0.5 && stats.mm_dimension <= 1.5,
359 "1D chain MM dimension expected in [0.5,1.5], got {}",
360 stats.mm_dimension
361 );
362 }
363
364 #[test]
365 fn test_causal_stats_tnet4d_single_site() {
366 let nodes = build_tnet4d(1, 1, 1, 5);
368 let stats = causal_stats(&nodes);
369 assert_eq!(stats.n_nodes, 5);
370 assert_eq!(stats.n_related_pairs, 10, "5-node chain → 10 pairs");
371 assert!(
372 stats.mm_dimension.is_finite(),
373 "MM dimension must be finite"
374 );
375 assert!(
376 stats.mm_dimension >= 0.5 && stats.mm_dimension <= 1.5,
377 "single-site temporal chain should be ~1D, got {}",
378 stats.mm_dimension
379 );
380 }
381
382 #[test]
383 fn test_causal_intervals_tnet4d_parallel() {
384 let nodes = build_tnet4d(2, 2, 1, 3);
386 let intervals = causal_intervals(&nodes);
387 assert_eq!(
388 intervals.len(),
389 12,
390 "4 parallel 3-chains → 12 causal intervals"
391 );
392 }
393}