Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/finite_function/arrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ impl<K: ArrayKind> FiniteFunction<K> {
}
}

impl<K: ArrayKind> FiniteFunction<K>
where
K::Type<K::I>: NaturalArray<K>,
{
/// 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<K: ArrayKind, T>(
Expand Down
16 changes: 15 additions & 1 deletion src/strict/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,21 @@ pub(crate) fn node_adjacency<K: ArrayKind, O, A>(
where
K::Type<K::I>: NaturalArray<K>,
{
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<K: ArrayKind>(
s: &IndexedCoproduct<K, FiniteFunction<K>>,
t: &IndexedCoproduct<K, FiniteFunction<K>>,
) -> IndexedCoproduct<K, FiniteFunction<K>>
where
K::Type<K::I>: NaturalArray<K>,
{
converse(s).flatmap(t)
}

/// A kahn-ish algorithm for topological sorting of an adjacency relation, encoded as an
Expand Down
180 changes: 169 additions & 11 deletions src/strict/hypergraph/arrow.rs
Original file line number Diff line number Diff line change
@@ -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<K: ArrayKind>(
adjacency: &IndexedCoproduct<K, FiniteFunction<K>>,
frontier: &K::Index,
) -> K::Index
where
K::Type<K::I>: NaturalArray<K>,
{
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<K: ArrayKind>(visited: &K::Index, candidates: &K::Index) -> K::Index
where
K::Type<K::I>: NaturalArray<K>,
{
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<K: ArrayKind, O, A> {
Expand Down Expand Up @@ -35,7 +73,10 @@ where
target: Hypergraph<K, O, A>,
w: FiniteFunction<K>,
x: FiniteFunction<K>,
) -> Result<Self, InvalidHypergraphArrow> {
) -> Result<Self, InvalidHypergraphArrow>
where
K::Type<K::I>: NaturalArray<K>,
{
HypergraphArrow {
source,
target,
Expand All @@ -46,7 +87,10 @@ where
}

/// Check validity of a HypergraphArrow.
pub fn validate(self) -> Result<Self, InvalidHypergraphArrow> {
pub fn validate(self) -> Result<Self, InvalidHypergraphArrow>
where
K::Type<K::I>: NaturalArray<K>,
{
// for self : g → h
let g = &self.source;
let h = &self.target;
Expand All @@ -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<K::I>: NaturalArray<K>,
{
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<K::I>: NaturalArray<K>,
{
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::<K>(&adj_in, &frontier0);
// From layer 0, outside edges move to layer 1.
let next1_from0 = successors::<K>(&adj_out, &frontier0);
// From layer 1, any edge keeps you in layer 1.
let next1_from1 = successors::<K>(&adj_all, &frontier1);

// Avoid revisiting nodes we've already seen in the same layer.
let next0: K::Index = filter_unvisited::<K>(&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::<K>(&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())
}
}

Expand Down
1 change: 0 additions & 1 deletion tests/graph/mod.rs

This file was deleted.

2 changes: 2 additions & 0 deletions tests/hypergraph/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod equality;
pub mod strategy;
pub mod test_arrow;
pub mod test_hypergraph;
pub mod test_monogamous;
15 changes: 6 additions & 9 deletions tests/hypergraph/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,12 @@ pub fn arb_inclusion<
labels: Labels<O, A>,
g: Hypergraph<VecKind, O, A>,
) -> BoxedStrategy<HypergraphArrow<VecKind, O, A>> {
// 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"))
})
Expand Down
92 changes: 92 additions & 0 deletions tests/hypergraph/test_arrow.rs
Original file line number Diff line number Diff line change
@@ -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<usize>,
edges: &[(usize, Vec<usize>, Vec<usize>)],
) -> Hypergraph<VecKind, Obj, Arr> {
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<usize>, target: usize) -> FiniteFunction<VecKind> {
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));
}
1 change: 0 additions & 1 deletion tests/hypergraph/test_hypergraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading