weavatrix_graph/algo/network/centrality/
basic.rs1#![allow(clippy::cast_precision_loss)]
2
3use super::super::adjacency::{SlotAdjacency, adjacency};
4use crate::algo::traversal::Direction;
5use crate::{IndexGraphView, Vec};
6use alloc::collections::VecDeque;
7#[cfg(feature = "rayon")]
8use rayon::prelude::*;
9
10#[must_use]
11pub fn degree_centrality<G>(graph: &G, direction: Direction) -> Vec<(G::Node, f64)>
12where
13 G: IndexGraphView,
14{
15 let adjacent = adjacency(graph, direction);
16 let denominator = adjacent.nodes.len().saturating_sub(1) as f64;
17 adjacent
18 .nodes
19 .iter()
20 .copied()
21 .map(|node| {
22 let degree = adjacent.neighbors[G::node_slot(node)].len() as f64;
23 (
24 node,
25 if denominator == 0.0 {
26 0.0
27 } else {
28 degree / denominator
29 },
30 )
31 })
32 .collect()
33}
34
35#[must_use]
36pub fn closeness_centrality<G>(graph: &G, direction: Direction) -> Vec<(G::Node, f64)>
37where
38 G: IndexGraphView,
39{
40 let adjacent = adjacency(graph, direction);
41 adjacent
42 .nodes
43 .iter()
44 .copied()
45 .map(|node| {
46 (
47 node,
48 closeness_from(&adjacent, G::node_slot(node), adjacent.nodes.len()),
49 )
50 })
51 .collect()
52}
53
54#[must_use]
55pub fn betweenness_centrality<G>(
56 graph: &G,
57 direction: Direction,
58 normalized: bool,
59) -> Vec<(G::Node, f64)>
60where
61 G: IndexGraphView,
62{
63 let adjacent = adjacency(graph, direction);
64 let mut scores = vec![0.0; graph.node_bound()];
65 for source in adjacent.nodes.iter().copied().map(G::node_slot) {
66 brandes_source(&adjacent, source, &mut scores);
67 }
68 scale_betweenness(&mut scores, adjacent.nodes.len(), direction, normalized);
69 adjacent
70 .nodes
71 .iter()
72 .copied()
73 .map(|node| (node, scores[G::node_slot(node)]))
74 .collect()
75}
76
77fn scale_betweenness(
78 scores: &mut [f64],
79 node_count: usize,
80 direction: Direction,
81 normalized: bool,
82) {
83 if direction == Direction::Both {
84 for score in &mut *scores {
85 *score /= 2.0;
86 }
87 }
88 if normalized && node_count > 2 {
89 let denominator = ((node_count - 1) * (node_count - 2)) as f64;
90 let scale = if direction == Direction::Both {
91 2.0 / denominator
92 } else {
93 1.0 / denominator
94 };
95 for score in scores {
96 *score *= scale;
97 }
98 }
99}
100
101#[cfg(feature = "rayon")]
102#[must_use]
103pub fn closeness_centrality_parallel<G>(graph: &G, direction: Direction) -> Vec<(G::Node, f64)>
104where
105 G: IndexGraphView,
106 G::Node: Send + Sync,
107{
108 let adjacent = adjacency(graph, direction);
109 adjacent
110 .nodes
111 .par_iter()
112 .map(|&node| {
113 (
114 node,
115 closeness_from(&adjacent, G::node_slot(node), adjacent.nodes.len()),
116 )
117 })
118 .collect()
119}
120
121#[cfg(feature = "rayon")]
122#[must_use]
123pub fn betweenness_centrality_parallel<G>(
124 graph: &G,
125 direction: Direction,
126 normalized: bool,
127) -> Vec<(G::Node, f64)>
128where
129 G: IndexGraphView,
130 G::Node: Send + Sync,
131{
132 let adjacent = adjacency(graph, direction);
133 let bound = graph.node_bound();
134 let sources = adjacent
135 .nodes
136 .iter()
137 .copied()
138 .map(G::node_slot)
139 .collect::<Vec<_>>();
140 let workers = rayon::current_num_threads().max(1);
141 let chunk_size = sources.len().div_ceil(workers).max(1);
142 let partials = sources
143 .par_chunks(chunk_size)
144 .map(|chunk| {
145 let mut scores = vec![0.0; bound];
146 for &source in chunk {
147 brandes_source(&adjacent, source, &mut scores);
148 }
149 scores
150 })
151 .collect::<Vec<_>>();
152 let mut scores = vec![0.0; bound];
153 for partial in partials {
154 for (score, value) in scores.iter_mut().zip(partial) {
155 *score += value;
156 }
157 }
158 scale_betweenness(&mut scores, adjacent.nodes.len(), direction, normalized);
159 adjacent
160 .nodes
161 .iter()
162 .copied()
163 .map(|node| (node, scores[G::node_slot(node)]))
164 .collect()
165}
166
167fn closeness_from<Node>(adjacent: &SlotAdjacency<Node>, source: usize, node_count: usize) -> f64 {
168 let mut distances = vec![usize::MAX; adjacent.neighbors.len()];
169 let mut queue = VecDeque::new();
170 distances[source] = 0;
171 queue.push_back(source);
172 while let Some(node) = queue.pop_front() {
173 for &neighbor in &adjacent.neighbors[node] {
174 if distances[neighbor] == usize::MAX {
175 distances[neighbor] = distances[node] + 1;
176 queue.push_back(neighbor);
177 }
178 }
179 }
180 let reachable = distances
181 .iter()
182 .filter(|distance| **distance != usize::MAX)
183 .count();
184 let total = distances
185 .iter()
186 .filter(|distance| **distance != usize::MAX)
187 .sum::<usize>();
188 if reachable <= 1 || total == 0 || node_count <= 1 {
189 return 0.0;
190 }
191 let reached = (reachable - 1) as f64;
192 reached / total as f64 * reached / (node_count - 1) as f64
193}
194
195fn brandes_source<Node>(adjacent: &SlotAdjacency<Node>, source: usize, scores: &mut [f64]) {
196 let bound = adjacent.neighbors.len();
197 let mut stack = Vec::new();
198 let mut predecessors = vec![Vec::new(); bound];
199 let mut paths = vec![0.0; bound];
200 let mut distances = vec![usize::MAX; bound];
201 let mut queue = VecDeque::new();
202 paths[source] = 1.0;
203 distances[source] = 0;
204 queue.push_back(source);
205 while let Some(node) = queue.pop_front() {
206 stack.push(node);
207 for &neighbor in &adjacent.neighbors[node] {
208 if distances[neighbor] == usize::MAX {
209 distances[neighbor] = distances[node] + 1;
210 queue.push_back(neighbor);
211 }
212 if distances[neighbor] == distances[node] + 1 {
213 paths[neighbor] += paths[node];
214 predecessors[neighbor].push(node);
215 }
216 }
217 }
218 let mut dependency = vec![0.0; bound];
219 while let Some(node) = stack.pop() {
220 for &predecessor in &predecessors[node] {
221 dependency[predecessor] += paths[predecessor] / paths[node] * (1.0 + dependency[node]);
222 }
223 if node != source {
224 scores[node] += dependency[node];
225 }
226 }
227}