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
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 for &s in &real_nodes {
54 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 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 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 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}