drft/analyses/
betweenness.rs1use 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 for &s in &real_nodes {
49 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 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 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 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}