1use std::collections::VecDeque;
2
3use crate::compatibility::compatible;
4use crate::config::SimulationConfig;
5use crate::event::StructuralEvent;
6use crate::metrics::component_entropy;
7
8#[derive(Clone, Debug)]
9pub struct CandidateEdge {
10 pub src: usize,
11 pub dst: usize,
12 pub compatible: bool,
13}
14
15#[derive(Clone, Debug)]
16pub struct CandidateGraph {
17 pub n_events: usize,
18 pub outgoing: Vec<Vec<usize>>,
19 pub candidate_edges: Vec<CandidateEdge>,
20}
21
22#[derive(Clone, Copy, Debug, Default)]
23pub struct DirectedGraphStats {
24 pub reachable_count: usize,
25 pub edge_count: usize,
26 pub mean_out_degree: f64,
27 pub largest_component_fraction: f64,
28 pub component_entropy: f64,
29}
30
31pub fn build_candidate_graph(
32 events: &[StructuralEvent],
33 config: &SimulationConfig,
34) -> CandidateGraph {
35 let mut outgoing = vec![Vec::new(); events.len()];
36 let mut candidate_edges = Vec::new();
37
38 for src in 0..events.len() {
39 let upper = (src + config.causal_window + 1).min(events.len());
40 for dst in (src + 1)..upper {
41 if compatible(&events[src], &events[dst], config.n_channels) {
42 let edge_index = candidate_edges.len();
43 candidate_edges.push(CandidateEdge {
44 src,
45 dst,
46 compatible: true,
47 });
48 outgoing[src].push(edge_index);
49 }
50 }
51 }
52
53 CandidateGraph {
54 n_events: events.len(),
55 outgoing,
56 candidate_edges,
57 }
58}
59
60pub fn compute_graph_stats(
61 candidate_graph: &CandidateGraph,
62 events: &[StructuralEvent],
63 tau_threshold: f64,
64 anchor: usize,
65) -> DirectedGraphStats {
66 compute_graph_stats_in_range(
67 candidate_graph,
68 events,
69 tau_threshold,
70 anchor,
71 (0, candidate_graph.n_events),
72 )
73}
74
75pub fn compute_graph_stats_in_range(
76 candidate_graph: &CandidateGraph,
77 events: &[StructuralEvent],
78 tau_threshold: f64,
79 anchor: usize,
80 range: (usize, usize),
81) -> DirectedGraphStats {
82 let (start, end) = range;
83 if start >= end || end > candidate_graph.n_events || anchor < start || anchor >= end {
84 return DirectedGraphStats::default();
85 }
86
87 let window_len = end - start;
88 let mut reachable = vec![false; candidate_graph.n_events];
89 let mut queue = VecDeque::new();
90 reachable[anchor] = true;
91 queue.push_back(anchor);
92
93 while let Some(node) = queue.pop_front() {
94 if !source_is_active(events, node, tau_threshold) {
95 continue;
96 }
97
98 for &edge_index in &candidate_graph.outgoing[node] {
99 let edge = &candidate_graph.candidate_edges[edge_index];
100 if edge.dst < start || edge.dst >= end {
101 continue;
102 }
103 if !reachable[edge.dst] {
104 reachable[edge.dst] = true;
105 queue.push_back(edge.dst);
106 }
107 }
108 }
109
110 let reachable_count = (start..end).filter(|&event_id| reachable[event_id]).count();
111 let (undirected, edge_count) =
112 thresholded_weak_adjacency(candidate_graph, events, tau_threshold, (start, end));
113 let component_sizes = weak_component_sizes(&undirected);
114
115 DirectedGraphStats {
116 reachable_count,
117 edge_count,
118 mean_out_degree: edge_count as f64 / window_len as f64,
119 largest_component_fraction: largest_component_fraction(&component_sizes, window_len),
120 component_entropy: component_entropy(&component_sizes, window_len),
121 }
122}
123
124pub fn collect_active_edges(
125 candidate_graph: &CandidateGraph,
126 events: &[StructuralEvent],
127 tau_threshold: f64,
128 range: Option<(usize, usize)>,
129) -> Vec<CandidateEdge> {
130 let (start, end) = range.unwrap_or((0, candidate_graph.n_events));
131 let mut active_edges = Vec::new();
132
133 for src in start..end {
134 if !source_is_active(events, src, tau_threshold) {
135 continue;
136 }
137 for &edge_index in &candidate_graph.outgoing[src] {
138 let edge = &candidate_graph.candidate_edges[edge_index];
139 if edge.dst < start || edge.dst >= end {
140 continue;
141 }
142 active_edges.push(edge.clone());
143 }
144 }
145
146 active_edges
147}
148
149pub fn weak_component_sizes(undirected: &[Vec<usize>]) -> Vec<usize> {
150 if undirected.is_empty() {
151 return Vec::new();
152 }
153
154 let mut visited = vec![false; undirected.len()];
155 let mut component_sizes = Vec::new();
156
157 for node in 0..undirected.len() {
158 if visited[node] {
159 continue;
160 }
161
162 let mut queue = VecDeque::new();
163 queue.push_back(node);
164 visited[node] = true;
165 let mut size = 0usize;
166
167 while let Some(current) = queue.pop_front() {
168 size += 1;
169 for &next in &undirected[current] {
170 if !visited[next] {
171 visited[next] = true;
172 queue.push_back(next);
173 }
174 }
175 }
176
177 component_sizes.push(size);
178 }
179
180 component_sizes
181}
182
183fn thresholded_weak_adjacency(
184 candidate_graph: &CandidateGraph,
185 events: &[StructuralEvent],
186 tau_threshold: f64,
187 range: (usize, usize),
188) -> (Vec<Vec<usize>>, usize) {
189 let (start, end) = range;
190 let mut edge_count = 0usize;
191 let mut undirected = vec![Vec::new(); end.saturating_sub(start)];
192
193 for src in start..end {
194 if !source_is_active(events, src, tau_threshold) {
195 continue;
196 }
197
198 for &edge_index in &candidate_graph.outgoing[src] {
199 let edge = &candidate_graph.candidate_edges[edge_index];
200 if edge.dst < start || edge.dst >= end {
201 continue;
202 }
203
204 edge_count += 1;
205 let src_local = src - start;
206 let dst_local = edge.dst - start;
207 undirected[src_local].push(dst_local);
208 undirected[dst_local].push(src_local);
209 }
210 }
211
212 (undirected, edge_count)
213}
214
215fn largest_component_fraction(component_sizes: &[usize], total_nodes: usize) -> f64 {
216 if total_nodes == 0 {
217 return 0.0;
218 }
219
220 component_sizes.iter().copied().max().unwrap_or(0) as f64 / total_nodes as f64
221}
222
223fn source_is_active(events: &[StructuralEvent], event_id: usize, tau_threshold: f64) -> bool {
224 events[event_id].trust >= tau_threshold
225}
226
227#[cfg(test)]
228mod tests {
229 use super::weak_component_sizes;
230
231 #[test]
232 fn weak_component_sizes_include_isolates() {
233 let undirected = vec![vec![1], vec![0], vec![], vec![4], vec![3]];
234 let mut sizes = weak_component_sizes(&undirected);
235 sizes.sort_unstable();
236 assert_eq!(sizes, vec![1, 2, 2]);
237 }
238}