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