open_hypergraphs/strict/
layer.rs

1//! A [Coffman-Graham](https://en.wikipedia.org/wiki/Coffman%E2%80%93Graham_algorithm)-inspired
2//! layering algorithm.
3use crate::array::*;
4use crate::category::*;
5use crate::finite_function::*;
6use crate::indexed_coproduct::*;
7
8use crate::strict::open_hypergraph::*;
9
10use num_traits::{One, Zero};
11
12/// Compute a *layering* of an [`OpenHypergraph`]: a mapping `layer : X → K` from operations to
13/// integers compatible with the partial ordering on `X` induced by hypergraph structure.
14///
15/// See also: the [Coffman-Graham Algorithm](https://en.wikipedia.org/wiki/Coffman%E2%80%93Graham_algorithm)
16///
17/// # Returns
18///
19/// - A finite function `layer : X → L` assigning a *layer* to each operation in the set `X`
20/// - An array of *flags* for each operation determining if it was visited in the layering. If any
21///   operation was unvisited, the hypergraph was not monogamous acyclic.
22pub fn layer<K: ArrayKind, O, A>(f: &OpenHypergraph<K, O, A>) -> (FiniteFunction<K>, K::Type<K::I>)
23where
24    K::Type<A>: Array<K, A>,
25    K::Type<K::I>: NaturalArray<K>,
26{
27    let a = operation_adjacency(f);
28    let (ordering, completed) = kahn(&a);
29    (
30        FiniteFunction::new(ordering, f.h.x.0.len()).unwrap(),
31        completed,
32    )
33}
34
35/// Given an [`OpenHypergraph`], compute a layering of its operations as a finite function `X → L`,
36/// then return this as an array-of-arrays `r`.
37///
38/// # Returns
39///
40/// - A `Vec` of arrays `r`, where `r[i]` is the array of operations in layer `i`.
41/// - An array of unvisited nodes, as in [`layer`]
42pub fn layered_operations<K: ArrayKind, O, A>(
43    f: &OpenHypergraph<K, O, A>,
44) -> (Vec<K::Index>, K::Index)
45where
46    K::Type<A>: Array<K, A>,
47    K::Type<K::I>: NaturalArray<K>,
48    K::I: Into<usize>,
49{
50    let (order, unvisited) = layer(f);
51    (converse_iter(order).collect(), unvisited.into())
52}
53
54/// A kahn-ish algorithm for topological sorting of an adjacency relation, encoded as an
55/// [`IndexedCoproduct`] (see [`converse`] for details)
56fn kahn<K: ArrayKind>(
57    adjacency: &IndexedCoproduct<K, FiniteFunction<K>>,
58) -> (K::Index, K::Type<K::I>)
59where
60    K::Type<K::I>: NaturalArray<K>,
61{
62    // The layering assignment to each node.
63    // A mutable array of length n with values in {0..n}
64    let mut order: K::Type<K::I> = K::Type::<K::I>::fill(K::I::zero(), adjacency.len());
65
66    // Predicate determining if a node has been visited.
67    // 1 = unvisited
68    // 0 = visited
69    // NOTE: we store this as "NOT visited" so we can efficiently filter using "repeat".
70    let mut unvisited: K::Type<K::I> = K::Type::<K::I>::fill(K::I::one(), adjacency.len());
71
72    // Indegree of each node.
73    let mut indegree = indegree(adjacency);
74
75    // the set of nodes on the frontier, initialized to those with zero indegree.
76    let mut frontier: K::Index = zero(&indegree);
77
78    // Loop until frontier is empty, or at max possible layering depth.
79    let mut depth = K::I::zero();
80
81    // Implementation outline:
82    // 1. Compute *sparse* relative indegree, which is:
83    //      - idxs of reachable nodes
84    //      - counts of reachability from a given set
85    // 2. Subtract from global indegree array using scatter_sub_assign
86    //      - scatter_sub_assign::<K>(&mut indegree.table, &reachable_ix, &reachable_count.table);
87    // 3. Compute new frontier:
88    //      - Numpy-esque: `reachable_ix[indegree[reachable_ix] == 0 && unvisited[reachable_ix]]`
89    while !frontier.is_empty() && depth <= adjacency.len() {
90        // Mark nodes in the current frontier as visited
91        // unvisited[frontier] = 0;
92        unvisited.scatter_assign_constant(&frontier, K::I::zero());
93
94        // Set the order of nodes in the frontier to the current depth.
95        // order[frontier] = depth;
96        order.scatter_assign_constant(&frontier, depth.clone());
97
98        // For each node, compute the number of incoming edges from nodes in the frontier,
99        // and count paths to each.
100        let (reachable_ix, reachable_count) = sparse_relative_indegree(
101            adjacency,
102            &FiniteFunction::new(frontier, adjacency.len()).unwrap(),
103        );
104
105        // indegree = indegree - dense_relative_indegree(a, f)
106        indegree
107            .table
108            .as_mut()
109            .scatter_sub_assign(&reachable_ix.table, &reachable_count.table);
110
111        // Reachable nodes with zero indegree...
112        // frontier = reachable_ix[indegree[reachable_ix] == 0]
113        frontier = {
114            // *indices* i of reachable_ix such that indegree[reachable_ix[i]] == 0
115            let reachable_ix_indegree_zero_ix = indegree
116                .table
117                .gather(reachable_ix.table.get_range(..))
118                .zero();
119
120            // only nodes in reachable_ix with indegree 0
121            reachable_ix
122                .table
123                .gather(reachable_ix_indegree_zero_ix.get_range(..))
124        };
125
126        // .. and filter out those which have been visited.
127        // frontier = frontier[unvisited[frontier]]
128        frontier = filter::<K>(
129            &frontier,
130            &unvisited.as_ref().gather(frontier.get_range(..)),
131        );
132
133        // Increment depth
134        depth = depth + K::I::one();
135    }
136
137    (order.into(), unvisited)
138}
139
140/// Given:
141///
142/// - `values : K → N`
143/// - `predicate : K → 2`
144///
145/// Return the subset of `values` for which `predicate(i) = 1`
146fn filter<K: ArrayKind>(values: &K::Index, predicate: &K::Index) -> K::Index {
147    predicate.repeat(values.get_range(..))
148}
149
150/// Given an array of indices `values` in `{0..N}` and a predicate `N → 2`, select select values `i` for
151/// which `predicate(i) = 1`.
152#[allow(dead_code)]
153fn filter_by_dense<K: ArrayKind>(values: &K::Index, predicate: &K::Index) -> K::Index
154where
155    K::Type<K::I>: NaturalArray<K>,
156{
157    predicate
158        .gather(values.get_range(..))
159        .repeat(values.get_range(..))
160}
161
162////////////////////////////////////////////////////////////////////////////////
163// Graph methods
164
165/// Using the adjacency information in `adjacency`, compute the indegree of all nodes reachable from `f`.
166///
167/// More formally, let:
168///
169/// - `a : Σ_{n ∈ A} s(n) → N` denote the adjacency information of each
170/// - `f : K → N` be a subset of `K` nodes
171///
172/// Then `sparse_relative_indegree(a, f)` computes:
173///
174/// - `g : R → N`, the subset of (R)eachable nodes reachable from `f`
175/// - `i : R → E+1`, the *indegree* of nodes in `R`.
176///
177fn sparse_relative_indegree<K: ArrayKind>(
178    a: &IndexedCoproduct<K, FiniteFunction<K>>,
179    f: &FiniteFunction<K>,
180) -> (FiniteFunction<K>, FiniteFunction<K>)
181where
182    K::Type<K::I>: NaturalArray<K>,
183{
184    // Must have that the number of nodes `adjacency.len()`
185    assert_eq!(a.len(), f.target());
186
187    // Indices of operations reachable from those in the set f.
188    // Indices may appear more than once.
189    let g = a.indexed_values(f).unwrap();
190    let (i, c) = g.table.sparse_bincount();
191    let target = a.len() + K::I::one();
192
193    (
194        FiniteFunction::new(i, a.len()).unwrap(),
195        FiniteFunction::new(c, target).unwrap(),
196    )
197}
198
199/// Using the adjacency information in `adjacency`, compute the indegree of all nodes reachable from `f`.
200///
201/// More formally, define:
202///
203/// ```text
204/// a : Σ_{n ∈ A} s(n) → N  // the adjacency information of each
205/// f : K → N               // a subset of `K` nodes
206/// ```
207///
208/// Then `dense_relative_indegree(a, f)` computes the indegree from `f` of all `N` nodes.
209///
210/// # Returns
211///
212/// A finite function `N → E+1` denoting indegree of each node in `N` relative to `f`.
213fn dense_relative_indegree<K: ArrayKind>(
214    adjacency: &IndexedCoproduct<K, FiniteFunction<K>>,
215    f: &FiniteFunction<K>,
216) -> FiniteFunction<K>
217where
218    K::Type<K::I>: NaturalArray<K>,
219{
220    // Must have that the number of nodes `adjacency.len()`
221    assert_eq!(adjacency.len(), f.target());
222
223    // Operations reachable from those in the set f.
224    let reached = adjacency.indexed_values(f).unwrap();
225
226    // target is +1 because all edges could point to the same operation, so its indegree will be
227    // adjacency.len().
228    let target = adjacency.len() + K::I::one();
229    let table = (reached.table.as_ref() as &K::Type<K::I>).bincount(adjacency.len());
230    FiniteFunction::new(table, target).unwrap()
231}
232
233/// Compute indegree of all nodes in a multigraph.
234pub fn indegree<K: ArrayKind>(
235    adjacency: &IndexedCoproduct<K, FiniteFunction<K>>,
236) -> FiniteFunction<K>
237where
238    K::Type<K::I>: NaturalArray<K>,
239{
240    // Indegree is *relative* indegree with respect to all nodes.
241    // PERFORMANCE: can compute this more efficiently by just bincounting adjacency directly.
242    dense_relative_indegree(adjacency, &FiniteFunction::<K>::identity(adjacency.len()))
243}
244
245/// Return the adjacency map for an [`OpenHypergraph`] `f`.
246///
247/// If `X` is the finite set of operations in `f`, then `operation_adjacency(f)` computes the
248/// indexed coproduct `adjacency : X → X*`, where the list `adjacency(x)` is all operations reachable in
249/// a single step from operation `x`.
250pub fn operation_adjacency<K: ArrayKind, O, A>(
251    f: &OpenHypergraph<K, O, A>,
252) -> IndexedCoproduct<K, FiniteFunction<K>>
253where
254    K::Type<K::I>: NaturalArray<K>,
255{
256    f.h.t.flatmap(&converse(&f.h.s))
257}
258
259/// Compute the *converse* of an [`IndexedCoproduct`] thought of as a "multirelation".
260///
261/// An [`IndexedCoproduct`] `c : Σ_{x ∈ X} s(x) → Q` can equivalently be thought of as `c : X →
262/// Q*`, i.e. a mapping from X to finite lists of elements in Q.
263///
264/// Such a list defines a (multi-)relation as the multiset of pairs
265///
266/// `R = { ( x, f(x)_i ) | x ∈ X, i ∈ len(f(x)) }`
267///
268/// This function computes the *converse* of that relation as an indexed coproduct
269/// `converse(c) : Q → X*`, or more precisely
270/// `converse(c) : Σ_{q ∈ Q} s(q) → X`.
271///
272/// NOTE: An indexed coproduct does not uniquely represent a 'multirelation', since *order* of the
273/// elements matters.
274/// The result of this function is only unique up to permutation of the sublists.
275pub fn converse<K: ArrayKind>(
276    r: &IndexedCoproduct<K, FiniteFunction<K>>,
277) -> IndexedCoproduct<K, FiniteFunction<K>>
278where
279    K::Type<K::I>: NaturalArray<K>,
280{
281    // Create the 'values' array of the resulting [`IndexedCoproduct`]
282    // Sort segmented_arange(r.sources.table) by the *values* of r.
283    let values_table = {
284        let arange = K::Index::arange(&K::I::zero(), &r.sources.len());
285        let unsorted_values = r.sources.table.repeat(arange.get_range(..));
286        unsorted_values.sort_by(&r.values.table)
287    };
288
289    // Create the "sources" array of the result
290    let sources_table =
291        (r.values.table.as_ref() as &K::Type<K::I>).bincount(r.values.target.clone());
292
293    let sources = FiniteFunction::new(sources_table, r.values.table.len() + K::I::one()).unwrap();
294    let values = FiniteFunction::new(values_table, r.len()).unwrap();
295
296    IndexedCoproduct::new(sources, values).unwrap()
297}
298
299/// Given a FiniteFunction `X → L`, compute its converse,
300/// a relation `r : L → X*`, and return the result as an array of arrays,
301/// where `r_i` is the list of elements in `X` mapping to i.
302pub fn converse_iter<K: ArrayKind>(order: FiniteFunction<K>) -> impl Iterator<Item = K::Index>
303where
304    K::Type<K::I>: NaturalArray<K>,
305    K::I: Into<usize>,
306{
307    let c = converse(&IndexedCoproduct::elements(order));
308    c.into_iter().map(|x| x.table)
309}
310
311////////////////////////////////////////////////////////////////////////////////
312// Array trait helpers
313
314// FiniteFunction helpers
315fn zero<K: ArrayKind>(f: &FiniteFunction<K>) -> K::Index
316where
317    K::Type<K::I>: NaturalArray<K>,
318{
319    (f.table.as_ref() as &K::Type<K::I>).zero()
320}