diff --git a/src/finite_function/arrow.rs b/src/finite_function/arrow.rs index cb3467d..359ec67 100644 --- a/src/finite_function/arrow.rs +++ b/src/finite_function/arrow.rs @@ -216,6 +216,21 @@ impl FiniteFunction { } } +impl FiniteFunction +where + K::Type: NaturalArray, +{ + /// Check if this finite function is injective. + pub fn is_injective(&self) -> bool { + if self.source().is_zero() { + return true; + } + + let counts = self.table.bincount(self.target.clone()); + counts.max().map_or(true, |m| m <= K::I::one()) + } +} + /// Compute the universal map for a coequalizer `q : B → Q` and arrow `f : B → T`, generalised to /// the case where `T` is an arbitrary set (i.e., `f` is an array of `T`) pub fn coequalizer_universal( diff --git a/src/strict/graph.rs b/src/strict/graph.rs index 3af6ae3..2cf4638 100644 --- a/src/strict/graph.rs +++ b/src/strict/graph.rs @@ -68,7 +68,21 @@ pub(crate) fn node_adjacency( where K::Type: NaturalArray, { - converse(&h.s).flatmap(&h.t) + node_adjacency_from_incidence(&h.s, &h.t) +} + +/// Return the node-level adjacency map from incidence data. +/// +/// If `W` is the finite set of nodes, then the result is an indexed coproduct +/// `adjacency : W → W*`, where `adjacency(w)` is all nodes reachable in a single step from `w`. +pub(crate) fn node_adjacency_from_incidence( + s: &IndexedCoproduct>, + t: &IndexedCoproduct>, +) -> IndexedCoproduct> +where + K::Type: NaturalArray, +{ + converse(s).flatmap(t) } /// A kahn-ish algorithm for topological sorting of an adjacency relation, encoded as an diff --git a/src/strict/hypergraph/arrow.rs b/src/strict/hypergraph/arrow.rs index 9f8a676..2fcffbd 100644 --- a/src/strict/hypergraph/arrow.rs +++ b/src/strict/hypergraph/arrow.rs @@ -1,13 +1,51 @@ use super::object::Hypergraph; -use crate::array::{Array, ArrayKind}; +use crate::array::{Array, ArrayKind, NaturalArray}; use crate::finite_function::FiniteFunction; +use crate::indexed_coproduct::IndexedCoproduct; +use crate::strict::graph::{ + node_adjacency, node_adjacency_from_incidence, sparse_relative_indegree, +}; use core::fmt::Debug; +use num_traits::{One, Zero}; + +fn successors( + adjacency: &IndexedCoproduct>, + frontier: &K::Index, +) -> K::Index +where + K::Type: NaturalArray, +{ + if frontier.is_empty() { + return K::Index::empty(); + } + + let f = FiniteFunction::new(frontier.clone(), adjacency.len()).unwrap(); + let (g, _) = sparse_relative_indegree(adjacency, &f); + g.table +} + +fn filter_unvisited(visited: &K::Index, candidates: &K::Index) -> K::Index +where + K::Type: NaturalArray, +{ + if candidates.is_empty() { + return K::Index::empty(); + } + + let visited_on_candidates = visited.gather(candidates.get_range(..)); + let unvisited_ix = visited_on_candidates.zero(); + candidates.gather(unvisited_ix.get_range(..)) +} #[derive(Debug)] pub enum InvalidHypergraphArrow { + TypeMismatchW, + TypeMismatchX, NotNaturalW, NotNaturalX, + NotNaturalS, + NotNaturalT, } pub struct HypergraphArrow { @@ -35,7 +73,10 @@ where target: Hypergraph, w: FiniteFunction, x: FiniteFunction, - ) -> Result { + ) -> Result + where + K::Type: NaturalArray, + { HypergraphArrow { source, target, @@ -46,7 +87,10 @@ where } /// Check validity of a HypergraphArrow. - pub fn validate(self) -> Result { + pub fn validate(self) -> Result + where + K::Type: NaturalArray, + { // for self : g → h let g = &self.source; let h = &self.target; @@ -55,22 +99,136 @@ where // wire labels, operation labels, and operation types should be preserved under the natural // transformations w and x. - // Check naturality of w - if g.w != (&self.w >> &h.w).unwrap() { + // Check naturality on node labels: + // g.w = w ; h.w + let composed_w = (&self.w >> &h.w).ok_or(InvalidHypergraphArrow::TypeMismatchW)?; + if g.w != composed_w { return Err(InvalidHypergraphArrow::NotNaturalW); } - // Check naturality of x - if g.x != (&self.x >> &h.x).unwrap() { + // Check naturality on operation labels: + // g.x = x ; h.x + let composed_x = (&self.x >> &h.x).ok_or(InvalidHypergraphArrow::TypeMismatchX)?; + if g.x != composed_x { return Err(InvalidHypergraphArrow::NotNaturalX); } + // Check naturality of incidence (sources): + // g.s; w = h.s reindexed along x. + let s_lhs = + g.s.map_values(&self.w) + .ok_or(InvalidHypergraphArrow::NotNaturalS)?; + let s_rhs = + h.s.map_indexes(&self.x) + .ok_or(InvalidHypergraphArrow::NotNaturalS)?; + if s_lhs != s_rhs { + return Err(InvalidHypergraphArrow::NotNaturalS); + } + + // Check naturality of incidence (targets). + // g.t; w = h.t reindexed along x. + let t_lhs = + g.t.map_values(&self.w) + .ok_or(InvalidHypergraphArrow::NotNaturalT)?; + let t_rhs = + h.t.map_indexes(&self.x) + .ok_or(InvalidHypergraphArrow::NotNaturalT)?; + if t_lhs != t_rhs { + return Err(InvalidHypergraphArrow::NotNaturalT); + } + Ok(self) + } + + /// True when this arrow is injective on both nodes and edges. + pub fn is_monomorphism(&self) -> bool + where + K::Type: NaturalArray, + { + self.w.is_injective() && self.x.is_injective() + } + + /// Check convexity of a subgraph `H → G`. + /// + pub fn is_convex_subgraph(&self) -> bool + where + K::Type: NaturalArray, + { + if !self.is_monomorphism() { + return false; + } + + let g = &self.target; + let n_nodes = g.w.len(); + let n_edges = g.x.len(); + + // Build the complement set of edges (those not in the image of x). + let mut edge_mask = K::Index::fill(K::I::zero(), n_edges.clone()); + edge_mask.scatter_assign_constant(&self.x.table, K::I::one()); + let outside_edge_ix = edge_mask.zero(); + let outside_edges = FiniteFunction::new(outside_edge_ix, n_edges).unwrap(); + + // Adjacency restricted to edges in the subobject (inside edges). + let s_in = g.s.map_indexes(&self.x).unwrap(); + let t_in = g.t.map_indexes(&self.x).unwrap(); + let adj_in = node_adjacency_from_incidence(&s_in, &t_in); + + // Adjacency restricted to edges outside the subobject. + let s_out = g.s.map_indexes(&outside_edges).unwrap(); + let t_out = g.t.map_indexes(&outside_edges).unwrap(); + let adj_out = node_adjacency_from_incidence(&s_out, &t_out); + + // Full adjacency (used after we've already left the subobject). + let adj_all = node_adjacency(g); + + // Two-layer reachability: + // - layer 0: paths that have used only inside edges + // - layer 1: paths that have used at least one outside edge + // + // Convexity fails iff some selected node is reachable in layer 1. + let mut visited0 = K::Index::fill(K::I::zero(), n_nodes.clone()); + let mut visited1 = K::Index::fill(K::I::zero(), n_nodes.clone()); + let mut frontier0 = self.w.table.clone(); + let mut frontier1 = K::Index::empty(); + + // Seed search from the selected nodes. + visited0.scatter_assign_constant(&frontier0, K::I::one()); + + while !frontier0.is_empty() || !frontier1.is_empty() { + // From layer 0, inside edges stay in layer 0. + let next0: K::Index = successors::(&adj_in, &frontier0); + // From layer 0, outside edges move to layer 1. + let next1_from0 = successors::(&adj_out, &frontier0); + // From layer 1, any edge keeps you in layer 1. + let next1_from1 = successors::(&adj_all, &frontier1); + + // Avoid revisiting nodes we've already seen in the same layer. + let next0: K::Index = filter_unvisited::(&visited0, &next0); + + let next1: K::Index = { + let merged = next1_from0.concatenate(&next1_from1); + if merged.is_empty() { + K::Index::empty() + } else { + let (unique, _) = merged.sparse_bincount(); + filter_unvisited::(&visited1, &unique) + } + }; + + if next0.is_empty() && next1.is_empty() { + break; + } + + // Mark and advance frontiers. + visited0.scatter_assign_constant(&next0, K::I::one()); + visited1.scatter_assign_constant(&next1, K::I::one()); + frontier0 = next0; + frontier1 = next1; + } - // TODO: add this check. - // Types of operations are also preserved under w and x. - //assert_eq!(g.s.values >> g.w, h.s.indexed_values(f.x) >> h.w); - //assert_eq!(g.t.values >> g.w, h.t.indexed_values(f.x) >> h.w); + // If any selected node is reachable in layer 1, it's not convex. + let reached_selected = visited1.gather(self.w.table.get_range(..)); + !reached_selected.max().map_or(false, |m| m >= K::I::one()) } } diff --git a/tests/graph/mod.rs b/tests/graph/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/tests/graph/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/hypergraph/mod.rs b/tests/hypergraph/mod.rs index 31e4727..6fb56c2 100644 --- a/tests/hypergraph/mod.rs +++ b/tests/hypergraph/mod.rs @@ -1,3 +1,5 @@ pub mod equality; pub mod strategy; +pub mod test_arrow; pub mod test_hypergraph; +pub mod test_monogamous; diff --git a/tests/hypergraph/strategy.rs b/tests/hypergraph/strategy.rs index 8022f2a..a198b24 100644 --- a/tests/hypergraph/strategy.rs +++ b/tests/hypergraph/strategy.rs @@ -174,15 +174,12 @@ pub fn arb_inclusion< labels: Labels, g: Hypergraph, ) -> BoxedStrategy> { - // We have an arbitrary hypergraph g, and some arbitrary label arrays. - let h_labels = Labels { - w: g.w.clone() + labels.w, - x: g.x.clone() + labels.x, - }; - arb_hypergraph(h_labels) - .prop_flat_map(move |h| { - let w = FiniteFunction::inj0(g.w.len(), h.w.len() - g.w.len()); - let x = FiniteFunction::inj0(g.x.len(), h.x.len() - g.x.len()); + // Build target as a coproduct g + k so inj0 is incidence-natural by construction. + arb_hypergraph(labels) + .prop_flat_map(move |k| { + let h = g.coproduct(&k); + let w = FiniteFunction::inj0(g.w.len(), k.w.len()); + let x = FiniteFunction::inj0(g.x.len(), k.x.len()); Just(HypergraphArrow::new(g.clone(), h, w, x).expect("valid HypergraphArrow")) }) diff --git a/tests/hypergraph/test_arrow.rs b/tests/hypergraph/test_arrow.rs new file mode 100644 index 0000000..d19557b --- /dev/null +++ b/tests/hypergraph/test_arrow.rs @@ -0,0 +1,92 @@ +use open_hypergraphs::array::vec::*; +use open_hypergraphs::finite_function::FiniteFunction; +use open_hypergraphs::lax::{Hyperedge, Hypergraph as LaxHypergraph, NodeId}; +use open_hypergraphs::strict::hypergraph::arrow::{HypergraphArrow, InvalidHypergraphArrow}; +use open_hypergraphs::strict::hypergraph::Hypergraph; + +type Obj = usize; +type Arr = usize; + +fn build_hypergraph( + node_labels: Vec, + edges: &[(usize, Vec, Vec)], +) -> Hypergraph { + let mut h = LaxHypergraph::empty(); + h.nodes = node_labels; + for (label, sources, targets) in edges { + let edge = Hyperedge { + sources: sources.iter().map(|&i| NodeId(i)).collect(), + targets: targets.iter().map(|&i| NodeId(i)).collect(), + }; + h.new_edge(*label, edge); + } + h.to_hypergraph() +} + +fn ff(table: Vec, target: usize) -> FiniteFunction { + FiniteFunction::new(VecArray(table), target).unwrap() +} + +#[test] +fn test_validate_rejects_non_natural_w() { + let source = build_hypergraph(vec![0], &[]); + let target = build_hypergraph(vec![1], &[]); + let w = ff(vec![0], 1); + let x = ff(vec![], 0); + let err = HypergraphArrow::new(source, target, w, x).unwrap_err(); + assert!(matches!(err, InvalidHypergraphArrow::NotNaturalW)); +} + +#[test] +fn test_validate_rejects_non_natural_x() { + let source = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + let target = build_hypergraph(vec![0, 0], &[(1, vec![0], vec![1])]); + let w = ff(vec![0, 1], 2); + let x = ff(vec![0], 1); + let err = HypergraphArrow::new(source, target, w, x).unwrap_err(); + assert!(matches!(err, InvalidHypergraphArrow::NotNaturalX)); +} + +#[test] +fn test_validate_rejects_undefined_w_composition() { + let source = build_hypergraph(vec![0], &[]); + let target = build_hypergraph(vec![0], &[]); + // valid finite function, but codomain size (2) does not match h.w.len() (1), + // so w ; h.w is undefined and validate should return TypeMismatchW. + let w = ff(vec![0], 2); + let x = ff(vec![], 0); + let err = HypergraphArrow::new(source, target, w, x).unwrap_err(); + assert!(matches!(err, InvalidHypergraphArrow::TypeMismatchW)); +} + +#[test] +fn test_validate_accepts_incidence_natural_arrow() { + let source = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + let target = build_hypergraph(vec![0, 0], &[(0, vec![1], vec![1])]); + + let w = ff(vec![1, 1], 2); + let x = ff(vec![0], 1); + assert!(HypergraphArrow::new(source, target, w, x).is_ok()); +} + +#[test] +fn test_validate_rejects_non_natural_s() { + let source = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + let target = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + + let w = ff(vec![1, 1], 2); + let x = ff(vec![0], 1); + let err = HypergraphArrow::new(source, target, w, x).unwrap_err(); + assert!(matches!(err, InvalidHypergraphArrow::NotNaturalS)); +} + +#[test] +fn test_validate_rejects_non_natural_t() { + let source = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + let target = build_hypergraph(vec![0, 0], &[(0, vec![0], vec![1])]); + + let w = ff(vec![0, 0], 2); + let x = ff(vec![0], 1); + let err = HypergraphArrow::new(source, target, w, x).unwrap_err(); + assert!(matches!(err, InvalidHypergraphArrow::NotNaturalT)); +} diff --git a/tests/hypergraph/test_hypergraph.rs b/tests/hypergraph/test_hypergraph.rs index 1ba1b41..83e3bab 100644 --- a/tests/hypergraph/test_hypergraph.rs +++ b/tests/hypergraph/test_hypergraph.rs @@ -125,7 +125,6 @@ fn test_is_acyclic_false_self_loop() { let h = build_hypergraph(1, &[(vec![0], vec![0])]); assert!(!h.is_acyclic()); } - #[test] fn test_in_out_degree_counts_multiplicity() { let sources = IndexedCoproduct::from_semifinite( diff --git a/tests/hypergraph/test_monogamous.rs b/tests/hypergraph/test_monogamous.rs new file mode 100644 index 0000000..270803f --- /dev/null +++ b/tests/hypergraph/test_monogamous.rs @@ -0,0 +1,78 @@ +use open_hypergraphs::array::vec::*; +use open_hypergraphs::finite_function::FiniteFunction; +use open_hypergraphs::lax::{Hyperedge, Hypergraph as LaxHypergraph, NodeId}; +use open_hypergraphs::strict::hypergraph::arrow::HypergraphArrow; +use open_hypergraphs::strict::hypergraph::Hypergraph; + +type Obj = usize; +type Arr = usize; + +fn build_hypergraph( + node_count: usize, + edges: &[(Vec, Vec)], +) -> Hypergraph { + let mut h = LaxHypergraph::empty(); + h.nodes = vec![0; node_count]; + for (sources, targets) in edges { + let edge = Hyperedge { + sources: sources.iter().map(|&i| NodeId(i)).collect(), + targets: targets.iter().map(|&i| NodeId(i)).collect(), + }; + h.new_edge(0, edge); + } + h.to_hypergraph() +} + +fn ff(table: Vec, target: usize) -> FiniteFunction { + FiniteFunction::new(VecArray(table), target).unwrap() +} + +#[test] +fn test_is_monomorphism_false_for_non_injective_w() { + let h = build_hypergraph(2, &[]); + let g = build_hypergraph(1, &[]); + let w = ff(vec![0, 0], 1); + let x = ff(vec![], 0); + let arrow = HypergraphArrow::new(h, g, w, x).unwrap(); + assert!(!arrow.is_monomorphism()); +} + +#[test] +fn test_convex_subgraph_false_with_shortcut() { + let g = build_hypergraph( + 3, + &[(vec![0], vec![1]), (vec![1], vec![2]), (vec![0], vec![2])], + ); + let h = build_hypergraph(3, &[(vec![0], vec![1]), (vec![1], vec![2])]); + + let w = ff(vec![0, 1, 2], 3); + let x = ff(vec![0, 1], 3); + let arrow = HypergraphArrow::new(h, g, w, x).unwrap(); + assert!(!arrow.is_convex_subgraph()); +} + +#[test] +fn test_convex_subgraph_true_identity() { + let g = build_hypergraph( + 3, + &[(vec![0], vec![1]), (vec![1], vec![2]), (vec![0], vec![2])], + ); + let w = ff(vec![0, 1, 2], 3); + let x = ff(vec![0, 1, 2], 3); + let arrow = HypergraphArrow::new(g.clone(), g, w, x).unwrap(); + assert!(arrow.is_convex_subgraph()); +} + +#[test] +fn test_convex_subgraph_true_small() { + let g = build_hypergraph( + 3, + &[(vec![0], vec![1]), (vec![1], vec![2]), (vec![0], vec![2])], + ); + let h = build_hypergraph(2, &[(vec![0], vec![1])]); + + let w = ff(vec![0, 1], 3); + let x = ff(vec![0], 3); + let arrow = HypergraphArrow::new(h, g, w, x).unwrap(); + assert!(arrow.is_convex_subgraph()); +} diff --git a/tests/lib.rs b/tests/lib.rs index 555f164..a38ebda 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -13,6 +13,5 @@ pub mod hypergraph; pub mod open_hypergraph; pub mod eval; -pub mod graph; pub mod lax; pub mod layer;