1use std::collections::{BTreeMap, HashMap};
5use std::fmt::Display;
6
7use petgraph::stable_graph::{StableDiGraph, NodeIndex, EdgeIndex};
8use yui_core::algo::UnionFind;
9
10use crate::{Link, Node, NodeType, Edge, Slot};
11
12pub type Port = (NodeIndex, usize);
18
19#[derive(Debug, Default)]
22pub struct LinkBuilder {
23 graph: StableDiGraph<NodeType, (usize, usize)>,
24 loops: usize,
25}
26
27impl LinkBuilder {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn n_nodes(&self) -> usize {
33 self.graph.node_count()
34 }
35
36 pub fn add_node(&mut self, node_type: NodeType) -> NodeIndex {
38 self.graph.add_node(node_type)
39 }
40
41 pub fn add_crossing(&mut self, node_type: NodeType) -> NodeIndex {
42 assert!(matches!(node_type, NodeType::XL | NodeType::XR), "add_crossing expects XL or XR, got {node_type}");
43 self.add_node(node_type)
44 }
45
46 pub fn connect(&mut self, a: Port, b: Port) {
48 assert!(a.1 < 4 && b.1 < 4, "slots must be 0..4, got {} and {}", a.1, b.1);
49 self.graph.add_edge(a.0, b.0, (a.1, b.1));
50 }
51
52 pub fn add_loop(&mut self) {
54 self.loops += 1;
55 }
56
57 pub fn add_v_twist(&mut self, tt: NodeType, k: usize) -> (Port, Port, Port, Port) {
60 assert!(k >= 1, "a twist needs at least one crossing");
61 let v: Vec<_> = (0..k).map(|_| self.add_crossing(tt)).collect();
62 for w in v.windows(2) {
63 self.connect((w[0], 3), (w[1], 0));
64 self.connect((w[0], 2), (w[1], 1));
65 }
66 ((v[0], 0), (v[0], 1), (v[k - 1], 2), (v[k - 1], 3))
67 }
68
69 pub fn add_h_twist(&mut self, tt: NodeType, k: usize) -> (Port, Port, Port, Port) {
72 assert!(k >= 1, "a twist needs at least one crossing");
73 let h: Vec<_> = (0..k).map(|_| self.add_crossing(tt)).collect();
74 for w in h.windows(2) {
75 self.connect((w[0], 1), (w[1], 0));
76 self.connect((w[0], 2), (w[1], 3));
77 }
78 ((h[0], 0), (h[k - 1], 1), (h[k - 1], 2), (h[0], 3))
79 }
80
81 pub fn add_link(&mut self, l: &Link) -> Vec<NodeIndex> {
84 let verts: Vec<NodeIndex> = l.nodes().map(|x| self.add_node(x.node_type())).collect();
85
86 let occ = l.nodes().enumerate().flat_map(|(i, x)| {
88 let v = verts[i];
89 Slot::ALL.map(move |s| (x.edge(s), (v, s.index())))
90 }).fold(BTreeMap::<Edge, Vec<Port>>::new(), |mut occ, (e, p)| {
91 occ.entry(e).or_default().push(p);
92 occ
93 });
94
95 for ports in occ.values() {
96 self.connect(ports[0], ports[1]);
97 }
98 for _ in l.loops() {
99 self.add_loop();
100 }
101 verts
102 }
103
104 pub fn disconnect(&mut self, p: Port) {
106 let e = self.graph.edge_indices().find(|&e| {
107 let (a, b) = self.ends(e);
108 a == p || b == p
109 }).unwrap_or_else(||
110 panic!("port (node {}, slot {}) has no edge to disconnect", p.0.index(), p.1)
111 );
112 self.graph.remove_edge(e);
113 }
114
115 pub fn edge_at(&self, p: Port) -> Option<Edge> {
118 self.graph.edge_indices().enumerate().find_map(|(i, e)| {
119 let (a, b) = self.ends(e);
120 (a == p || b == p).then_some(i as Edge + 1)
121 })
122 }
123
124 pub fn build(self) -> Result<Link, LinkError> {
125 self.build_with(|_, _| true) }
127
128 pub fn build_with<F>(self, is_incoming: F) -> Result<Link, LinkError>
131 where F: Fn(usize, usize) -> bool {
132 self.validate()?;
133
134 let edge_at: HashMap<Port, Edge> = self.graph.edge_indices().enumerate().flat_map(|(i, e)| {
136 let (a, b) = self.ends(e);
137 let id = i as Edge + 1;
138 [(a, id), (b, id)]
139 }).collect();
140
141 let nodes = self.graph.node_indices().map(|v|
142 Node::new(self.graph[v], None, [0, 1, 2, 3].map(|s| edge_at[&(v, s)]))
143 );
144
145 let e0 = self.graph.edge_count() as Edge + 1;
146 let loops = e0 .. e0 + self.loops as Edge;
147
148 let mut l = Link::new(nodes.collect::<Vec<_>>(), loops);
149 l.reorient(|i, s| is_incoming(i, s.index()));
150 Ok(l)
151 }
152
153 fn validate(&self) -> Result<(), LinkError> {
155 let alpha = self.edge_pairing()?;
156 if self.is_planar_with(&alpha) {
157 Ok(())
158 } else {
159 Err(LinkError::NonPlanar)
160 }
161 }
162
163 pub fn is_planar(&self) -> bool {
165 self.edge_pairing().map(|alpha|
166 self.is_planar_with(&alpha)
167 ).unwrap_or(false)
168 }
169
170 fn is_planar_with(&self, alpha: &HashMap<Port, Port>) -> bool {
174 let (v, e) = (self.graph.node_count(), self.graph.edge_count());
175
176 let dart = |(n, s): &Port| 4 * n.index() + s;
177 let phi = |p: &Port| {
178 let (n, s) = alpha[p];
179 (n, (s + 1) % 4)
180 };
181
182 let mut faces = UnionFind::new(4 * v);
184 let mut comps = UnionFind::new(v);
185 for p in alpha.keys() {
186 faces.union(dart(p), dart(&phi(p)));
187 comps.union(p.0.index(), alpha[p].0.index());
188 }
189 let (f, c) = (faces.into_disjoint().len(), comps.into_disjoint().len());
190
191 (v + f) as isize - e as isize == 2 * c as isize
192 }
193
194 fn edge_pairing(&self) -> Result<HashMap<Port, Port>, LinkError> {
197 let mut alpha = HashMap::new();
198 for e in self.graph.edge_indices() {
199 let (a, b) = self.ends(e);
200 for (p, q) in [(a, b), (b, a)] {
201 if alpha.insert(p, q).is_some() {
202 return Err(LinkError::DuplicatePort(p));
203 }
204 }
205 }
206
207 let open = self.graph.node_indices().flat_map(|v|
208 (0..4).map(move |s| (v, s))
209 ).find(|p|
210 !alpha.contains_key(p)
211 );
212 match open {
213 Some(p) => Err(LinkError::UnconnectedPort(p)),
214 None => Ok(alpha),
215 }
216 }
217
218 fn ends(&self, e: EdgeIndex) -> (Port, Port) {
220 let (s, t) = self.graph.edge_endpoints(e).unwrap();
221 let &(fs, ts) = self.graph.edge_weight(e).unwrap();
222 ((s, fs), (t, ts))
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum LinkError {
228 UnconnectedPort(Port),
230 DuplicatePort(Port),
232 NonPlanar,
234}
235
236impl Display for LinkError {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 match self {
239 LinkError::UnconnectedPort((v, s)) =>
240 write!(f, "port (node {}, slot {s}) is not connected", v.index()),
241 LinkError::DuplicatePort((v, s)) =>
242 write!(f, "port (node {}, slot {s}) is connected more than once", v.index()),
243 LinkError::NonPlanar =>
244 write!(f, "the wiring is non-planar (genus > 0)"),
245 }
246 }
247}
248
249impl std::error::Error for LinkError {}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::misc::jones_polynomial;
255
256 fn rebuild(l: &Link) -> Link {
257 let mut b = LinkBuilder::new();
258 b.add_link(l);
259 b.build().unwrap()
260 }
261
262 #[test]
263 fn round_trip_preserves_knot() {
264 for name in ["3_1", "4_1", "5_2", "6_2"] {
265 let l = Link::test_data(name);
266 let r = rebuild(&l);
267 assert_eq!(r.n_comps(), l.n_comps());
268 assert!(r.is_oriented());
269 assert_eq!(jones_polynomial(&r), jones_polynomial(&l), "round-trip changed {name}");
270 }
271 }
272
273 #[test]
274 fn round_trip_orients_unlink2() {
275 let l = Link::test_data("unlink2");
278 assert!(!l.is_oriented());
279
280 let r = rebuild(&l);
281 assert_eq!(r.n_comps(), 2);
282 assert!(r.is_oriented());
283 assert_eq!(jones_polynomial(&r), jones_polynomial(&Link::unlink(2)));
284 }
285
286 #[test]
287 fn is_planar_detects_genus() {
288 let mut planar = LinkBuilder::new();
290 let x = planar.add_crossing(NodeType::XR);
291 planar.connect((x, 0), (x, 1));
292 planar.connect((x, 2), (x, 3));
293 assert!(planar.is_planar());
294
295 let mut torus = LinkBuilder::new();
296 let y = torus.add_crossing(NodeType::XR);
297 torus.connect((y, 0), (y, 2));
298 torus.connect((y, 1), (y, 3));
299 assert!(!torus.is_planar());
300 assert!(matches!(torus.build(), Err(LinkError::NonPlanar)));
301 }
302
303 #[test]
304 fn is_planar_accepts_real_knots() {
305 for name in ["3_1", "4_1", "5_1", "5_2", "6_1", "6_2", "6_3", "7_1", "7_2"] {
307 let mut b = LinkBuilder::new();
308 b.add_link(&Link::test_data(name));
309 assert!(b.is_planar(), "{name} is planar but is_planar returned false");
310 }
311 }
312
313 #[test]
314 fn build_with_free_loop() {
315 let mut b = LinkBuilder::new();
316 b.add_loop();
317 let l = b.build().unwrap();
318 assert_eq!(l.n_comps(), 1);
319 assert_eq!(l.n_crossings(), 0);
320 }
321
322 #[test]
323 fn unconnected_port_errors() {
324 let mut b = LinkBuilder::new();
325 b.add_crossing(NodeType::XR);
326 assert!(matches!(b.build(), Err(LinkError::UnconnectedPort(_))));
327 }
328}