bevy_ecs/schedule/
graph_utils.rs

1use core::fmt::Debug;
2
3use bevy_utils::{HashMap, HashSet};
4use fixedbitset::FixedBitSet;
5use petgraph::{algo::TarjanScc, graphmap::NodeTrait, prelude::*};
6
7use crate::schedule::set::*;
8
9/// Unique identifier for a system or system set stored in a [`ScheduleGraph`].
10///
11/// [`ScheduleGraph`]: super::ScheduleGraph
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum NodeId {
14    /// Identifier for a system.
15    System(usize),
16    /// Identifier for a system set.
17    Set(usize),
18}
19
20impl NodeId {
21    /// Returns the internal integer value.
22    pub(crate) fn index(&self) -> usize {
23        match self {
24            NodeId::System(index) | NodeId::Set(index) => *index,
25        }
26    }
27
28    /// Returns `true` if the identified node is a system.
29    pub const fn is_system(&self) -> bool {
30        matches!(self, NodeId::System(_))
31    }
32
33    /// Returns `true` if the identified node is a system set.
34    pub const fn is_set(&self) -> bool {
35        matches!(self, NodeId::Set(_))
36    }
37}
38
39/// Specifies what kind of edge should be added to the dependency graph.
40#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
41pub(crate) enum DependencyKind {
42    /// A node that should be preceded.
43    Before,
44    /// A node that should be succeeded.
45    After,
46    /// A node that should be preceded and will **not** automatically insert an instance of `apply_deferred` on the edge.
47    BeforeNoSync,
48    /// A node that should be succeeded and will **not** automatically insert an instance of `apply_deferred` on the edge.
49    AfterNoSync,
50}
51
52/// An edge to be added to the dependency graph.
53#[derive(Clone)]
54pub(crate) struct Dependency {
55    pub(crate) kind: DependencyKind,
56    pub(crate) set: InternedSystemSet,
57}
58
59impl Dependency {
60    pub fn new(kind: DependencyKind, set: InternedSystemSet) -> Self {
61        Self { kind, set }
62    }
63}
64
65/// Configures ambiguity detection for a single system.
66#[derive(Clone, Debug, Default)]
67pub(crate) enum Ambiguity {
68    #[default]
69    Check,
70    /// Ignore warnings with systems in any of these system sets. May contain duplicates.
71    IgnoreWithSet(Vec<InternedSystemSet>),
72    /// Ignore all warnings.
73    IgnoreAll,
74}
75
76/// Metadata about how the node fits in the schedule graph
77#[derive(Clone, Default)]
78pub(crate) struct GraphInfo {
79    /// the sets that the node belongs to (hierarchy)
80    pub(crate) hierarchy: Vec<InternedSystemSet>,
81    /// the sets that the node depends on (must run before or after)
82    pub(crate) dependencies: Vec<Dependency>,
83    pub(crate) ambiguous_with: Ambiguity,
84}
85
86/// Converts 2D row-major pair of indices into a 1D array index.
87pub(crate) fn index(row: usize, col: usize, num_cols: usize) -> usize {
88    debug_assert!(col < num_cols);
89    (row * num_cols) + col
90}
91
92/// Converts a 1D array index into a 2D row-major pair of indices.
93pub(crate) fn row_col(index: usize, num_cols: usize) -> (usize, usize) {
94    (index / num_cols, index % num_cols)
95}
96
97/// Stores the results of the graph analysis.
98pub(crate) struct CheckGraphResults<V> {
99    /// Boolean reachability matrix for the graph.
100    pub(crate) reachable: FixedBitSet,
101    /// Pairs of nodes that have a path connecting them.
102    pub(crate) connected: HashSet<(V, V)>,
103    /// Pairs of nodes that don't have a path connecting them.
104    pub(crate) disconnected: Vec<(V, V)>,
105    /// Edges that are redundant because a longer path exists.
106    pub(crate) transitive_edges: Vec<(V, V)>,
107    /// Variant of the graph with no transitive edges.
108    pub(crate) transitive_reduction: DiGraphMap<V, ()>,
109    /// Variant of the graph with all possible transitive edges.
110    // TODO: this will very likely be used by "if-needed" ordering
111    #[allow(dead_code)]
112    pub(crate) transitive_closure: DiGraphMap<V, ()>,
113}
114
115impl<V: NodeTrait + Debug> Default for CheckGraphResults<V> {
116    fn default() -> Self {
117        Self {
118            reachable: FixedBitSet::new(),
119            connected: HashSet::new(),
120            disconnected: Vec::new(),
121            transitive_edges: Vec::new(),
122            transitive_reduction: DiGraphMap::new(),
123            transitive_closure: DiGraphMap::new(),
124        }
125    }
126}
127
128/// Processes a DAG and computes its:
129/// - transitive reduction (along with the set of removed edges)
130/// - transitive closure
131/// - reachability matrix (as a bitset)
132/// - pairs of nodes connected by a path
133/// - pairs of nodes not connected by a path
134///
135/// The algorithm implemented comes from
136/// ["On the calculation of transitive reduction-closure of orders"][1] by Habib, Morvan and Rampon.
137///
138/// [1]: https://doi.org/10.1016/0012-365X(93)90164-O
139pub(crate) fn check_graph<V>(
140    graph: &DiGraphMap<V, ()>,
141    topological_order: &[V],
142) -> CheckGraphResults<V>
143where
144    V: NodeTrait + Debug,
145{
146    if graph.node_count() == 0 {
147        return CheckGraphResults::default();
148    }
149
150    let n = graph.node_count();
151
152    // build a copy of the graph where the nodes and edges appear in topsorted order
153    let mut map = HashMap::with_capacity(n);
154    let mut topsorted = DiGraphMap::<V, ()>::new();
155    // iterate nodes in topological order
156    for (i, &node) in topological_order.iter().enumerate() {
157        map.insert(node, i);
158        topsorted.add_node(node);
159        // insert nodes as successors to their predecessors
160        for pred in graph.neighbors_directed(node, Incoming) {
161            topsorted.add_edge(pred, node, ());
162        }
163    }
164
165    let mut reachable = FixedBitSet::with_capacity(n * n);
166    let mut connected = HashSet::new();
167    let mut disconnected = Vec::new();
168
169    let mut transitive_edges = Vec::new();
170    let mut transitive_reduction = DiGraphMap::<V, ()>::new();
171    let mut transitive_closure = DiGraphMap::<V, ()>::new();
172
173    let mut visited = FixedBitSet::with_capacity(n);
174
175    // iterate nodes in topological order
176    for node in topsorted.nodes() {
177        transitive_reduction.add_node(node);
178        transitive_closure.add_node(node);
179    }
180
181    // iterate nodes in reverse topological order
182    for a in topsorted.nodes().rev() {
183        let index_a = *map.get(&a).unwrap();
184        // iterate their successors in topological order
185        for b in topsorted.neighbors_directed(a, Outgoing) {
186            let index_b = *map.get(&b).unwrap();
187            debug_assert!(index_a < index_b);
188            if !visited[index_b] {
189                // edge <a, b> is not redundant
190                transitive_reduction.add_edge(a, b, ());
191                transitive_closure.add_edge(a, b, ());
192                reachable.insert(index(index_a, index_b, n));
193
194                let successors = transitive_closure
195                    .neighbors_directed(b, Outgoing)
196                    .collect::<Vec<_>>();
197                for c in successors {
198                    let index_c = *map.get(&c).unwrap();
199                    debug_assert!(index_b < index_c);
200                    if !visited[index_c] {
201                        visited.insert(index_c);
202                        transitive_closure.add_edge(a, c, ());
203                        reachable.insert(index(index_a, index_c, n));
204                    }
205                }
206            } else {
207                // edge <a, b> is redundant
208                transitive_edges.push((a, b));
209            }
210        }
211
212        visited.clear();
213    }
214
215    // partition pairs of nodes into "connected by path" and "not connected by path"
216    for i in 0..(n - 1) {
217        // reachable is upper triangular because the nodes were topsorted
218        for index in index(i, i + 1, n)..=index(i, n - 1, n) {
219            let (a, b) = row_col(index, n);
220            let pair = (topological_order[a], topological_order[b]);
221            if reachable[index] {
222                connected.insert(pair);
223            } else {
224                disconnected.push(pair);
225            }
226        }
227    }
228
229    // fill diagonal (nodes reach themselves)
230    // for i in 0..n {
231    //     reachable.set(index(i, i, n), true);
232    // }
233
234    CheckGraphResults {
235        reachable,
236        connected,
237        disconnected,
238        transitive_edges,
239        transitive_reduction,
240        transitive_closure,
241    }
242}
243
244/// Returns the simple cycles in a strongly-connected component of a directed graph.
245///
246/// The algorithm implemented comes from
247/// ["Finding all the elementary circuits of a directed graph"][1] by D. B. Johnson.
248///
249/// [1]: https://doi.org/10.1137/0204007
250pub fn simple_cycles_in_component<N>(graph: &DiGraphMap<N, ()>, scc: &[N]) -> Vec<Vec<N>>
251where
252    N: NodeTrait + Debug,
253{
254    let mut cycles = vec![];
255    let mut sccs = vec![scc.to_vec()];
256
257    while let Some(mut scc) = sccs.pop() {
258        // only look at nodes and edges in this strongly-connected component
259        let mut subgraph = DiGraphMap::new();
260        for &node in &scc {
261            subgraph.add_node(node);
262        }
263
264        for &node in &scc {
265            for successor in graph.neighbors(node) {
266                if subgraph.contains_node(successor) {
267                    subgraph.add_edge(node, successor, ());
268                }
269            }
270        }
271
272        // path of nodes that may form a cycle
273        let mut path = Vec::with_capacity(subgraph.node_count());
274        // we mark nodes as "blocked" to avoid finding permutations of the same cycles
275        let mut blocked = HashSet::with_capacity(subgraph.node_count());
276        // connects nodes along path segments that can't be part of a cycle (given current root)
277        // those nodes can be unblocked at the same time
278        let mut unblock_together: HashMap<N, HashSet<N>> =
279            HashMap::with_capacity(subgraph.node_count());
280        // stack for unblocking nodes
281        let mut unblock_stack = Vec::with_capacity(subgraph.node_count());
282        // nodes can be involved in multiple cycles
283        let mut maybe_in_more_cycles: HashSet<N> = HashSet::with_capacity(subgraph.node_count());
284        // stack for DFS
285        let mut stack = Vec::with_capacity(subgraph.node_count());
286
287        // we're going to look for all cycles that begin and end at this node
288        let root = scc.pop().unwrap();
289        // start a path at the root
290        path.clear();
291        path.push(root);
292        // mark this node as blocked
293        blocked.insert(root);
294
295        // DFS
296        stack.clear();
297        stack.push((root, subgraph.neighbors(root)));
298        while !stack.is_empty() {
299            let (ref node, successors) = stack.last_mut().unwrap();
300            if let Some(next) = successors.next() {
301                if next == root {
302                    // found a cycle
303                    maybe_in_more_cycles.extend(path.iter());
304                    cycles.push(path.clone());
305                } else if !blocked.contains(&next) {
306                    // first time seeing `next` on this path
307                    maybe_in_more_cycles.remove(&next);
308                    path.push(next);
309                    blocked.insert(next);
310                    stack.push((next, subgraph.neighbors(next)));
311                    continue;
312                } else {
313                    // not first time seeing `next` on this path
314                }
315            }
316
317            if successors.peekable().peek().is_none() {
318                if maybe_in_more_cycles.contains(node) {
319                    unblock_stack.push(*node);
320                    // unblock this node's ancestors
321                    while let Some(n) = unblock_stack.pop() {
322                        if blocked.remove(&n) {
323                            let unblock_predecessors = unblock_together.entry(n).or_default();
324                            unblock_stack.extend(unblock_predecessors.iter());
325                            unblock_predecessors.clear();
326                        }
327                    }
328                } else {
329                    // if its descendants can be unblocked later, this node will be too
330                    for successor in subgraph.neighbors(*node) {
331                        unblock_together.entry(successor).or_default().insert(*node);
332                    }
333                }
334
335                // remove node from path and DFS stack
336                path.pop();
337                stack.pop();
338            }
339        }
340
341        // remove node from subgraph
342        subgraph.remove_node(root);
343
344        // divide remainder into smaller SCCs
345        let mut tarjan_scc = TarjanScc::new();
346        tarjan_scc.run(&subgraph, |scc| {
347            if scc.len() > 1 {
348                sccs.push(scc.to_vec());
349            }
350        });
351    }
352
353    cycles
354}