Skip to main content

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}