Skip to main content

weavatrix_graph/algo/
distance.rs

1use super::undirected_neighbors::UndirectedNeighbors;
2use crate::{IndexUndirectedGraphView, Vec};
3use alloc::collections::VecDeque;
4
5/// Exact unweighted distance metrics for a connected undirected graph.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DistanceAnalytics<Node> {
8    eccentricities: Vec<(Node, usize)>,
9    radius: usize,
10    diameter: usize,
11    center: Vec<Node>,
12    periphery: Vec<Node>,
13}
14
15impl<Node: Copy + Eq> DistanceAnalytics<Node> {
16    /// Returns `(node, eccentricity)` pairs in canonical node-index order.
17    #[must_use]
18    pub fn eccentricities(&self) -> &[(Node, usize)] {
19        &self.eccentricities
20    }
21
22    /// Returns the eccentricity of `node`, if it belongs to the graph.
23    #[must_use]
24    pub fn eccentricity(&self, node: Node) -> Option<usize> {
25        self.eccentricities
26            .iter()
27            .find_map(|&(candidate, value)| (candidate == node).then_some(value))
28    }
29
30    /// Returns the minimum eccentricity.
31    #[must_use]
32    pub const fn radius(&self) -> usize {
33        self.radius
34    }
35
36    /// Returns the maximum eccentricity.
37    #[must_use]
38    pub const fn diameter(&self) -> usize {
39        self.diameter
40    }
41
42    /// Returns all minimum-eccentricity nodes in canonical order.
43    #[must_use]
44    pub fn center(&self) -> &[Node] {
45        &self.center
46    }
47
48    /// Returns all maximum-eccentricity nodes in canonical order.
49    #[must_use]
50    pub fn periphery(&self) -> &[Node] {
51        &self.periphery
52    }
53
54    /// Consumes the result and returns its canonical eccentricity pairs.
55    #[must_use]
56    pub fn into_eccentricities(self) -> Vec<(Node, usize)> {
57        self.eccentricities
58    }
59}
60
61/// Computes exact unweighted distance metrics.
62///
63/// Returns `None` for an empty or disconnected graph. A singleton has radius,
64/// diameter, and eccentricity zero.
65#[must_use]
66pub fn distance_analytics<G>(graph: &G) -> Option<DistanceAnalytics<G::Node>>
67where
68    G: IndexUndirectedGraphView,
69{
70    let neighbors = UndirectedNeighbors::new(graph, |_| true);
71    neighbors.nodes().first()?;
72    let mut workspace = BfsWorkspace::<G::Node>::new(graph.node_bound());
73    let mut eccentricities = Vec::with_capacity(neighbors.nodes().len());
74    for &node in neighbors.nodes() {
75        let (value, reached) = workspace.run(&neighbors, node);
76        if reached != neighbors.nodes().len() {
77            return None;
78        }
79        eccentricities.push((node, value));
80    }
81    finish(eccentricities)
82}
83
84/// Computes exact metrics using accepted edges only.
85///
86/// The predicate is evaluated exactly once per edge. Returns `None` when the
87/// accepted-edge graph is empty or disconnected.
88#[must_use]
89pub fn distance_analytics_filtered<G, F>(
90    graph: &G,
91    allows_edge: F,
92) -> Option<DistanceAnalytics<G::Node>>
93where
94    G: IndexUndirectedGraphView,
95    F: Fn(G::Edge) -> bool,
96{
97    let neighbors = UndirectedNeighbors::new(graph, allows_edge);
98    neighbors.nodes().first()?;
99    let mut workspace = BfsWorkspace::<G::Node>::new(graph.node_bound());
100    let mut eccentricities = Vec::with_capacity(neighbors.nodes().len());
101    for &node in neighbors.nodes() {
102        let (eccentricity, reached) = workspace.run(&neighbors, node);
103        if reached != neighbors.nodes().len() {
104            return None;
105        }
106        eccentricities.push((node, eccentricity));
107    }
108    finish(eccentricities)
109}
110
111fn finish<Node: Copy + Eq>(eccentricities: Vec<(Node, usize)>) -> Option<DistanceAnalytics<Node>> {
112    let radius = eccentricities.iter().map(|pair| pair.1).min()?;
113    let diameter = eccentricities.iter().map(|pair| pair.1).max()?;
114    let center = select_nodes(&eccentricities, radius);
115    let periphery = select_nodes(&eccentricities, diameter);
116    Some(DistanceAnalytics {
117        eccentricities,
118        radius,
119        diameter,
120        center,
121        periphery,
122    })
123}
124
125/// Returns the exact eccentricity of `node` in a connected graph.
126#[must_use]
127pub fn eccentricity<G>(graph: &G, node: G::Node) -> Option<usize>
128where
129    G: IndexUndirectedGraphView,
130{
131    if !graph.contains_node(node) || graph.node_count() == 0 {
132        return None;
133    }
134    let neighbors = UndirectedNeighbors::new(graph, |_| true);
135    let (value, reached) = BfsWorkspace::<G::Node>::new(graph.node_bound()).run(&neighbors, node);
136    (reached == neighbors.nodes().len()).then_some(value)
137}
138
139/// Returns the exact diameter of a connected graph.
140#[must_use]
141pub fn diameter<G>(graph: &G) -> Option<usize>
142where
143    G: IndexUndirectedGraphView,
144{
145    distance_analytics(graph).map(|result| result.diameter())
146}
147
148/// Returns the exact radius of a connected graph.
149#[must_use]
150pub fn radius<G>(graph: &G) -> Option<usize>
151where
152    G: IndexUndirectedGraphView,
153{
154    distance_analytics(graph).map(|result| result.radius())
155}
156
157/// Returns all center nodes of a connected graph.
158#[must_use]
159pub fn center<G>(graph: &G) -> Option<Vec<G::Node>>
160where
161    G: IndexUndirectedGraphView,
162{
163    distance_analytics(graph).map(|result| result.center)
164}
165
166/// Returns all peripheral nodes of a connected graph.
167#[must_use]
168pub fn periphery<G>(graph: &G) -> Option<Vec<G::Node>>
169where
170    G: IndexUndirectedGraphView,
171{
172    distance_analytics(graph).map(|result| result.periphery)
173}
174
175fn select_nodes<Node: Copy>(values: &[(Node, usize)], target: usize) -> Vec<Node> {
176    values
177        .iter()
178        .filter_map(|&(node, value)| (value == target).then_some(node))
179        .collect()
180}
181
182struct BfsWorkspace<Node> {
183    seen: Vec<usize>,
184    distances: Vec<usize>,
185    epoch: usize,
186    queue: VecDeque<Node>,
187}
188
189impl<Node: Copy> BfsWorkspace<Node> {
190    fn new(node_bound: usize) -> Self {
191        Self {
192            seen: vec![0; node_bound],
193            distances: vec![0; node_bound],
194            epoch: 0,
195            queue: VecDeque::new(),
196        }
197    }
198
199    fn run<G>(&mut self, graph: &UndirectedNeighbors<G>, source: G::Node) -> (usize, usize)
200    where
201        G: IndexUndirectedGraphView<Node = Node>,
202    {
203        self.next_epoch();
204        let source_slot = G::node_slot(source);
205        self.seen[source_slot] = self.epoch;
206        self.distances[source_slot] = 0;
207        self.queue.push_back(source);
208        let mut reached = 0;
209        let mut maximum = 0;
210        while let Some(node) = self.queue.pop_front() {
211            let slot = G::node_slot(node);
212            reached += 1;
213            maximum = maximum.max(self.distances[slot]);
214            for &neighbor in graph.neighbors(node) {
215                let neighbor_slot = G::node_slot(neighbor);
216                if self.seen[neighbor_slot] != self.epoch {
217                    self.seen[neighbor_slot] = self.epoch;
218                    self.distances[neighbor_slot] = self.distances[slot] + 1;
219                    self.queue.push_back(neighbor);
220                }
221            }
222        }
223        (maximum, reached)
224    }
225
226    fn next_epoch(&mut self) {
227        self.queue.clear();
228        if self.epoch == usize::MAX {
229            self.seen.fill(0);
230            self.epoch = 1;
231        } else {
232            self.epoch += 1;
233        }
234    }
235}