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