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_file_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 w = graph.edges[idx].target.as_str();
79 if !graph.is_file_node(w) {
80 continue;
81 }
82 if dist[w] < 0 {
83 dist.insert(w, v_dist + 1);
84 queue.push_back(w);
85 }
86 if dist[w] == v_dist + 1 {
87 *sigma.get_mut(w).unwrap() += sigma[v];
88 predecessors.get_mut(w).unwrap().push(v);
89 }
90 }
91 }
92 }
93
94 let mut delta: HashMap<&str, f64> = HashMap::new();
96 for &node in &real_nodes {
97 delta.insert(node, 0.0);
98 }
99
100 while let Some(w) = stack.pop() {
101 for &v in &predecessors[w] {
102 let d = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
103 *delta.get_mut(v).unwrap() += d;
104 }
105 if w != s {
106 *centrality.get_mut(w).unwrap() += delta[w];
107 }
108 }
109 }
110
111 let norm = ((n - 1) * (n - 2)) as f64;
113 let mut nodes: Vec<NodeBetweenness> = centrality
114 .into_iter()
115 .map(|(node, score)| NodeBetweenness {
116 node: node.to_string(),
117 score: score / norm,
118 })
119 .collect();
120
121 nodes.sort_by(|a, b| {
123 b.score
124 .partial_cmp(&a.score)
125 .unwrap()
126 .then_with(|| a.node.cmp(&b.node))
127 });
128
129 BetweennessResult { nodes }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::analyses::AnalysisContext;
137 use crate::config::Config;
138 use crate::graph::Graph;
139 use crate::graph::test_helpers::{make_edge, make_node};
140 use std::path::Path;
141
142 fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> AnalysisContext<'a> {
143 AnalysisContext {
144 graph,
145 root: Path::new("."),
146 config,
147 lockfile: None,
148 }
149 }
150
151 #[test]
152 fn hub_node() {
153 let mut graph = Graph::new();
154 graph.add_node(make_node("a.md"));
155 graph.add_node(make_node("b.md"));
156 graph.add_node(make_node("c.md"));
157 graph.add_node(make_node("center.md"));
158 graph.add_node(make_node("d.md"));
159 graph.add_edge(make_edge("a.md", "center.md"));
160 graph.add_edge(make_edge("c.md", "center.md"));
161 graph.add_edge(make_edge("center.md", "b.md"));
162 graph.add_edge(make_edge("center.md", "d.md"));
163
164 let config = Config::defaults();
165 let result = Betweenness.run(&make_ctx(&graph, &config));
166 assert_eq!(result.nodes[0].node, "center.md");
167 assert!(result.nodes[0].score > 0.0);
168 }
169
170 #[test]
171 fn linear_chain() {
172 let mut graph = Graph::new();
173 graph.add_node(make_node("a.md"));
174 graph.add_node(make_node("b.md"));
175 graph.add_node(make_node("c.md"));
176 graph.add_node(make_node("d.md"));
177 graph.add_edge(make_edge("a.md", "b.md"));
178 graph.add_edge(make_edge("b.md", "c.md"));
179 graph.add_edge(make_edge("c.md", "d.md"));
180
181 let config = Config::defaults();
182 let result = Betweenness.run(&make_ctx(&graph, &config));
183 let b = result.nodes.iter().find(|n| n.node == "b.md").unwrap();
184 let c = result.nodes.iter().find(|n| n.node == "c.md").unwrap();
185 let a = result.nodes.iter().find(|n| n.node == "a.md").unwrap();
186 assert!(b.score > a.score);
187 assert!(c.score > a.score);
188 }
189
190 #[test]
191 fn single_node() {
192 let mut graph = Graph::new();
193 graph.add_node(make_node("a.md"));
194
195 let config = Config::defaults();
196 let result = Betweenness.run(&make_ctx(&graph, &config));
197 assert_eq!(result.nodes.len(), 1);
198 assert_eq!(result.nodes[0].score, 0.0);
199 }
200
201 #[test]
202 fn empty_graph() {
203 let graph = Graph::new();
204 let config = Config::defaults();
205 let result = Betweenness.run(&make_ctx(&graph, &config));
206 assert!(result.nodes.is_empty());
207 }
208}