Skip to main content

yui_link/link/
construct.rs

1//! Constructions producing new links from patterns — twist knots, cables and satellites.
2//! Contrast with [`crate::link::link_ops`], which operates on links you already have.
3
4use std::collections::HashSet;
5use petgraph::stable_graph::NodeIndex;
6use crate::{Link, Edge, LinkBuilder, Port};
7
8impl Link {
9    // Twist knot = numerator closure of the rational tangle [n, 2]: a horizontal |n|-twist region
10    // summed with a vertical 2-twist clasp. twist_knot(0) = unknot, (1,2,3,4) = 3_1, 4_1, 5_2, 6_1;
11    // n < 0 mirrors.
12    pub fn twist_knot(n: i32) -> Link {
13        use crate::NodeType::{XL, XR};
14        let mut b = LinkBuilder::new();
15
16        // the vertical 2-twist clasp; corners (v0, v1, v2, v3) = (SW, SE, NE, NW).
17        let (v0, v1, v2, v3) = b.add_v_twist(XR, 2);
18
19        if n == 0 {
20            b.connect(v3, v2);
21            b.connect(v0, v1);   // N([2] clasp) = unknot
22            return b.build().unwrap();
23        }
24
25        // the horizontal |n|-twist region.
26        let tt = if n >= 0 { XR } else { XL };
27        let (h0, h1, h2, h3) = b.add_h_twist(tt, n.unsigned_abs() as usize);
28
29        b.connect(v0, h3);
30        b.connect(v3, h0);
31        b.connect(v1, h2);
32        b.connect(v2, h1);
33        b.build().unwrap()
34    }
35
36    // The 3-pretzel P(a, b, c): three vertical twist regions of |a|, |b|, |c| half-twists, chained by
37    // their inner arcs and closed by one spanning arc on the top and one on the bottom. A positive
38    // parameter is a right-handed (XR) region; all three must be nonzero. Edge 1 is the top spanning
39    // arc.
40    pub fn pretzel(a: i32, b: i32, c: i32) -> Link {
41        use crate::NodeType::{XL, XR};
42        assert!(a != 0 && b != 0 && c != 0, "pretzel parameters must be nonzero");
43
44        let mut bld = LinkBuilder::new();
45        let ends: Vec<_> = [a, b, c].iter().map(|&v| {
46            let ty = if v > 0 { XR } else { XL };
47            bld.add_v_twist(ty, v.unsigned_abs() as usize) // (sw, se, ne, nw)
48        }).collect();
49        let (sw1, se1, ne1, nw1) = ends[0];
50        let (sw2, se2, ne2, nw2) = ends[1];
51        let (sw3, se3, ne3, nw3) = ends[2];
52
53        bld.connect(ne1, nw2);
54        bld.connect(ne2, nw3);
55        bld.connect(se1, sw2);
56        bld.connect(se2, sw3);
57        bld.connect(nw1, ne3); // spanning top
58        bld.connect(sw1, se3); // spanning bottom
59
60        let start = bld.edge_at(nw1).unwrap();
61        bld.build().expect("pretzel must be planar").reindexed(start, 1)
62    }
63
64    // Blackboard-framed 2-cable: each crossing → a 2×2 block of 4 sub-crossings of the same type,
65    // each edge → 2 parallel edges. Every component doubles into its two parallel copies
66    // (n components → 2n; framing = the diagram's writhe per component).
67    pub fn cable2(l: &Link) -> Link {
68        let (b, cab) = Self::cable2_builder(l);
69
70        // both copies must enter where the companion does — left free, `build` picks the
71        // anti-parallel orientation and the cross-copy crossings cancel the rest.
72        let incoming: HashSet<Port> = l.nodes().enumerate().flat_map(|(i, x)| {
73            let (p, q) = x.incoming().expect("cable2 needs an oriented diagram");
74            [p, q].map(|s| cab[i][s.index()])
75        }).flat_map(|(a0, a1)|
76            [a0, a1]
77        ).collect();
78
79        b.build_with(|i, s| incoming.contains(&(NodeIndex::new(i), s))).unwrap()
80    }
81
82    // The 2-cable in an open builder, plus `cab[i][slot.index()] = (copy-0 port, copy-1 port)` so callers can
83    // re-splice the cable (e.g. the Whitehead clasp) before building.
84    fn cable2_builder(l: &Link) -> (LinkBuilder, Vec<[(Port, Port); 4]>) {
85        let mut b = LinkBuilder::new();
86
87        // per crossing: 4 sub-crossings + the 4 internal edges; record the two cable ports at each slot
88        let cab: Vec<[(Port, Port); 4]> = l.nodes().map(|x| {
89            let t = x.node_type();
90            let [s0, s1, s2, s3] = [b.add_node(t), b.add_node(t), b.add_node(t), b.add_node(t)];
91            b.connect((s0, 2), (s1, 0));
92            b.connect((s0, 3), (s2, 1));
93            b.connect((s1, 3), (s3, 1));
94            b.connect((s2, 2), (s3, 0));
95            // cab[slot] = the slot's two cable ports in a consistent (CCW) order, so edge-joins line up.
96            [((s2, 0), (s0, 0)), ((s0, 1), (s1, 1)), ((s1, 2), (s3, 2)), ((s3, 3), (s2, 3))]
97        }).collect();
98
99        // join the two cables of each original edge across its two endpoints. the two ends list their
100        // ports in CCW order, which reverses along the edge, so copy 0 pairs with the other's copy 1.
101        for e in l.edges() {
102            let ((i, s), (j, t)) = l.edge_ends(e, false);
103            let ((a0, a1), (b0, b1)) = (cab[i][s.index()], cab[j][t.index()]);
104            b.connect(a0, b1);
105            b.connect(a1, b0);
106        }
107        for _ in l.loops() {   // each free loop doubles
108            b.add_loop();
109            b.add_loop();
110        }
111        (b, cab)
112    }
113
114    // The `tw`-twisted Whitehead double D±(K), `tw` from the Seifert (0) framing (tw = 0 = untwisted,
115    // trivial Alexander); `positive` = clasp sign. Seifert sits at 2·writhe blackboard half-twists.
116    pub fn whitehead_double(l: &Link, positive: bool, tw: i32) -> Link {
117        Self::whitehead_double_bbf(l, positive, 2 * l.writhe() + tw)
118    }
119
120    // Whitehead double with the framing counted from the blackboard framing (tw = 0 = the diagram's
121    // blackboard 2-cable): cut the cable to a 4-end tangle, add `tw` half-twists, close with the clasp.
122    pub fn whitehead_double_bbf(l: &Link, positive: bool, tw: i32) -> Link {
123        // cut a clean edge (joining two distinct crossings): frees the 4 cable ends
124        let cut = l.edges().into_iter()
125            .find(|&e| {
126                let ((i, _), (j, _)) = l.edge_ends(e, false);
127                i != j
128            })
129            .expect("the companion needs an edge joining two distinct crossings");
130        Self::whitehead_double_impl(l, positive, 0, tw, cut, None)
131    }
132
133    // Whitehead double cutting the cable at edge `cut` (must join two distinct crossings), placing
134    // `tw_a` framing half-twists on one side of the cut and `tw_b` on the other. The result is based
135    // at one of `base`'s two doubled strands.
136    pub(crate) fn whitehead_double_impl(l: &Link, positive: bool, tw_a: i32, tw_b: i32, cut: Edge, base: Option<Edge>) -> Link {
137        use crate::NodeType::{XL, XR};
138        assert!(l.is_knot(), "the Whitehead double requires a knot companion");
139        assert!(base.is_none_or(|b| b != cut), "the base point must be away from the cut");
140
141        let (mut b, cab) = Self::cable2_builder(l);
142
143        // the cut edge's two ends: the a-side (node ia, slot sa) and b-side (ib, sb), with their
144        // cable ports (a0, a1) / (b0, b1) in CCW order.
145        let ((ia, sa), (ib, sb)) = l.edge_ends(cut, false);
146        let ((a0, a1), (b0, b1)) = (cab[ia][sa.index()], cab[ib][sb.index()]);
147        b.disconnect(a0);   // the swapped join means a0–b1, a1–b0 are removed
148        b.disconnect(a1);
149
150        // The insert replaces the parallel cable strands (CCW at the cut ⇒ (a0, a1) = (lower, upper),
151        // (b0, b1) = (upper, lower)):
152        //
153        //   a1 ────[       ]────[       ]────[       ]──── b0   (upper strand)
154        //          [ row_a ]    [ clasp ]    [ row_b ]
155        //   a0 ────[       ]────[       ]────[       ]──── b1   (lower strand)
156        //
157        // The clasp turns each side's pair back on itself and the turn-backs hook (winding 0).
158        // Corners are destructured by picture position (ul/ur/ll/lr = upper/lower × left/right);
159        // the wiring is identical for every piece: enter at (ul, ll), continue from (ur, lr).
160        let twist_type = |tw: i32| if tw >= 0 { XR } else { XL };
161        let (mut upper, mut lower) = (a1, a0);
162
163        if tw_a != 0 {
164            let (ll, lr, ur, ul) = b.add_h_twist(twist_type(tw_a), tw_a.unsigned_abs() as usize);
165            b.connect(upper, ul);
166            b.connect(lower, ll);
167            (upper, lower) = (ur, lr);
168        }
169
170        // positive = a positive clasp = D⁺ (pinned by the clasp-sign determinant test).
171        let ct = if positive { XL } else { XR };
172        let (ll, lr, ur, ul) = b.add_v_twist(ct, 2);
173        b.connect(upper, ul);
174        b.connect(lower, ll);
175        (upper, lower) = (ur, lr);
176
177        if tw_b != 0 {
178            let (ll, lr, ur, ul) = b.add_h_twist(twist_type(tw_b), tw_b.unsigned_abs() as usize);
179            b.connect(upper, ul);
180            b.connect(lower, ll);
181            (upper, lower) = (ur, lr);
182        }
183
184        b.connect(upper, b0);
185        b.connect(lower, b1);
186
187        // `base` is away from the cut, so its cable join survives: `cab` holds the two ports its
188        // doubled strands run through. Read the id off before `build` consumes the builder.
189        let base_pt = base.map(|e| {
190            let ((i, s), _) = l.edge_ends(e, false);
191            b.edge_at(cab[i][s.index()].0).unwrap()
192        });
193
194        let double = b.build().unwrap();
195        match base_pt {
196            Some(e) => double.with_base_pt(e),
197            None => double,
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::Braid;
206    use crate::misc::{jones_polynomial, det};
207
208    fn same_knot(a: &Link, b: &Link) -> bool {
209        let ja = jones_polynomial(a);
210        ja == jones_polynomial(b) || ja == jones_polynomial(&b.mirror())
211    }
212
213    #[test]
214    fn braid_closures_are_jones_faithful() {
215        assert!(same_knot(&Braid::from([1, 1, 1]).closure(), &Link::test_data("3_1")));
216        assert!(same_knot(&Braid::from([1, -2, 1, -2]).closure(), &Link::test_data("4_1")));
217    }
218
219    #[test]
220    fn twist_knot_matches_references() {
221        // twist_knot(n) vs the independent PD-code reference, by Jones (mirror allowed);
222        // twist_knot(-1-n) is the mirror.
223        let table = ["3_1", "4_1", "5_2", "6_1", "7_2"];
224        for (i, name) in table.iter().enumerate() {
225            let n = i as i32 + 1;
226            assert!(same_knot(&Link::twist_knot(n), &Link::test_data(name)), "twist_knot({n}) ≠ {name}");
227            assert!(same_knot(&Link::twist_knot(-1 - n), &Link::test_data(name)), "twist_knot({}) ≠ mirror {name}", -1 - n);
228        }
229    }
230
231
232    #[test]
233    fn twist_knot_determinants() {
234        // twist knot K_n is a knot of determinant 2n+1 (3_1, 4_1, 5_2, … → 3, 5, 7, …); K_0 = unknot.
235        // negative n is the mirror (twist_knot(-1-n) = mirror K_n), with the same determinant.
236        for n in 0..=6 {
237            let k = Link::twist_knot(n);
238            assert_eq!(k.n_comps(), 1);
239            assert!(k.is_oriented());
240            assert_eq!(det(&k), 2 * n + 1, "det twist_knot({n})");
241            assert_eq!(det(&Link::twist_knot(-1 - n)), 2 * n + 1, "det twist_knot({})", -1 - n);
242        }
243    }
244
245    #[test]
246    fn pretzel_determinants() {
247        // det P(a, b, c) = |ab + bc + ca| — includes the (-2, 3, 7)-pretzel (det 1). `det` sums over
248        // all 2^n resolutions, so the 15-crossing cases are left to `pretzel_band_symmetries`.
249        // link-valued parameters (two or more even) are included: those are 2- and 3-component
250        // pretzel links, which the renumbering has to carry as well as knots.
251        for (a, b, c) in [(1, 1, 1), (-1, -1, -1), (1, 3, 5), (-2, 3, 7), (2, 2, 2), (2, 2, 3), (2, 2, -3)] {
252            let l = Link::pretzel(a, b, c);
253            let n = (a.unsigned_abs() + b.unsigned_abs() + c.unsigned_abs()) as usize;
254            assert_eq!(l.n_crossings(), n, "P({a},{b},{c}) crossing count");
255            assert_eq!(det(&l), (a * b + b * c + c * a).abs(), "det P({a},{b},{c})");
256            assert_eq!(l.edges(), (1..=l.n_edges() as Edge).collect::<Vec<_>>(), "P({a},{b},{c}) numbering");
257        }
258    }
259
260    #[test]
261    fn pretzel_band_symmetries() {
262        // the three bands sit in a cycle, so rotating them — or reversing their order — leaves the
263        // diagram itself unchanged, not merely the knot type.
264        // Canonical form of the *unoriented* diagram: least PD code over all start edges and both
265        // strand directions. Rotating or reversing the bands may reverse the direction (it does
266        // whenever a band is even), so the orientation must be quotiented out here.
267        let canon = |l: &Link| {
268            // reverse the strand: the under-strand enters at the far end, CCW order unchanged.
269            let rev = Link::from_pd_code(l.pd_code().into_iter().map(|[a, b, c, d]| [c, d, a, b]));
270            l.reindexed_canon().pd_code().min(rev.reindexed_canon().pd_code())
271        };
272
273        for (a, b, c) in [(1, 3, 5), (3, 5, 7), (-3, 3, -3), (-2, 3, 7), (-5, 5, -5)] {
274            let p = canon(&Link::pretzel(a, b, c));
275            assert_eq!(canon(&Link::pretzel(b, c, a)), p, "cyclic P({a},{b},{c})");
276            assert_eq!(canon(&Link::pretzel(c, a, b)), p, "cyclic P({a},{b},{c})");
277            assert_eq!(canon(&Link::pretzel(c, b, a)), p, "reversal P({a},{b},{c})");
278        }
279    }
280
281    #[test]
282    fn mirror_identities() {
283        // mirroring flips every band, every cable crossing and the clasp.
284        for (a, b, c) in [(1, 3, 5), (3, 5, 7), (-2, 3, 7), (-5, 5, -5)] {
285            assert_eq!(Link::pretzel(a, b, c).mirror().pd_code(), Link::pretzel(-a, -b, -c).pd_code(),
286                "mirror P({a},{b},{c})");
287        }
288        for name in ["3_1", "4_1", "5_2"] {
289            let k = Link::test_data(name);
290            assert_eq!(Link::cable2(&k).mirror().pd_code(), Link::cable2(&k.mirror()).pd_code(),
291                "mirror cable2({name})");
292            assert_eq!(Link::whitehead_double(&k, true, 0).mirror().pd_code(),
293                       Link::whitehead_double(&k.mirror(), false, 0).pd_code(),
294                "mirror D+({name}) vs D-(mirror {name})");
295        }
296    }
297
298    #[test]
299    fn pretzel_trefoil() {
300        assert!(same_knot(&Link::pretzel(1, 1, 1), &Link::test_data("3_1")));
301    }
302
303    #[test]
304    #[ignore = "slow: det sums over all 2^n resolutions, and the framing test needs 14-crossing doubles"]
305    fn whitehead_double_of_unknot() {
306        // the untwisted double of the unknot is the unknot, from any companion diagram (framing 2·writhe).
307        for word in [vec![1, -2], vec![1, 2], vec![-1, -2]] {
308            let u = Braid::from_iter(word).closure();
309            for positive in [true, false] {
310                let d = Link::whitehead_double(&u, positive, 0);
311                assert_eq!(d.n_comps(), 1);
312                assert_eq!(det(&d), 1, "D±(unknot) must be the unknot (trivial Alexander)");
313            }
314        }
315    }
316
317    #[test]
318    fn whitehead_double_is_a_knot() {
319        // D±(K) of a nontrivial companion builds (exercising the planarity check) and is a knot with
320        // 4·(crossings) + 2·|writhe| (framing) + 2 (clasp) crossings. det would confirm untwisted but
321        // is exponential here — see _of_unknot.
322        for name in ["3_1", "4_1", "5_2", "6_1"] {
323            let k = Link::test_data(name);
324            let expected = 4 * k.n_crossings() + 2 * k.writhe().unsigned_abs() as usize + 2;
325            for positive in [true, false] {
326                let d = Link::whitehead_double(&k, positive, 0);
327                assert_eq!(d.n_comps(), 1, "D±({name}) is a knot");
328                assert_eq!(d.n_crossings(), expected, "D±({name}) crossing count");
329                assert!(d.is_oriented());
330            }
331        }
332    }
333
334    #[test]
335    fn whitehead_double_clasp_sign() {
336        // twisted doubles of the unknot are twist knots, whose determinants separate the clasp
337        // signs; values pinned against the Kh-verified reference implementation. `tw` counts
338        // half-twists, and det is 2*tw ∓ 1 — two values are enough to pin that line, and the
339        // smallest two keep the 2^n determinant cheap.
340        let u = Braid::from([1, -2]).closure(); // writhe-0 unknot diagram
341        for (tw, pos_det, neg_det) in [(1, 1, 3), (2, 3, 5)] {
342            assert_eq!(det(&Link::whitehead_double(&u, true, tw)), pos_det, "D+(U, tw={tw})");
343            assert_eq!(det(&Link::whitehead_double(&u, false, tw)), neg_det, "D-(U, tw={tw})");
344        }
345    }
346
347    #[test]
348    fn cable2_trefoil() {
349        let k = Link::test_data("3_1");
350        let c = Link::cable2(&k);
351        assert_eq!(c.n_crossings(), 4 * k.n_crossings());
352        assert_eq!(c.n_comps(), 2, "2-cable of a knot is a 2-component link");
353        assert!(c.is_oriented());
354        let _ = c.seifert_circles(); // exercises orientation consistency
355    }
356
357    #[test]
358    fn cable2_is_parallel() {
359        // the two copies run the same way, so every sub-crossing keeps the companion's sign and
360        // the cable carries the blackboard framing. Anti-parallel copies would cancel to 0.
361        for name in ["3_1", "5_1", "5_2"] {
362            let k = Link::test_data(name);
363            let c = Link::cable2(&k);
364            assert_eq!(c.writhe(), 4 * k.writhe(), "cable2({name}) is not parallel");
365        }
366    }
367
368    #[test]
369    fn cable2_is_hopf() {
370        // 2-cable of a ±1-framed unknot is the Hopf link (linking ±1), not the 2-component unlink.
371        let c = Link::cable2(&Link::test_data("unknot_l_twist"));
372        assert_eq!(c.n_comps(), 2);
373        assert_ne!(jones_polynomial(&c), jones_polynomial(&Link::unlink(2)),
374            "2-cable of a framed unknot must be linked (Hopf), not the unlink");
375    }
376
377}