lean_ctx/core/graph_analysis/
centrality.rs1use std::collections::{HashMap, VecDeque};
9
10use serde::Serialize;
11
12use super::dependency_edges;
13use crate::core::graph_provider::EdgeInfo;
14
15#[derive(Debug, Clone, Serialize, PartialEq)]
17pub struct BridgeNode {
18 pub path: String,
19 pub betweenness: f64,
21}
22
23#[derive(Debug, Clone, Serialize, PartialEq)]
26pub struct BridgeCentrality {
27 pub nodes: Vec<BridgeNode>,
28 pub sampled: bool,
30 pub total_nodes: usize,
32 pub sources_used: usize,
34}
35
36pub fn compute_bridge_nodes(edges: &[EdgeInfo], limit: usize) -> Vec<BridgeNode> {
40 compute_bridge_centrality(edges, limit).nodes
41}
42
43pub fn compute_bridge_centrality(edges: &[EdgeInfo], limit: usize) -> BridgeCentrality {
46 let deps = dependency_edges(edges);
47 if deps.is_empty() {
48 return BridgeCentrality {
49 nodes: Vec::new(),
50 sampled: false,
51 total_nodes: 0,
52 sources_used: 0,
53 };
54 }
55
56 let mut index_of: HashMap<&str, usize> = HashMap::new();
58 let mut names: Vec<&str> = Vec::new();
59 let mut adj: Vec<Vec<usize>> = Vec::new();
60 for (u, v) in &deps {
61 for s in [*u, *v] {
62 if !index_of.contains_key(s) {
63 index_of.insert(s, names.len());
64 names.push(s);
65 adj.push(Vec::new());
66 }
67 }
68 }
69 for (u, v) in &deps {
70 let a = index_of[*u];
71 let b = index_of[*v];
72 adj[a].push(b);
73 adj[b].push(a);
74 }
75 let n = names.len();
76
77 let mut betweenness = vec![0f64; n];
78
79 const EXACT_SOURCE_CAP: usize = 1500;
84 const SAMPLE_SOURCES: usize = 500;
85 let sampled = n > EXACT_SOURCE_CAP;
86 let sources: Vec<usize> = if sampled {
87 let step = (n / SAMPLE_SOURCES).max(1);
88 (0..n).step_by(step).collect()
89 } else {
90 (0..n).collect()
91 };
92 let sources_used = sources.len();
93
94 for &s in &sources {
95 let mut stack: Vec<usize> = Vec::new();
96 let mut preds: Vec<Vec<usize>> = vec![Vec::new(); n];
97 let mut sigma = vec![0f64; n];
98 let mut dist = vec![-1i64; n];
99 sigma[s] = 1.0;
100 dist[s] = 0;
101
102 let mut queue: VecDeque<usize> = VecDeque::new();
103 queue.push_back(s);
104 while let Some(v) = queue.pop_front() {
105 stack.push(v);
106 for &w in &adj[v] {
107 if dist[w] < 0 {
108 dist[w] = dist[v] + 1;
109 queue.push_back(w);
110 }
111 if dist[w] == dist[v] + 1 {
112 sigma[w] += sigma[v];
113 preds[w].push(v);
114 }
115 }
116 }
117
118 let mut delta = vec![0f64; n];
119 while let Some(w) = stack.pop() {
120 let preds_w = std::mem::take(&mut preds[w]);
121 for v in preds_w {
122 if sigma[w] > 0.0 {
123 delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]);
124 }
125 }
126 if w != s {
127 betweenness[w] += delta[w];
128 }
129 }
130 }
131
132 for b in &mut betweenness {
134 *b /= 2.0;
135 }
136 let max = betweenness.iter().copied().fold(0.0f64, f64::max);
137
138 let mut nodes: Vec<BridgeNode> = (0..n)
139 .map(|i| BridgeNode {
140 path: names[i].to_string(),
141 betweenness: if max > 0.0 {
142 (betweenness[i] / max * 1000.0).round() / 1000.0
143 } else {
144 0.0
145 },
146 })
147 .collect();
148
149 nodes.sort_by(|a, b| {
150 b.betweenness
151 .partial_cmp(&a.betweenness)
152 .unwrap_or(std::cmp::Ordering::Equal)
153 .then_with(|| a.path.cmp(&b.path))
154 });
155 nodes.retain(|node| node.betweenness > 0.0);
156 nodes.truncate(limit);
157 BridgeCentrality {
158 nodes,
159 sampled,
160 total_nodes: n,
161 sources_used,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 fn e(from: &str, to: &str) -> EdgeInfo {
170 EdgeInfo {
171 from: from.into(),
172 to: to.into(),
173 kind: "import".into(),
174 weight: 1.0,
175 }
176 }
177
178 #[test]
179 fn path_graph_center_is_top_bridge() {
180 let edges = vec![e("a.rs", "b.rs"), e("b.rs", "c.rs")];
182 let bridges = compute_bridge_nodes(&edges, 10);
183 assert_eq!(bridges.len(), 1); assert_eq!(bridges[0].path, "b.rs");
185 assert_eq!(bridges[0].betweenness, 1.0);
186 }
187
188 #[test]
189 fn star_center_is_top_bridge() {
190 let edges = vec![
191 e("hub.rs", "a.rs"),
192 e("hub.rs", "b.rs"),
193 e("hub.rs", "c.rs"),
194 ];
195 let bridges = compute_bridge_nodes(&edges, 10);
196 assert_eq!(bridges[0].path, "hub.rs");
197 assert_eq!(bridges[0].betweenness, 1.0);
198 }
199
200 #[test]
201 fn fully_connected_has_no_bridges() {
202 let edges = vec![e("a", "b"), e("b", "c"), e("a", "c")];
204 assert!(compute_bridge_nodes(&edges, 10).is_empty());
205 }
206
207 #[test]
208 fn large_graph_uses_sampling_and_still_finds_hub() {
209 let edges: Vec<EdgeInfo> = (0..2000)
212 .map(|i| e("hub.rs", &format!("leaf{i}.rs")))
213 .collect();
214 let bridges = compute_bridge_nodes(&edges, 10);
215 assert_eq!(bridges[0].path, "hub.rs");
216 assert_eq!(bridges[0].betweenness, 1.0);
217 }
218
219 #[test]
220 fn centrality_reports_sampling_provenance() {
221 let small = vec![e("a.rs", "b.rs"), e("b.rs", "c.rs")];
223 let bc = compute_bridge_centrality(&small, 10);
224 assert!(!bc.sampled, "small graph must be computed exactly");
225 assert_eq!(bc.sources_used, bc.total_nodes);
226
227 let big: Vec<EdgeInfo> = (0..2000)
229 .map(|i| e("hub.rs", &format!("leaf{i}.rs")))
230 .collect();
231 let bc = compute_bridge_centrality(&big, 10);
232 assert!(bc.sampled, "large graph must report sampling");
233 assert!(
234 bc.sources_used < bc.total_nodes,
235 "sampling must use fewer sources than nodes"
236 );
237 assert_eq!(bc.nodes[0].path, "hub.rs");
238 }
239}