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