open_hypergraphs/lax/spider.rs
1//! Make the Frobenius structure of an open hypergraph explicit.
2
3use super::{Hyperedge, NodeId, OpenHypergraph};
4use crate::strict::vec::FiniteFunction;
5
6/// An operation from `A`, or an explicitly represented spider.
7///
8/// A spider's arity is determined by its corresponding [`Hyperedge`].
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub enum WithSpider<A> {
12 Operation(A),
13 Spider,
14}
15
16impl<O: Clone + PartialEq, A: Clone> OpenHypergraph<O, A> {
17 /// Replace implicit wiring with explicit spider operations.
18 ///
19 /// The result is strict (has no pending quotient), monogamous, and
20 /// acyclic. Each original operation is retained as
21 /// [`WithSpider::Operation`].
22 ///
23 /// For every original node, the construction inserts two spiders:
24 ///
25 /// - `p -> 1 + m` on the left, where `p` is the number of global source
26 /// occurrences and `m` is the number of operation-source occurrences;
27 /// - `1 + n -> q` on the right, where `n` is the number of
28 /// operation-target occurrences and `q` is the number of global target
29 /// occurrences.
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(self) -> Result<OpenHypergraph<O, WithSpider<A>>, FiniteFunction> {
40 let nodes: Vec<NodeId> = (0..self.hypergraph.nodes.len()).map(NodeId).collect();
41 self.spiderize_nodes(&nodes)
42 }
43
44 /// Replace the chosen nodes' implicit wiring with explicit spider
45 /// operations.
46 ///
47 /// Each selected node is replaced by the same pair of spiders used by
48 /// [`Self::spiderize`]. Unselected nodes retain their implicit wiring.
49 /// Consequently, unlike [`Self::spiderize`], this operation does not by
50 /// itself guarantee that the result is acyclic or monogamous.
51 ///
52 /// `nodes` contains IDs from the input hypergraph. Pending identifications
53 /// are applied first, and IDs in `nodes` are mapped through the resulting
54 /// quotient. Selecting any representative therefore selects its complete
55 /// equivalence class. Duplicate selections are ignored.
56 ///
57 /// # Panics
58 ///
59 /// Panics if a selected node ID is out of bounds.
60 pub fn spiderize_nodes(
61 mut self,
62 nodes: &[NodeId],
63 ) -> Result<OpenHypergraph<O, WithSpider<A>>, FiniteFunction> {
64 let input_node_count = self.hypergraph.nodes.len();
65 for node in nodes {
66 assert!(
67 node.0 < input_node_count,
68 "node id {:?} is out of bounds",
69 node
70 );
71 }
72
73 // Quotient the input if necessary and remap the selected node IDs.
74 let remapped_nodes;
75 let nodes = if self.hypergraph.is_strict() {
76 nodes
77 } else {
78 let quotient = self.quotient()?;
79 remapped_nodes = nodes
80 .iter()
81 .map(|node| NodeId(quotient.table[node.0]))
82 .collect::<Vec<_>>();
83 remapped_nodes.as_slice()
84 };
85
86 assert_eq!(
87 self.hypergraph.edges.len(),
88 self.hypergraph.adjacency.len(),
89 "malformed hypergraph: edges and adjacency lengths differ"
90 );
91
92 // Split selected-node occurrences and record the spiders that reconnect them.
93 let spiders = rewrite_occurrences(
94 nodes,
95 &mut self.hypergraph.nodes,
96 &mut self.hypergraph.adjacency,
97 &mut self.sources,
98 &mut self.targets,
99 );
100
101 // Reuse the input graph, wrapping its existing operation labels.
102 let mut result = self.map_edges(WithSpider::Operation);
103
104 // Append the two explicit operations for each selected node.
105 for (left_spider, right_spider) in spiders {
106 result.new_edge(WithSpider::Spider, left_spider);
107 result.new_edge(WithSpider::Spider, right_spider);
108 }
109
110 Ok(result)
111 }
112}
113
114/// Append a fresh occurrence of `node`, carrying the same label.
115fn new_occurrence<O: Clone>(nodes: &mut Vec<O>, node: NodeId) -> NodeId {
116 let occurrence = NodeId(nodes.len());
117 nodes.push(nodes[node.0].clone());
118 occurrence
119}
120
121/// Replace selected node occurrences and build their spider interfaces.
122///
123/// For every selected node, the returned vector contains a pair consisting of:
124///
125/// - a left spider whose sources are global-source occurrences and whose
126/// targets are the original node followed by operation-source occurrences;
127/// - a right spider whose sources are the original node followed by
128/// operation-target occurrences and whose targets are global-target
129/// occurrences.
130///
131/// Each selected occurrence is replaced in-place with a fresh node carrying
132/// the original label. Unselected occurrences remain unchanged.
133fn rewrite_occurrences<O: Clone>(
134 selected: &[NodeId],
135 nodes: &mut Vec<O>,
136 adjacency: &mut [Hyperedge],
137 sources: &mut [NodeId],
138 targets: &mut [NodeId],
139) -> Vec<(Hyperedge, Hyperedge)> {
140 let node_count = nodes.len();
141
142 // Index spiders by original node while rewriting, but only create selected pairs.
143 let mut spiders: Vec<Option<(Hyperedge, Hyperedge)>> = (0..node_count).map(|_| None).collect();
144 for &node in selected {
145 spiders[node.0].get_or_insert_with(|| {
146 (
147 Hyperedge {
148 sources: vec![],
149 targets: vec![node],
150 },
151 Hyperedge {
152 sources: vec![node],
153 targets: vec![],
154 },
155 )
156 });
157 }
158
159 // Operation sources leave the left spider; operation targets enter the
160 // right spider.
161 for adjacency in adjacency {
162 for node in &mut adjacency.sources {
163 let original = *node;
164 if let Some((left_spider, _)) = spiders[original.0].as_mut() {
165 let occurrence = new_occurrence(nodes, original);
166 left_spider.targets.push(occurrence);
167 *node = occurrence;
168 }
169 }
170
171 for node in &mut adjacency.targets {
172 let original = *node;
173 if let Some((_, right_spider)) = spiders[original.0].as_mut() {
174 let occurrence = new_occurrence(nodes, original);
175 right_spider.sources.push(occurrence);
176 *node = occurrence;
177 }
178 }
179 }
180
181 // Global sources enter the left spider; global targets leave the right.
182 for node in sources {
183 let original = *node;
184 if let Some((left_spider, _)) = spiders[original.0].as_mut() {
185 let occurrence = new_occurrence(nodes, original);
186 left_spider.sources.push(occurrence);
187 *node = occurrence;
188 }
189 }
190
191 for node in targets {
192 let original = *node;
193 if let Some((_, right_spider)) = spiders[original.0].as_mut() {
194 let occurrence = new_occurrence(nodes, original);
195 right_spider.targets.push(occurrence);
196 *node = occurrence;
197 }
198 }
199
200 spiders.into_iter().flatten().collect()
201}