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