Skip to main content

pedant_graph/analysis/
traversal.rs

1//! Bounded neighborhoods, shortest paths, and induced subgraph selections.
2//!
3//! Every walk is iterative and indexed by dense node identity, so no query
4//! recurses on a depth the repository controls. All three read the shared
5//! selected adjacency and containment-parent indexes; none reads the raw edge
6//! slice or restates the selection.
7//!
8//! [`GraphDirection`] lives beside the walks that consume it rather than beside
9//! the edge selection: it states how a walk follows an edge, not which edges an
10//! analysis admits, and every operation that reads it is here.
11
12use std::collections::VecDeque;
13
14use crate::containment::ContainmentEdge;
15use crate::id::{GraphEdgeId, GraphNodeId, index_of, position};
16
17use super::error::GraphAnalysisError;
18use super::index::{AnalysisContext, SelectedAdjacency, SelectedIndexes};
19use super::limits::GraphAnalysisLimits;
20
21/// The adjacency list a one-sided walk does not read.
22const NEITHER: &[SelectedAdjacency] = &[];
23
24/// Which way a neighborhood or subgraph walk follows a selected edge.
25///
26/// Every other directed operation follows outgoing edges. A reversed question
27/// is asked by swapping the endpoints rather than by changing what an operation
28/// means.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum GraphDirection {
31    /// Follow edges away from their source.
32    Outgoing,
33    /// Follow edges back towards their source.
34    Incoming,
35    /// Follow edges either way. A self-loop reaches the node it starts at,
36    /// which the walk has already reached.
37    Both,
38}
39
40/// One reached node and how many selected steps away it is.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct GraphNeighbor {
43    node: GraphNodeId,
44    distance: u32,
45}
46
47impl GraphNeighbor {
48    /// The node this walk reached.
49    pub fn node(self) -> GraphNodeId {
50        self.node
51    }
52
53    /// The minimum number of selected steps that reach it.
54    pub fn distance(self) -> u32 {
55        self.distance
56    }
57}
58
59/// One directed route over selected outgoing edges.
60///
61/// A route always states one more node than it states edges: the nodes are the
62/// stops, and the edges are the steps between them.
63#[derive(Debug)]
64pub struct GraphPath {
65    nodes: Box<[GraphNodeId]>,
66    edges: Box<[GraphEdgeId]>,
67}
68
69impl GraphPath {
70    /// The nodes this route visits, from source to target.
71    pub fn nodes(&self) -> &[GraphNodeId] {
72        &self.nodes
73    }
74
75    /// The raw edges this route takes, in the order it takes them.
76    pub fn edges(&self) -> &[GraphEdgeId] {
77        &self.edges
78    }
79}
80
81/// One selection into the raw graph, never a second graph.
82///
83/// Every identity indexes back into the graph the analysis borrows, which is
84/// where a caller resolves it. The selection owns no record, no evidence, and
85/// no serialized form of its own.
86#[derive(Debug)]
87pub struct GraphSubgraph {
88    nodes: Box<[GraphNodeId]>,
89    edges: Box<[GraphEdgeId]>,
90    containment: Box<[ContainmentEdge]>,
91}
92
93impl GraphSubgraph {
94    /// The selected nodes, in node-id order. The seed is always one of them.
95    pub fn nodes(&self) -> &[GraphNodeId] {
96        &self.nodes
97    }
98
99    /// Every selected edge whose source and target are both selected, in raw
100    /// edge-id order.
101    pub fn edges(&self) -> &[GraphEdgeId] {
102        &self.edges
103    }
104
105    /// Every containment edge whose parent and child are both selected, in
106    /// child order.
107    pub fn containment(&self) -> &[ContainmentEdge] {
108        &self.containment
109    }
110}
111
112/// The minimum distance from one seed to every node one walk reached.
113struct Distances {
114    measured: Box<[Option<u32>]>,
115}
116
117impl Distances {
118    /// How many selected steps reach one node, if the walk reached it.
119    fn of(&self, node: GraphNodeId) -> Option<u32> {
120        self.measured.get(index_of(node.index())).copied().flatten()
121    }
122
123    /// Every node the walk reached, in node-id order, including the seed.
124    fn reached(&self) -> Box<[GraphNodeId]> {
125        self.measured
126            .iter()
127            .enumerate()
128            .filter(|(_, distance)| distance.is_some())
129            .map(|(at, _)| GraphNodeId::new(position(at)))
130            .collect()
131    }
132
133    /// Every node the walk reached beyond the seed, in distance then node
134    /// order.
135    fn ordered(&self) -> Box<[GraphNeighbor]> {
136        let mut found: Vec<GraphNeighbor> = self
137            .measured
138            .iter()
139            .enumerate()
140            .filter_map(|(at, distance)| beyond_seed(at, *distance))
141            .collect();
142        found.sort_unstable_by_key(|neighbor| (neighbor.distance, neighbor.node));
143        found.into_boxed_slice()
144    }
145}
146
147/// One measured node, unless it is the seed the walk started at.
148fn beyond_seed(at: usize, distance: Option<u32>) -> Option<GraphNeighbor> {
149    distance
150        .filter(|steps| *steps > 0)
151        .map(|steps| GraphNeighbor {
152            node: GraphNodeId::new(position(at)),
153            distance: steps,
154        })
155}
156
157/// Every node connected to `node` within `depth` selected steps in `direction`,
158/// at its minimum distance.
159pub(crate) fn neighbors(
160    analysis: &AnalysisContext<'_>,
161    node: GraphNodeId,
162    depth: u32,
163    direction: GraphDirection,
164) -> Result<Box<[GraphNeighbor]>, GraphAnalysisError> {
165    let indexes = analysis.indexes();
166    admitted_walk(indexes, analysis.limits(), node, depth)?;
167    Ok(walk(indexes, node, depth, direction).ordered())
168}
169
170/// The shortest route from one node to another over selected outgoing edges.
171///
172/// Among equally short routes the answer is the one whose raw edge identities
173/// are lexicographically least, because each step takes the least edge that
174/// still reaches the target in the steps that remain.
175///
176/// No depth is requested and none is proved: a route is as long as the topology
177/// makes it, so this walk is bounded by the admitted node and selected-edge
178/// counts rather than by the depth ceiling a neighborhood or subgraph states.
179pub(crate) fn path(
180    analysis: &AnalysisContext<'_>,
181    route: (GraphNodeId, GraphNodeId),
182) -> Result<Option<GraphPath>, GraphAnalysisError> {
183    let (_, target) = route;
184    let indexes = analysis.indexes();
185    admitted_route(indexes, route)?;
186    let remaining = walk(indexes, target, u32::MAX, GraphDirection::Incoming);
187    Ok(least_route(indexes, &remaining, route))
188}
189
190/// The selection induced by the nodes `seed` reaches within `depth` selected
191/// steps.
192pub(crate) fn subgraph(
193    analysis: &AnalysisContext<'_>,
194    seed: GraphNodeId,
195    depth: u32,
196    direction: GraphDirection,
197) -> Result<GraphSubgraph, GraphAnalysisError> {
198    let indexes = analysis.indexes();
199    admitted_walk(indexes, analysis.limits(), seed, depth)?;
200    Ok(induced(indexes, &walk(indexes, seed, depth, direction)))
201}
202
203/// One node this analysis holds a record for.
204fn known_node(indexes: &SelectedIndexes, node: GraphNodeId) -> Result<(), GraphAnalysisError> {
205    match indexes.holds(node) {
206        true => Ok(()),
207        false => Err(GraphAnalysisError::UnknownNode { node }),
208    }
209}
210
211/// One seed and one requested depth, proved in the order a bounded walk
212/// refuses them.
213///
214/// The one owner of that order. A borrowed walk proves it on the way in, and a
215/// cached handle proves it before it examines anything it retained, so the two
216/// cannot answer one pair of arguments with two different refusals. The seed is
217/// proved first: a node this analysis holds no record for is not a question a
218/// ceiling has anything to say about.
219pub(crate) fn admitted_walk(
220    indexes: &SelectedIndexes,
221    limits: GraphAnalysisLimits,
222    node: GraphNodeId,
223    depth: u32,
224) -> Result<(), GraphAnalysisError> {
225    known_node(indexes, node)?;
226    admitted_depth(limits, depth)
227}
228
229/// One ordered node pair, proved in the order a route refuses them.
230///
231/// The one owner of that order, beside [`admitted_walk`]. A route requests no
232/// depth and no ceiling has anything to say about how long the topology makes
233/// it, so the pair is the whole admission. The borrowed route proves it on the
234/// way in, and a cached handle proves it before it examines anything it
235/// retained, so the two cannot answer one pair with two different refusals and
236/// a refused pair is never counted as a product nothing derived.
237pub(crate) fn admitted_route(
238    indexes: &SelectedIndexes,
239    route: (GraphNodeId, GraphNodeId),
240) -> Result<(), GraphAnalysisError> {
241    let (source, target) = route;
242    known_node(indexes, source)?;
243    known_node(indexes, target)
244}
245
246/// One requested depth, proved against the stated ceiling before any walk
247/// state exists.
248fn admitted_depth(limits: GraphAnalysisLimits, depth: u32) -> Result<(), GraphAnalysisError> {
249    match depth <= limits.max_depth() {
250        true => Ok(()),
251        false => Err(GraphAnalysisError::DepthLimitExceeded {
252            requested: depth,
253            limit: limits.max_depth(),
254        }),
255    }
256}
257
258/// Measure every node one seed reaches within `depth` selected steps.
259///
260/// A breadth-first queue over dense node identities, so the walk is iterative
261/// and each node is measured once, at its minimum distance.
262fn walk(
263    indexes: &SelectedIndexes,
264    seed: GraphNodeId,
265    depth: u32,
266    direction: GraphDirection,
267) -> Distances {
268    let mut measured: Vec<Option<u32>> = vec![None; indexes.node_count()];
269    let mut queue: VecDeque<GraphNeighbor> = VecDeque::new();
270    admit(
271        &mut measured,
272        &mut queue,
273        GraphNeighbor {
274            node: seed,
275            distance: 0,
276        },
277    );
278    while let Some(reached) = queue.pop_front() {
279        if reached.distance < depth {
280            extend(indexes, (&mut measured, &mut queue), reached, direction);
281        }
282    }
283    Distances {
284        measured: measured.into_boxed_slice(),
285    }
286}
287
288/// Measure every unmeasured node one reached node leads to.
289fn extend(
290    indexes: &SelectedIndexes,
291    state: (&mut [Option<u32>], &mut VecDeque<GraphNeighbor>),
292    from: GraphNeighbor,
293    direction: GraphDirection,
294) {
295    let (measured, queue) = state;
296    let distance = from.distance + 1;
297    for entry in adjacent(indexes, from.node, direction) {
298        admit(
299            measured,
300            queue,
301            GraphNeighbor {
302                node: entry.node(),
303                distance,
304            },
305        );
306    }
307}
308
309/// Record one node's distance, unless the walk already measured it.
310fn admit(
311    measured: &mut [Option<u32>],
312    queue: &mut VecDeque<GraphNeighbor>,
313    reached: GraphNeighbor,
314) {
315    match measured.get_mut(index_of(reached.node.index())) {
316        Some(slot) if slot.is_none() => {
317            *slot = Some(reached.distance);
318            queue.push_back(reached);
319        }
320        _ => (),
321    }
322}
323
324/// The adjacency one direction reads from one node, in raw edge-id order.
325fn adjacent(
326    indexes: &SelectedIndexes,
327    node: GraphNodeId,
328    direction: GraphDirection,
329) -> impl Iterator<Item = SelectedAdjacency> + '_ {
330    let (first, second) = match direction {
331        GraphDirection::Outgoing => (indexes.outgoing(node), NEITHER),
332        GraphDirection::Incoming => (indexes.incoming(node), NEITHER),
333        GraphDirection::Both => (indexes.outgoing(node), indexes.incoming(node)),
334    };
335    first.iter().chain(second).copied()
336}
337
338/// Walk the least route to the target the measured distances describe.
339fn least_route(
340    indexes: &SelectedIndexes,
341    remaining: &Distances,
342    route: (GraphNodeId, GraphNodeId),
343) -> Option<GraphPath> {
344    let (source, _) = route;
345    let mut steps = remaining.of(source)?;
346    let mut nodes: Vec<GraphNodeId> = vec![source];
347    let mut edges: Vec<GraphEdgeId> = Vec::new();
348    let mut current = source;
349    while steps > 0 {
350        let step = least_step(indexes, remaining, current, steps)?;
351        nodes.push(step.node());
352        edges.push(step.edge());
353        current = step.node();
354        steps -= 1;
355    }
356    Some(GraphPath {
357        nodes: nodes.into_boxed_slice(),
358        edges: edges.into_boxed_slice(),
359    })
360}
361
362/// The least selected edge leaving one node that still reaches the target in
363/// the steps that remain.
364fn least_step(
365    indexes: &SelectedIndexes,
366    remaining: &Distances,
367    from: GraphNodeId,
368    steps: u32,
369) -> Option<SelectedAdjacency> {
370    indexes
371        .outgoing(from)
372        .iter()
373        .filter(|entry| remaining.of(entry.node()) == Some(steps - 1))
374        .min_by_key(|entry| entry.edge())
375        .copied()
376}
377
378/// The induced selection over every node one walk reached.
379fn induced(indexes: &SelectedIndexes, distances: &Distances) -> GraphSubgraph {
380    let held = distances.reached();
381    GraphSubgraph {
382        edges: internal_edges(indexes, &held),
383        containment: internal_containment(indexes, &held),
384        nodes: held,
385    }
386}
387
388/// Every selected edge whose source and target are both held, in raw edge-id
389/// order.
390fn internal_edges(indexes: &SelectedIndexes, held: &[GraphNodeId]) -> Box<[GraphEdgeId]> {
391    let mut found: Vec<GraphEdgeId> = held
392        .iter()
393        .flat_map(|node| leaving(indexes, *node, held))
394        .collect();
395    found.sort_unstable();
396    found.into_boxed_slice()
397}
398
399/// The selected edges leaving one held node for another held node.
400fn leaving<'index>(
401    indexes: &'index SelectedIndexes,
402    node: GraphNodeId,
403    held: &'index [GraphNodeId],
404) -> impl Iterator<Item = GraphEdgeId> + 'index {
405    indexes
406        .outgoing(node)
407        .iter()
408        .filter(move |entry| held.binary_search(&entry.node()).is_ok())
409        .map(|entry| entry.edge())
410}
411
412/// Every containment edge whose parent and child are both held, in child
413/// order.
414fn internal_containment(indexes: &SelectedIndexes, held: &[GraphNodeId]) -> Box<[ContainmentEdge]> {
415    held.iter()
416        .filter_map(|child| owned_pair(indexes, *child, held))
417        .collect()
418}
419
420/// One held child beside the parent that owns it, unless that parent is
421/// outside the held set.
422fn owned_pair(
423    indexes: &SelectedIndexes,
424    child: GraphNodeId,
425    held: &[GraphNodeId],
426) -> Option<ContainmentEdge> {
427    indexes
428        .parent(child)
429        .filter(|parent| held.binary_search(parent).is_ok())
430        .map(|parent| ContainmentEdge::new(parent, child))
431}