Skip to main content

pedant_graph/analysis/
betweenness.rs

1//! How much of the selected topology's shortest routing passes through each
2//! node.
3//!
4//! One bounded directed Brandes pass over the shared selected indexes. It is the
5//! only superlinear operation this crate offers, so it proves its own work bound
6//! before it allocates any state, and it walks iteratively so a repository's own
7//! depth cannot reach the stack.
8//!
9//! The selected topology is a multigraph: two parallel edges are two distinct
10//! shortest routes, and both contribute to the share of every node they pass
11//! through.
12
13use std::collections::VecDeque;
14
15use crate::id::{GraphNodeId, index_of, position};
16
17use super::error::GraphAnalysisError;
18use super::index::{AnalysisContext, SelectedIndexes};
19use super::limits::GraphAnalysisLimits;
20
21/// How much of the selected topology's shortest routing passes through one
22/// node.
23///
24/// The raw score sums, over every ordered pair of other nodes, the fraction of
25/// that pair's shortest routes this node sits inside. The normalized score
26/// divides it by how many such pairs a graph this size can state at all.
27#[derive(Clone, Copy, Debug)]
28pub struct BetweennessCentrality {
29    node: GraphNodeId,
30    raw: f64,
31    normalized: f64,
32}
33
34impl BetweennessCentrality {
35    /// The node this score describes.
36    pub fn node(self) -> GraphNodeId {
37        self.node
38    }
39
40    /// The summed share of shortest routes passing through it.
41    pub fn raw(self) -> f64 {
42        self.raw
43    }
44
45    /// That share against every ordered pair a graph this size can state, or
46    /// zero when it can state none.
47    pub fn normalized(self) -> f64 {
48        self.normalized
49    }
50}
51
52/// The buffers one bounded pass mutates from its first source to its last.
53///
54/// Each round clears what it needs rather than allocating again, so a pass over
55/// an admitted graph pays for its state once. Every node-indexed table is sized
56/// once and only its elements move, so only `order`, `queue`, and one node's
57/// predecessor list are `Vec`; the scores the pass produces are sealed on the
58/// way out.
59struct Brandes {
60    totals: Box<[f64]>,
61    distance: Box<[Option<u32>]>,
62    routes: Box<[f64]>,
63    predecessors: Box<[Vec<GraphNodeId>]>,
64    share: Box<[f64]>,
65    order: Vec<GraphNodeId>,
66    queue: VecDeque<GraphNodeId>,
67}
68
69impl Brandes {
70    /// One pass's state over a topology of `width` nodes.
71    fn empty(width: usize) -> Self {
72        Self {
73            totals: vec![0.0; width].into_boxed_slice(),
74            distance: vec![None; width].into_boxed_slice(),
75            routes: vec![0.0; width].into_boxed_slice(),
76            predecessors: vec![Vec::new(); width].into_boxed_slice(),
77            share: vec![0.0; width].into_boxed_slice(),
78            order: Vec::with_capacity(width),
79            queue: VecDeque::new(),
80        }
81    }
82
83    /// Run one round per source, in raw node-id order.
84    fn walk(&mut self, indexes: &SelectedIndexes) {
85        for at in 0..self.totals.len() {
86            self.round(indexes, GraphNodeId::new(position(at)));
87        }
88    }
89
90    /// Measure one source's shortest routes, then credit what they pass
91    /// through.
92    fn round(&mut self, indexes: &SelectedIndexes, source: GraphNodeId) {
93        self.restart(source);
94        self.measure(indexes);
95        self.credit(source);
96    }
97
98    /// Clear every round-local buffer and settle the source at distance zero.
99    ///
100    /// A source reaches itself by exactly one route and arrives from nothing,
101    /// which is what makes every later count a count of routes from it.
102    fn restart(&mut self, source: GraphNodeId) {
103        self.distance.iter_mut().for_each(|steps| *steps = None);
104        self.routes.iter_mut().for_each(|routes| *routes = 0.0);
105        self.share.iter_mut().for_each(|share| *share = 0.0);
106        self.predecessors.iter_mut().for_each(Vec::clear);
107        self.order.clear();
108        self.queue.clear();
109        self.settle(source, 0);
110        add(&mut self.routes, source, 1.0);
111    }
112
113    /// Follow every settled node's edges until nothing is left to settle.
114    fn measure(&mut self, indexes: &SelectedIndexes) {
115        while let Some(node) = self.queue.pop_front() {
116            self.relax(indexes, node);
117        }
118    }
119
120    /// Extend one settled node's routes along every selected edge leaving it.
121    fn relax(&mut self, indexes: &SelectedIndexes, node: GraphNodeId) {
122        let next = match self.steps(node) {
123            Some(steps) => steps + 1,
124            None => return,
125        };
126        for entry in indexes.outgoing(node) {
127            self.follow((node, entry.node()), next);
128        }
129    }
130
131    /// Follow one selected edge into the node it reaches.
132    ///
133    /// A node reached for the first time settles here. A node already settled at
134    /// this distance gains another route, which is how a parallel edge and a
135    /// merging branch each count once. Anything nearer is already behind.
136    fn follow(&mut self, step: (GraphNodeId, GraphNodeId), next: u32) {
137        let (from, reached) = step;
138        match self.steps(reached) {
139            None => self.settle(reached, next),
140            Some(steps) if steps == next => (),
141            Some(_) => return,
142        }
143        self.join(reached, from);
144    }
145
146    /// Record one node as first reached, at `steps` steps, and queue it.
147    ///
148    /// A node this pass does not address settles nowhere: recording it would put
149    /// it on the walk once per arriving edge, because nothing would ever answer
150    /// that it had already been reached.
151    fn settle(&mut self, node: GraphNodeId, steps: u32) {
152        match self.distance.get_mut(index_of(node.index())) {
153            Some(slot) => *slot = Some(steps),
154            None => return,
155        }
156        self.order.push(node);
157        self.queue.push_back(node);
158    }
159
160    /// Record every route into `node` that arrives from `from`.
161    fn join(&mut self, node: GraphNodeId, from: GraphNodeId) {
162        let arriving = held(&self.routes, from);
163        add(&mut self.routes, node, arriving);
164        if let Some(list) = self.predecessors.get_mut(index_of(node.index())) {
165            list.push(from);
166        }
167    }
168
169    /// How many selected steps reach one node in the current round.
170    fn steps(&self, node: GraphNodeId) -> Option<u32> {
171        self.distance.get(index_of(node.index())).copied().flatten()
172    }
173
174    /// Credit every node the current round's shortest routes pass through.
175    ///
176    /// Nodes settle in reverse discovery order, so a node's own share is
177    /// complete before it is passed back to the nodes its routes arrive from.
178    fn credit(&mut self, source: GraphNodeId) {
179        let Self {
180            totals,
181            routes,
182            predecessors,
183            share,
184            order,
185            ..
186        } = self;
187        for node in order.iter().rev() {
188            pass_back((&routes[..], &predecessors[..]), share, *node);
189            award((totals, &share[..]), source, *node);
190        }
191    }
192
193    /// Every accumulated score beside its normalization, in raw node-id order.
194    fn scored(self) -> Box<[BetweennessCentrality]> {
195        let pairs = ordered_pairs(self.totals.len());
196        self.totals
197            .iter()
198            .enumerate()
199            .map(|(at, score)| BetweennessCentrality {
200                node: GraphNodeId::new(position(at)),
201                raw: *score,
202                normalized: against(*score, pairs),
203            })
204            .collect()
205    }
206}
207
208/// Every node's betweenness over the selected topology, in raw node-id order.
209///
210/// The work bound is proved first, so a graph beyond the caller's budget costs
211/// two counts rather than a table for every node.
212pub(crate) fn betweenness(
213    analysis: &AnalysisContext<'_>,
214) -> Result<Box<[BetweennessCentrality]>, GraphAnalysisError> {
215    let indexes = analysis.indexes();
216    admitted_work(indexes, analysis.limits())?;
217    let mut pass = Brandes::empty(indexes.node_count());
218    pass.walk(indexes);
219    Ok(pass.scored())
220}
221
222/// The work one pass would cost, proved against the stated ceiling.
223///
224/// One breadth-first round per node costs the node and selected-edge counts
225/// together. Both are admitted fixed-width counts, so their product can still
226/// overshoot the width it is counted in; the arithmetic is checked and refuses
227/// rather than wrapping into a bound that would pass.
228pub(crate) fn admitted_work(
229    indexes: &SelectedIndexes,
230    limits: GraphAnalysisLimits,
231) -> Result<(), GraphAnalysisError> {
232    let nodes = u64::from(position(indexes.node_count()));
233    let required = nodes
234        .checked_add(u64::from(indexes.selected_edges()))
235        .and_then(|round| nodes.checked_mul(round))
236        .ok_or(GraphAnalysisError::BetweennessWorkOverflow)?;
237    match required <= limits.max_betweenness_work() {
238        true => Ok(()),
239        false => Err(GraphAnalysisError::BetweennessWorkLimitExceeded {
240            required,
241            limit: limits.max_betweenness_work(),
242        }),
243    }
244}
245
246/// Pass one node's share back to every node its shortest routes arrive from.
247fn pass_back(reached: (&[f64], &[Vec<GraphNodeId>]), share: &mut [f64], node: GraphNodeId) {
248    let (routes, predecessors) = reached;
249    let carried = (1.0 + held(share, node)) / held(routes, node);
250    for from in entering(predecessors, node) {
251        add(share, *from, held(routes, *from) * carried);
252    }
253}
254
255/// Credit one node with its own share, unless it is the source it came from.
256fn award(state: (&mut [f64], &[f64]), source: GraphNodeId, node: GraphNodeId) {
257    let (totals, share) = state;
258    match node == source {
259        true => (),
260        false => add(totals, node, held(share, node)),
261    }
262}
263
264/// The nodes one node's shortest routes arrive from, one entry per route.
265fn entering(predecessors: &[Vec<GraphNodeId>], node: GraphNodeId) -> &[GraphNodeId] {
266    match predecessors.get(index_of(node.index())) {
267        Some(list) => list,
268        None => &[],
269    }
270}
271
272/// The value one node holds, or nothing at all for a node outside the table.
273fn held(values: &[f64], node: GraphNodeId) -> f64 {
274    values
275        .get(index_of(node.index()))
276        .copied()
277        .unwrap_or_default()
278}
279
280/// Add one amount to the value a node holds.
281fn add(values: &mut [f64], node: GraphNodeId, amount: f64) {
282    if let Some(slot) = values.get_mut(index_of(node.index())) {
283        *slot += amount;
284    }
285}
286
287/// How many ordered pairs a graph this size states that a third node could sit
288/// between, or nothing at all when it states none.
289fn ordered_pairs(nodes: usize) -> Option<f64> {
290    match nodes > 2 {
291        true => Some((nodes - 1) as f64 * (nodes - 2) as f64),
292        false => None,
293    }
294}
295
296/// One raw score against the ordered pairs it could have come from.
297fn against(raw: f64, pairs: Option<f64>) -> f64 {
298    match pairs {
299        Some(count) => raw / count,
300        None => 0.0,
301    }
302}