Skip to main content

drft/analyses/
betweenness.rs

1use super::{Analysis, AnalysisContext};
2use std::collections::{HashMap, VecDeque};
3
4#[derive(Debug, Clone, serde::Serialize)]
5pub struct NodeBetweenness {
6    pub node: String,
7    pub score: f64,
8}
9
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct BetweennessResult {
12    pub nodes: Vec<NodeBetweenness>,
13}
14
15pub struct Betweenness;
16
17impl Analysis for Betweenness {
18    type Output = BetweennessResult;
19
20    fn name(&self) -> &str {
21        "betweenness"
22    }
23
24    fn run(&self, ctx: &AnalysisContext) -> BetweennessResult {
25        let graph = ctx.graph;
26        let real_nodes: Vec<&str> = graph.included_nodes().map(|(s, _)| s.as_str()).collect();
27
28        let n = real_nodes.len();
29        let mut centrality: HashMap<&str, f64> = HashMap::new();
30        for &node in &real_nodes {
31            centrality.insert(node, 0.0);
32        }
33
34        if n <= 2 {
35            let nodes = real_nodes
36                .iter()
37                .map(|&node| NodeBetweenness {
38                    node: node.to_string(),
39                    score: 0.0,
40                })
41                .collect();
42            let mut result = BetweennessResult { nodes };
43            result.nodes.sort_by(|a, b| a.node.cmp(&b.node));
44            return result;
45        }
46
47        // Brandes' algorithm (directed)
48        for &s in &real_nodes {
49            // BFS from s
50            let mut stack: Vec<&str> = Vec::new();
51            let mut predecessors: HashMap<&str, Vec<&str>> = HashMap::new();
52            let mut sigma: HashMap<&str, f64> = HashMap::new();
53            let mut dist: HashMap<&str, i64> = HashMap::new();
54
55            for &node in &real_nodes {
56                predecessors.insert(node, Vec::new());
57                sigma.insert(node, 0.0);
58                dist.insert(node, -1);
59            }
60
61            sigma.insert(s, 1.0);
62            dist.insert(s, 0);
63
64            let mut queue = VecDeque::new();
65            queue.push_back(s);
66
67            while let Some(v) = queue.pop_front() {
68                stack.push(v);
69                let v_dist = dist[v];
70
71                if let Some(edge_indices) = graph.forward.get(v) {
72                    for &idx in edge_indices {
73                        let edge = &graph.edges[idx];
74                        if !graph.is_internal_edge(edge) {
75                            continue;
76                        }
77                        let w = edge.target.as_str();
78                        if dist[w] < 0 {
79                            dist.insert(w, v_dist + 1);
80                            queue.push_back(w);
81                        }
82                        if dist[w] == v_dist + 1 {
83                            *sigma.get_mut(w).unwrap() += sigma[v];
84                            predecessors.get_mut(w).unwrap().push(v);
85                        }
86                    }
87                }
88            }
89
90            // Back-propagation
91            let mut delta: HashMap<&str, f64> = HashMap::new();
92            for &node in &real_nodes {
93                delta.insert(node, 0.0);
94            }
95
96            while let Some(w) = stack.pop() {
97                for &v in &predecessors[w] {
98                    let d = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
99                    *delta.get_mut(v).unwrap() += d;
100                }
101                if w != s {
102                    *centrality.get_mut(w).unwrap() += delta[w];
103                }
104            }
105        }
106
107        // Normalize by (n-1)*(n-2) for directed graphs
108        let norm = ((n - 1) * (n - 2)) as f64;
109        let mut nodes: Vec<NodeBetweenness> = centrality
110            .into_iter()
111            .map(|(node, score)| NodeBetweenness {
112                node: node.to_string(),
113                score: score / norm,
114            })
115            .collect();
116
117        // Sort by score descending, then node ascending
118        nodes.sort_by(|a, b| {
119            b.score
120                .partial_cmp(&a.score)
121                .unwrap()
122                .then_with(|| a.node.cmp(&b.node))
123        });
124
125        BetweennessResult { nodes }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::analyses::AnalysisContext;
133    use crate::config::Config;
134    use crate::graph::Graph;
135    use crate::graph::test_helpers::{make_edge, make_node};
136    use std::path::Path;
137
138    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> AnalysisContext<'a> {
139        AnalysisContext {
140            graph,
141            root: Path::new("."),
142            config,
143            lockfile: None,
144        }
145    }
146
147    #[test]
148    fn hub_node() {
149        let mut graph = Graph::new();
150        graph.add_node(make_node("a.md"));
151        graph.add_node(make_node("b.md"));
152        graph.add_node(make_node("c.md"));
153        graph.add_node(make_node("center.md"));
154        graph.add_node(make_node("d.md"));
155        graph.add_edge(make_edge("a.md", "center.md"));
156        graph.add_edge(make_edge("c.md", "center.md"));
157        graph.add_edge(make_edge("center.md", "b.md"));
158        graph.add_edge(make_edge("center.md", "d.md"));
159
160        let config = Config::defaults();
161        let result = Betweenness.run(&make_ctx(&graph, &config));
162        assert_eq!(result.nodes[0].node, "center.md");
163        assert!(result.nodes[0].score > 0.0);
164    }
165
166    #[test]
167    fn linear_chain() {
168        let mut graph = Graph::new();
169        graph.add_node(make_node("a.md"));
170        graph.add_node(make_node("b.md"));
171        graph.add_node(make_node("c.md"));
172        graph.add_node(make_node("d.md"));
173        graph.add_edge(make_edge("a.md", "b.md"));
174        graph.add_edge(make_edge("b.md", "c.md"));
175        graph.add_edge(make_edge("c.md", "d.md"));
176
177        let config = Config::defaults();
178        let result = Betweenness.run(&make_ctx(&graph, &config));
179        let b = result.nodes.iter().find(|n| n.node == "b.md").unwrap();
180        let c = result.nodes.iter().find(|n| n.node == "c.md").unwrap();
181        let a = result.nodes.iter().find(|n| n.node == "a.md").unwrap();
182        assert!(b.score > a.score);
183        assert!(c.score > a.score);
184    }
185
186    #[test]
187    fn single_node() {
188        let mut graph = Graph::new();
189        graph.add_node(make_node("a.md"));
190
191        let config = Config::defaults();
192        let result = Betweenness.run(&make_ctx(&graph, &config));
193        assert_eq!(result.nodes.len(), 1);
194        assert_eq!(result.nodes[0].score, 0.0);
195    }
196
197    #[test]
198    fn empty_graph() {
199        let graph = Graph::new();
200        let config = Config::defaults();
201        let result = Betweenness.run(&make_ctx(&graph, &config));
202        assert!(result.nodes.is_empty());
203    }
204}