Skip to main content

dsfb_srd/
graph.rs

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}