Skip to main content

weavatrix_graph/algo/network/centrality/
basic.rs

1#![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}