facett_graphview/analysis/
centrality.rs1use std::collections::VecDeque;
5
6use super::Adjacency;
7
8#[must_use]
11pub fn degree(g: &Adjacency) -> Vec<f32> {
12 let denom = (g.n.saturating_sub(1)).max(1) as f32;
13 (0..g.n).map(|i| g.degree(i) as f32 / denom).collect()
14}
15
16#[must_use]
18pub fn in_out_degree(g: &Adjacency) -> (Vec<f32>, Vec<f32>) {
19 let denom = (g.n.saturating_sub(1)).max(1) as f32;
20 let ins = (0..g.n).map(|i| g.in_degree(i) as f32 / denom).collect();
21 let outs = (0..g.n).map(|i| g.out_degree(i) as f32 / denom).collect();
22 (ins, outs)
23}
24
25#[must_use]
29pub fn closeness(g: &Adjacency) -> Vec<f32> {
30 let n = g.n;
31 (0..n)
32 .map(|s| {
33 let dist = bfs_dist(g, s);
34 let mut sum = 0.0f32;
35 let mut reach = 0usize;
36 for (i, &d) in dist.iter().enumerate() {
37 if i != s && d >= 0 {
38 sum += d as f32;
39 reach += 1;
40 }
41 }
42 if sum <= 0.0 || n <= 1 {
43 0.0
44 } else {
45 (reach as f32 / sum) * (reach as f32 / (n - 1) as f32)
47 }
48 })
49 .collect()
50}
51
52#[must_use]
56pub fn betweenness(g: &Adjacency) -> Vec<f32> {
57 let n = g.n;
58 let mut bc = vec![0.0f32; n];
59 for s in 0..n {
60 let mut stack = Vec::new();
62 let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
63 let mut sigma = vec![0.0f32; n];
64 let mut dist = vec![-1i32; n];
65 sigma[s] = 1.0;
66 dist[s] = 0;
67 let mut q = VecDeque::new();
68 q.push_back(s);
69 while let Some(v) = q.pop_front() {
70 stack.push(v);
71 for &(w, _) in &g.und[v] {
72 if dist[w] < 0 {
73 dist[w] = dist[v] + 1;
74 q.push_back(w);
75 }
76 if dist[w] == dist[v] + 1 {
77 sigma[w] += sigma[v];
78 pred[w].push(v);
79 }
80 }
81 }
82 let mut delta = vec![0.0f32; n];
84 while let Some(w) = stack.pop() {
85 for &v in &pred[w] {
86 if sigma[w] > 0.0 {
87 delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]);
88 }
89 }
90 if w != s {
91 bc[w] += delta[w];
92 }
93 }
94 }
95 let norm = if n > 2 { ((n - 1) * (n - 2)) as f32 } else { 1.0 };
97 for x in &mut bc {
98 *x = (*x * 0.5) / norm;
99 }
100 bc
101}
102
103#[must_use]
107pub fn pagerank(g: &Adjacency, d: f32, iters: usize) -> Vec<f32> {
108 let n = g.n;
109 if n == 0 {
110 return Vec::new();
111 }
112 let base = (1.0 - d) / n as f32;
113 let mut rank = vec![1.0f32 / n as f32; n];
114 let out_sum: Vec<f32> = (0..n).map(|i| g.out[i].iter().map(|(_, w)| *w).sum::<f32>()).collect();
116 for _ in 0..iters {
117 let mut next = vec![base; n];
118 let dangling: f32 = (0..n).filter(|&i| g.out[i].is_empty()).map(|i| rank[i]).sum();
120 let dangle_share = d * dangling / n as f32;
121 for v in &mut next {
122 *v += dangle_share;
123 }
124 for i in 0..n {
125 if out_sum[i] <= 0.0 {
126 continue;
127 }
128 for &(j, w) in &g.out[i] {
129 next[j] += d * rank[i] * (w / out_sum[i]);
130 }
131 }
132 rank = next;
133 }
134 let total: f32 = rank.iter().sum();
136 if total > 0.0 {
137 for v in &mut rank {
138 *v /= total;
139 }
140 }
141 rank
142}
143
144#[must_use]
150pub fn eigenvector(g: &Adjacency, iters: usize) -> Vec<f32> {
151 let n = g.n;
152 if n == 0 {
153 return Vec::new();
154 }
155 if (0..n).all(|i| g.und[i].is_empty()) {
157 return vec![0.0; n];
158 }
159 let mut x = vec![1.0f32 / (n as f32).sqrt(); n];
160 for _ in 0..iters {
161 let mut next = x.clone();
163 for i in 0..n {
164 for &(j, w) in &g.und[i] {
165 next[i] += w * x[j];
166 }
167 }
168 let norm = next.iter().map(|v| v * v).sum::<f32>().sqrt();
169 if norm <= f32::EPSILON {
170 return vec![0.0; n];
171 }
172 for v in &mut next {
173 *v /= norm;
174 }
175 x = next;
176 }
177 if x.iter().sum::<f32>() < 0.0 {
179 for v in &mut x {
180 *v = -*v;
181 }
182 }
183 x
184}
185
186fn bfs_dist(g: &Adjacency, s: usize) -> Vec<i32> {
188 let mut dist = vec![-1i32; g.n];
189 let mut q = VecDeque::new();
190 dist[s] = 0;
191 q.push_back(s);
192 while let Some(v) = q.pop_front() {
193 for &(w, _) in &g.und[v] {
194 if dist[w] < 0 {
195 dist[w] = dist[v] + 1;
196 q.push_back(w);
197 }
198 }
199 }
200 dist
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 fn path5() -> Adjacency {
209 Adjacency::from_edges(5, &[(0, 1), (1, 2), (2, 3), (3, 4)])
210 }
211
212 fn star5() -> Adjacency {
214 Adjacency::from_edges(5, &[(0, 1), (0, 2), (0, 3), (0, 4)])
215 }
216
217 #[test]
218 fn degree_peaks_at_the_star_centre() {
219 let d = degree(&star5());
220 assert!((d[0] - 1.0).abs() < 1e-6, "the hub is adjacent to everyone");
221 for i in 1..5 {
222 assert!((d[i] - 0.25).abs() < 1e-6, "a leaf touches only the hub");
223 }
224 }
225
226 #[test]
227 fn betweenness_peaks_at_the_path_middle() {
228 let b = betweenness(&path5());
229 assert!(b[2] > b[1] && b[1] > b[0], "betweenness rises toward the centre: {b:?}");
231 assert!((b[0]).abs() < 1e-6 && (b[4]).abs() < 1e-6, "endpoints are on no through-path");
232 }
233
234 #[test]
235 fn closeness_peaks_at_the_star_centre() {
236 let c = closeness(&star5());
237 for i in 1..5 {
238 assert!(c[0] > c[i], "the hub is closest to all: {c:?}");
239 }
240 }
241
242 #[test]
243 fn pagerank_is_a_distribution_and_favours_a_sink() {
244 let g = Adjacency::from_edges(4, &[(1, 0), (2, 0), (3, 0)]);
246 let pr = pagerank(&g, 0.85, 50);
247 let sum: f32 = pr.iter().sum();
248 assert!((sum - 1.0).abs() < 1e-3, "pagerank sums to 1 (got {sum})");
249 assert!(pr[0] > pr[1] && pr[0] > pr[2] && pr[0] > pr[3], "the sink ranks highest: {pr:?}");
250 }
251
252 #[test]
253 fn eigenvector_peaks_at_the_star_centre() {
254 let e = eigenvector(&star5(), 100);
255 for i in 1..5 {
256 assert!(e[0] > e[i], "the hub dominates the eigenvector: {e:?}");
257 }
258 assert!(e.iter().all(|&v| v >= -1e-6));
260 }
261
262 #[test]
263 fn edgeless_graphs_do_not_panic() {
264 let g = Adjacency::from_edges(3, &[]);
265 assert_eq!(betweenness(&g), vec![0.0; 3]);
266 assert_eq!(closeness(&g), vec![0.0; 3]);
267 assert_eq!(eigenvector(&g, 10), vec![0.0; 3]);
268 let pr = pagerank(&g, 0.85, 10);
269 assert!((pr.iter().sum::<f32>() - 1.0).abs() < 1e-3);
270 }
271}