Skip to content

Commit 568df59

Browse files
committed
frobenius decomposition v1
1 parent 0164043 commit 568df59

4 files changed

Lines changed: 306 additions & 0 deletions

File tree

src/lax/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ pub mod functor;
7676
pub mod hypergraph;
7777
pub mod mut_category;
7878
pub mod open_hypergraph;
79+
pub mod spider;
7980

8081
pub use crate::category::*;
8182
pub use hypergraph::*;
8283
pub use open_hypergraph::*;
84+
pub use spider::*;
8385

8486
pub mod optic;
8587
pub mod var;

src/lax/spider.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Make the Frobenius structure of an open hypergraph explicit.
2+
3+
use super::{Hyperedge, NodeId, OpenHypergraph};
4+
use crate::strict::vec::FiniteFunction;
5+
6+
/// An operation from `A`, or an explicitly represented spider.
7+
///
8+
/// A spider records its number of sources and targets. Its incident nodes still
9+
/// carry the object labels, just like those of an ordinary operation.
10+
#[derive(Debug, Clone, PartialEq, Eq)]
11+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12+
pub enum WithSpider<A> {
13+
Operation(A),
14+
Spider { sources: usize, targets: usize },
15+
}
16+
17+
impl<O: Clone + PartialEq, A: Clone> OpenHypergraph<O, A> {
18+
/// Replace implicit wiring with explicit spider operations.
19+
///
20+
/// The result is strict (has no pending quotient), monogamous, and
21+
/// acyclic. Each original operation is retained as
22+
/// [`WithSpider::Operation`].
23+
///
24+
/// For every original node, the construction inserts two spiders:
25+
///
26+
/// - `0 -> 1 + m`, where `m` is the number of occurrences as an operation
27+
/// source or global target;
28+
/// - `1 + n -> 0`, where `n` is the number of occurrences as an operation
29+
/// target or global source.
30+
///
31+
/// The extra leg connects the two spiders. Every other occurrence gets a
32+
/// distinct node, making every node occur exactly once as a source and
33+
/// exactly once as a target (counting the global interfaces).
34+
///
35+
/// Pending identifications in the lax quotient are applied first. An error
36+
/// means that the quotient attempted to identify nodes with different
37+
/// labels; the returned finite function is the quotient witness, as for
38+
/// [`OpenHypergraph::quotient`].
39+
pub fn spiderize(mut self) -> Result<OpenHypergraph<O, WithSpider<A>>, FiniteFunction> {
40+
self.quotient()?;
41+
42+
let OpenHypergraph {
43+
sources,
44+
targets,
45+
hypergraph,
46+
} = self;
47+
48+
assert_eq!(
49+
hypergraph.edges.len(),
50+
hypergraph.adjacency.len(),
51+
"malformed hypergraph: edges and adjacency lengths differ"
52+
);
53+
54+
let mut result = OpenHypergraph::empty();
55+
56+
// Keeping these first and in the original order makes forgetting the
57+
// spiders recover the original node ordering after quotienting.
58+
let central: Vec<NodeId> = hypergraph
59+
.nodes
60+
.iter()
61+
.cloned()
62+
.map(|label| result.new_node(label))
63+
.collect();
64+
65+
let mut splitter_targets: Vec<Vec<NodeId>> =
66+
central.iter().copied().map(|node| vec![node]).collect();
67+
let mut merger_sources: Vec<Vec<NodeId>> =
68+
central.iter().copied().map(|node| vec![node]).collect();
69+
70+
for (operation, adjacency) in hypergraph.edges.into_iter().zip(hypergraph.adjacency) {
71+
let operation_sources = adjacency
72+
.sources
73+
.into_iter()
74+
.map(|node| {
75+
let occurrence = result.new_node(hypergraph.nodes[node.0].clone());
76+
splitter_targets[node.0].push(occurrence);
77+
occurrence
78+
})
79+
.collect();
80+
81+
let operation_targets = adjacency
82+
.targets
83+
.into_iter()
84+
.map(|node| {
85+
let occurrence = result.new_node(hypergraph.nodes[node.0].clone());
86+
merger_sources[node.0].push(occurrence);
87+
occurrence
88+
})
89+
.collect();
90+
91+
result.new_edge(
92+
WithSpider::Operation(operation),
93+
Hyperedge {
94+
sources: operation_sources,
95+
targets: operation_targets,
96+
},
97+
);
98+
}
99+
100+
result.sources = sources
101+
.into_iter()
102+
.map(|node| {
103+
let occurrence = result.new_node(hypergraph.nodes[node.0].clone());
104+
merger_sources[node.0].push(occurrence);
105+
occurrence
106+
})
107+
.collect();
108+
109+
result.targets = targets
110+
.into_iter()
111+
.map(|node| {
112+
let occurrence = result.new_node(hypergraph.nodes[node.0].clone());
113+
splitter_targets[node.0].push(occurrence);
114+
occurrence
115+
})
116+
.collect();
117+
118+
for (splitter, merger) in splitter_targets.into_iter().zip(merger_sources) {
119+
let splitter_outputs = splitter.len();
120+
result.new_edge(
121+
WithSpider::Spider {
122+
sources: 0,
123+
targets: splitter_outputs,
124+
},
125+
Hyperedge {
126+
sources: vec![],
127+
targets: splitter,
128+
},
129+
);
130+
131+
let merger_inputs = merger.len();
132+
result.new_edge(
133+
WithSpider::Spider {
134+
sources: merger_inputs,
135+
targets: 0,
136+
},
137+
Hyperedge {
138+
sources: merger,
139+
targets: vec![],
140+
},
141+
);
142+
}
143+
144+
Ok(result)
145+
}
146+
}

