Skip to main content

open_hypergraphs/strict/hypergraph/
arrow.rs

1use super::object::Hypergraph;
2use crate::array::{Array, ArrayKind, NaturalArray};
3use crate::category::Arrow;
4use crate::finite_function::FiniteFunction;
5use crate::indexed_coproduct::IndexedCoproduct;
6use crate::strict::graph::{
7    node_adjacency, node_adjacency_from_incidence, sparse_relative_indegree,
8};
9
10use core::fmt::Debug;
11use num_traits::{One, Zero};
12
13fn successors<K: ArrayKind>(
14    adjacency: &IndexedCoproduct<K, FiniteFunction<K>>,
15    frontier: &K::Index,
16) -> K::Index
17where
18    K::Type<K::I>: NaturalArray<K>,
19{
20    if frontier.is_empty() {
21        return K::Index::empty();
22    }
23
24    let f = FiniteFunction::new(frontier.clone(), adjacency.len()).unwrap();
25    let (g, _) = sparse_relative_indegree(adjacency, &f);
26    g.table
27}
28
29fn filter_unvisited<K: ArrayKind>(visited: &K::Index, candidates: &K::Index) -> K::Index
30where
31    K::Type<K::I>: NaturalArray<K>,
32{
33    if candidates.is_empty() {
34        return K::Index::empty();
35    }
36
37    let visited_on_candidates = visited.gather(candidates.get_range(..));
38    let unvisited_ix = visited_on_candidates.zero();
39    candidates.gather(unvisited_ix.get_range(..))
40}
41
42#[derive(Debug)]
43pub enum InvalidHypergraphArrow {
44    TypeMismatchW,
45    TypeMismatchX,
46    NotNaturalW,
47    NotNaturalX,
48    NotNaturalS,
49    NotNaturalT,
50}
51
52pub(crate) fn validate_hypergraph_morphism<K: ArrayKind, O, A>(
53    source: &Hypergraph<K, O, A>,
54    target: &Hypergraph<K, O, A>,
55    w: &FiniteFunction<K>,
56    x: &FiniteFunction<K>,
57) -> Result<(), InvalidHypergraphArrow>
58where
59    K::Type<K::I>: NaturalArray<K>,
60    K::Type<O>: Array<K, O> + PartialEq,
61    K::Type<A>: Array<K, A> + PartialEq,
62{
63    if w.source() != source.w.len() || w.target() != target.w.len() {
64        return Err(InvalidHypergraphArrow::TypeMismatchW);
65    }
66    if x.source() != source.x.len() || x.target() != target.x.len() {
67        return Err(InvalidHypergraphArrow::TypeMismatchX);
68    }
69
70    // Check naturality on node labels: source.w = w ; target.w
71    let composed_w = (w >> &target.w).ok_or(InvalidHypergraphArrow::TypeMismatchW)?;
72    if source.w != composed_w {
73        return Err(InvalidHypergraphArrow::NotNaturalW);
74    }
75
76    // Check naturality on operation labels: source.x = x ; target.x
77    let composed_x = (x >> &target.x).ok_or(InvalidHypergraphArrow::TypeMismatchX)?;
78    if source.x != composed_x {
79        return Err(InvalidHypergraphArrow::NotNaturalX);
80    }
81
82    // Check naturality of incidence (sources): source.s; w = target.s reindexed along x.
83    let s_lhs = source
84        .s
85        .map_values(w)
86        .ok_or(InvalidHypergraphArrow::NotNaturalS)?;
87    let s_rhs = target
88        .s
89        .map_indexes(x)
90        .ok_or(InvalidHypergraphArrow::NotNaturalS)?;
91    if s_lhs != s_rhs {
92        return Err(InvalidHypergraphArrow::NotNaturalS);
93    }
94
95    // Check naturality of incidence (targets): source.t; w = target.t reindexed along x.
96    let t_lhs = source
97        .t
98        .map_values(w)
99        .ok_or(InvalidHypergraphArrow::NotNaturalT)?;
100    let t_rhs = target
101        .t
102        .map_indexes(x)
103        .ok_or(InvalidHypergraphArrow::NotNaturalT)?;
104    if t_lhs != t_rhs {
105        return Err(InvalidHypergraphArrow::NotNaturalT);
106    }
107
108    Ok(())
109}
110
111pub struct HypergraphArrow<K: ArrayKind, O, A> {
112    /// Source hypergraph
113    pub source: Hypergraph<K, O, A>,
114
115    /// target hypergraph
116    pub target: Hypergraph<K, O, A>,
117
118    /// Natural transformation on wires
119    pub w: FiniteFunction<K>,
120
121    /// Natural transformation on operations
122    pub x: FiniteFunction<K>,
123}
124
125pub(crate) fn is_convex_subgraph_morphism<K: ArrayKind, O, A>(
126    source: &Hypergraph<K, O, A>,
127    target: &Hypergraph<K, O, A>,
128    w: &FiniteFunction<K>,
129    x: &FiniteFunction<K>,
130) -> bool
131where
132    K::Type<K::I>: NaturalArray<K>,
133    K::Type<O>: Array<K, O> + PartialEq,
134    K::Type<A>: Array<K, A> + PartialEq,
135{
136    if validate_hypergraph_morphism(source, target, w, x).is_err() {
137        return false;
138    }
139    if !w.is_injective() || !x.is_injective() {
140        return false;
141    }
142
143    let g = target;
144    let n_nodes = g.w.len();
145    let n_edges = g.x.len();
146
147    // Build the complement set of edges (those not in the image of x).
148    let mut edge_mask = K::Index::fill(K::I::zero(), n_edges.clone());
149    edge_mask.scatter_assign_constant(&x.table, K::I::one());
150    let outside_edge_ix = edge_mask.zero();
151    let outside_edges = if let Some(e) = FiniteFunction::new(outside_edge_ix, n_edges) {
152        e
153    } else {
154        return false;
155    };
156
157    // Adjacency restricted to edges in the subobject (inside edges).
158    let s_in = if let Some(s) = g.s.map_indexes(x) {
159        s
160    } else {
161        return false;
162    };
163    let t_in = if let Some(t) = g.t.map_indexes(x) {
164        t
165    } else {
166        return false;
167    };
168    let adj_in = node_adjacency_from_incidence(&s_in, &t_in);
169
170    // Adjacency restricted to edges outside the subobject.
171    let s_out = if let Some(s) = g.s.map_indexes(&outside_edges) {
172        s
173    } else {
174        return false;
175    };
176    let t_out = if let Some(t) = g.t.map_indexes(&outside_edges) {
177        t
178    } else {
179        return false;
180    };
181    let adj_out = node_adjacency_from_incidence(&s_out, &t_out);
182
183    // Full adjacency (used after we've already left the subobject).
184    let adj_all = node_adjacency(g);
185
186    // Two-layer reachability:
187    // - layer 0: paths that have used only inside edges
188    // - layer 1: paths that have used at least one outside edge
189    //
190    // Convexity fails iff some selected node is reachable in layer 1.
191    let mut visited0 = K::Index::fill(K::I::zero(), n_nodes.clone());
192    let mut visited1 = K::Index::fill(K::I::zero(), n_nodes.clone());
193    let mut frontier0 = w.table.clone();
194    let mut frontier1 = K::Index::empty();
195
196    // Seed search from the selected nodes.
197    visited0.scatter_assign_constant(&frontier0, K::I::one());
198
199    while !frontier0.is_empty() || !frontier1.is_empty() {
200        // From layer 0, inside edges stay in layer 0.
201        let next0: K::Index = successors::<K>(&adj_in, &frontier0);
202        // From layer 0, outside edges move to layer 1.
203        let next1_from0 = successors::<K>(&adj_out, &frontier0);
204        // From layer 1, any edge keeps you in layer 1.
205        let next1_from1 = successors::<K>(&adj_all, &frontier1);
206
207        // Avoid revisiting nodes we've already seen in the same layer.
208        let next0: K::Index = filter_unvisited::<K>(&visited0, &next0);
209
210        let next1: K::Index = {
211            let merged = next1_from0.concatenate(&next1_from1);
212            if merged.is_empty() {
213                K::Index::empty()
214            } else {
215                let (unique, _) = merged.sparse_bincount();
216                filter_unvisited::<K>(&visited1, &unique)
217            }
218        };
219
220        if next0.is_empty() && next1.is_empty() {
221            break;
222        }
223
224        // Mark and advance frontiers.
225        visited0.scatter_assign_constant(&next0, K::I::one());
226        visited1.scatter_assign_constant(&next1, K::I::one());
227        frontier0 = next0;
228        frontier1 = next1;
229    }
230
231    // If any selected node is reachable in layer 1, it's not convex.
232    let reached_selected = visited1.gather(w.table.get_range(..));
233    !reached_selected.max().is_some_and(|m| m >= K::I::one())
234}
235
236impl<K: ArrayKind, O, A> HypergraphArrow<K, O, A>
237where
238    K::Type<O>: Array<K, O> + PartialEq,
239    K::Type<A>: Array<K, A> + PartialEq,
240{
241    /// Safely create a new HypergraphArrow by checking naturality of `w` and `x`.
242    pub fn new(
243        source: Hypergraph<K, O, A>,
244        target: Hypergraph<K, O, A>,
245        w: FiniteFunction<K>,
246        x: FiniteFunction<K>,
247    ) -> Result<Self, InvalidHypergraphArrow>
248    where
249        K::Type<K::I>: NaturalArray<K>,
250    {
251        HypergraphArrow {
252            source,
253            target,
254            w,
255            x,
256        }
257        .validate()
258    }
259
260    /// Check validity of a HypergraphArrow.
261    pub fn validate(self) -> Result<Self, InvalidHypergraphArrow>
262    where
263        K::Type<K::I>: NaturalArray<K>,
264    {
265        validate_hypergraph_morphism(&self.source, &self.target, &self.w, &self.x)?;
266        Ok(self)
267    }
268
269    /// True when this arrow is injective on both nodes and edges.
270    pub fn is_monomorphism(&self) -> bool
271    where
272        K::Type<K::I>: NaturalArray<K>,
273    {
274        self.w.is_injective() && self.x.is_injective()
275    }
276
277    /// Check convexity of a subgraph `H → G`.
278    ///
279    pub fn is_convex_subgraph(&self) -> bool
280    where
281        K::Type<K::I>: NaturalArray<K>,
282    {
283        is_convex_subgraph_morphism(&self.source, &self.target, &self.w, &self.x)
284    }
285}
286
287impl<K: ArrayKind, O: Debug, A: Debug> Debug for HypergraphArrow<K, O, A>
288where
289    K::Index: Debug,
290    K::Type<K::I>: Debug,
291    K::Type<A>: Debug,
292    K::Type<O>: Debug,
293{
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.debug_struct("HypergraphArrow")
296            .field("source", &self.source)
297            .field("target", &self.target)
298            .field("w", &self.w)
299            .field("x", &self.x)
300            .finish()
301    }
302}
303
304impl<K: ArrayKind, O: Debug, A: Debug> Clone for HypergraphArrow<K, O, A>
305where
306    K::Type<K::I>: Clone,
307    K::Type<A>: Clone,
308    K::Type<O>: Clone,
309{
310    fn clone(&self) -> Self {
311        Self {
312            source: self.source.clone(),
313            target: self.target.clone(),
314            w: self.w.clone(),
315            x: self.x.clone(),
316        }
317    }
318}