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