tests/lax/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pub mod eval;
22
pub mod functor;
33
pub mod hypergraph;
44
pub mod open_hypergraph;
5+
pub mod spider;

tests/lax/spider.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use open_hypergraphs::lax::{Hyperedge, Hypergraph, NodeId, OpenHypergraph, WithSpider};
2+
use proptest::proptest;
3+
4+
use crate::theory::meaningless::{arb_open_hypergraph, Arr, Obj};
5+
6+
fn forget_spiders<O: Clone + PartialEq, A: Clone>(
7+
f: OpenHypergraph<O, WithSpider<A>>,
8+
) -> OpenHypergraph<O, A> {
9+
let OpenHypergraph {
10+
sources,
11+
targets,
12+
hypergraph,
13+
} = f;
14+
15+
let mut result = OpenHypergraph {
16+
sources,
17+
targets,
18+
hypergraph: Hypergraph {
19+
nodes: hypergraph.nodes,
20+
edges: vec![],
21+
adjacency: vec![],
22+
quotient: hypergraph.quotient,
23+
},
24+
};
25+
26+
for (operation, adjacency) in hypergraph.edges.into_iter().zip(hypergraph.adjacency) {
27+
match operation {
28+
WithSpider::Operation(operation) => {
29+
result.hypergraph.edges.push(operation);
30+
result.hypergraph.adjacency.push(adjacency);
31+
}
32+
WithSpider::Spider { sources, targets } => {
33+
assert_eq!(sources, adjacency.sources.len());
34+
assert_eq!(targets, adjacency.targets.len());
35+
36+
let mut incident = adjacency.sources.into_iter().chain(adjacency.targets);
37+
if let Some(first) = incident.next() {
38+
for node in incident {
39+
result.unify(first, node);
40+
}
41+
}
42+
}
43+
}
44+
}
45+
46+
result.quotient().expect("spider legs have equal labels");
47+
result
48+
}
49+
50+
#[test]
51+
fn spiderize_makes_a_cyclic_non_monogamous_hypergraph_syntactic() {
52+
let mut f = OpenHypergraph::empty();
53+
let x = f.new_node(());
54+
let y = f.new_node(());
55+
56+
f.new_edge("forward", ([x], [y]));
57+
f.new_edge("backward", ([y], [x]));
58+
f.sources = vec![x, x];
59+
f.targets = vec![y, y];
60+
61+
let spiderized = f.spiderize().unwrap();
62+
let strict = spiderized.clone().to_strict();
63+
64+
assert!(strict.is_monogamous());
65+
assert!(strict.is_acyclic());
66+
assert!(spiderized.hypergraph.is_strict());
67+
assert_eq!(spiderized.hypergraph.edges.len(), 2 + 2 * 2);
68+
}
69+
70+
#[test]
71+
fn spider_arities_count_all_occurrences() {
72+
let mut f = OpenHypergraph::empty();
73+
let node = f.new_node(());
74+
f.new_edge("op", ([node, node], [node]));
75+
f.sources = vec![node, node];
76+
f.targets = vec![node];
77+
78+
let spiderized = f.spiderize().unwrap();
79+
80+
assert_eq!(
81+
spiderized.hypergraph.edges,
82+
vec![
83+
WithSpider::Operation("op"),
84+
WithSpider::Spider {
85+
sources: 0,
86+
targets: 4,
87+
},
88+
WithSpider::Spider {
89+
sources: 4,
90+
targets: 0,
91+
},
92+
]
93+
);
94+
95+
assert_eq!(spiderized.sources.len(), 2);
96+
assert_ne!(spiderized.sources[0], spiderized.sources[1]);
97+
}
98+
99+
#[test]
100+
fn spiderize_applies_pending_quotients() {
101+
let mut f = OpenHypergraph::empty();
102+
let x = f.new_node(());
103+
let y = f.new_node(());
104+
f.new_edge("loop", ([x], [y]));
105+
f.unify(x, y);
106+
107+
let spiderized = f.spiderize().unwrap();
108+
109+
assert_eq!(spiderized.hypergraph.edges.len(), 1 + 2);
110+
assert!(spiderized.clone().to_strict().is_acyclic());
111+
assert_eq!(
112+
forget_spiders(spiderized),
113+
OpenHypergraph {
114+
sources: vec![],
115+
targets: vec![],
116+
hypergraph: Hypergraph {
117+
nodes: vec![()],
118+
edges: vec!["loop"],
119+
adjacency: vec![Hyperedge {
120+
sources: vec![NodeId(0)],
121+
targets: vec![NodeId(0)],
122+
}],
123+
quotient: (vec![], vec![]),
124+
},
125+
}
126+
);
127+
}
128+
129+
#[test]
130+
fn spiderize_rejects_inconsistent_quotients() {
131+
let mut f = OpenHypergraph::<_, ()>::identity(vec!["a", "b"]);
132+
f.unify(NodeId(0), NodeId(1));
133+
134+
assert!(f.spiderize().is_err());
135+
}
136+
137+
proptest! {
138+
#[test]
139+
fn spiderize_is_acyclic_monogamous_and_forgetful(
140+
strict_input in arb_open_hypergraph()
141+
) {
142+
let input: OpenHypergraph<Obj, Arr> =
143+
OpenHypergraph::from_strict(strict_input);
144+
let input_node_count = input.hypergraph.nodes.len();
145+
let input_operation_count = input.hypergraph.edges.len();
146+
let spiderized = input.clone().spiderize().unwrap();
147+
let strict_spiderized = spiderized.clone().to_strict();
148+
149+
assert!(strict_spiderized.is_monogamous());
150+
assert!(strict_spiderized.is_acyclic());
151+
assert_eq!(
152+
spiderized.hypergraph.edges.len(),
153+
input_operation_count + 2 * input_node_count
154+
);
155+
assert_eq!(forget_spiders(spiderized), input);
156+
}
157+
}

0 commit comments

Comments
 (0)