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