Skip to main content

yui_link/inv_link/
construct.rs

1//! Constructions specific to [`InvLink`] — the equivariant counterparts of the operations in
2//! [`crate::link::construct`]. Each one produces a diagram carrying a strong inversion, recovered by
3//! reindexing to the standard involution `e ↦ (n+1-e)%n+1`.
4
5use num_integer::Integer;
6
7use crate::{Edge, InvLink, Link};
8
9impl InvLink {
10    /// The 3-pretzel `P(a, b, a)` with its strong inversion (the π-rotation through the middle
11    /// band), reindexed to the standard involution. Standard convention: all-odd 3-pretzel bands
12    /// are anti-parallel, so a positive (right-handed) half-twist is a NEGATIVE crossing —
13    /// `writhe(P(a, b, a)) = -(2a + b)`.
14    pub fn sym_pretzel(a: i32, b: i32, c: i32) -> InvLink {
15        assert_eq!(a, c, "the strong inversion needs P(a, b, a)");
16        assert!(a % 2 != 0 && b % 2 != 0, "all-odd parameters required");
17
18        // the two spanning arcs are τ-fixed; `Link::pretzel` numbers from the top one, which is what
19        // the standard involution expects.
20        let link = Link::pretzel(a, b, c);
21        InvLink::from_symmetric_pd_code(link.pd_code())
22    }
23
24    // Strongly-invertible Whitehead double of a symmetric companion. The 2-cable inherits τ; the
25    // clasp and `tw` framing twists go in at the *other* on-axis edge (`is_on_axis`,
26    // e ≠ base_pt), split evenly across the axis so the diagram stays τ-invariant. `tw` counts from
27    // the Seifert framing and must be even. The base point lands on the doubled on-axis strand.
28    pub fn whitehead_double(l: &InvLink, positive: bool, tw: i32) -> InvLink {
29        let base = l.base_pt().expect("companion needs a base point");
30        let cut = l.on_axis_edges().into_iter()
31            .find(|&e| e != base)
32            .expect("need a second on-axis edge for the clasp");
33        Self::whitehead_double_at(l, positive, tw, cut)
34    }
35
36    // The same, with the clasp placed at a chosen on-axis edge. The axis meets the knot twice, so
37    // `cut` is the on-axis edge that does not carry the base point.
38    pub fn whitehead_double_at(l: &InvLink, positive: bool, tw: i32, cut: Edge) -> InvLink {
39        assert!(l.is_knot(), "the companion must be a knot");
40        assert!(l.is_strongly_invertible(), "the companion must be strongly invertible");
41        assert!(tw.is_even(), "tw must be even for a τ-symmetric diagram");
42        assert_eq!(l.inv_edge(cut), cut, "the clasp edge {cut} must be on-axis");
43
44        let base = l.base_pt().expect("companion needs a base point");
45        assert_eq!(l.inv_edge(base), base, "base point must be on-axis");
46        assert_ne!(cut, base, "the clasp cannot sit at the base point");
47
48        let half = l.writhe() + tw / 2;   // (2·writhe + tw) / 2 = half the blackboard framing
49        // the double is based at a doubled copy of `base`, which lies on the axis — that pins τ.
50        let inner = Link::whitehead_double_impl(l.inner(), positive, half, half, cut, Some(base));
51        Self::si_knot_from(inner)
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use itertools::Itertools;
59    use crate::misc::det;
60
61    #[test]
62    fn sym_pretzel_is_symmetric() {
63        // construction succeeding ⟺ the standard involution is realized by the pretzel numbering.
64        let k = InvLink::sym_pretzel(-3, 3, -3);
65        assert!(k.is_knot());
66        assert_eq!(k.n_crossings(), 9);
67        assert_eq!(det(k.inner()), 9);  // |ab + bc + ca|
68        assert_eq!(k.writhe(), 3);      // = -(2a + b) for anti-parallel bands
69    }
70
71    #[test]
72    fn whitehead_double_clasp_placement() {
73        // The axis meets the knot twice, so the clasp has exactly two possible homes. They give
74        // different diagrams in general; for P(a, b, a) the pretzel's extra symmetry makes them
75        // agree, up to relabelling (also checked for P(-5,5,-5) in experiments/link_check).
76        let k = InvLink::sym_pretzel(-3, 3, -3);
77        let axis = k.on_axis_edges();
78        assert_eq!(axis.len(), 2, "a strong inversion fixes exactly two edges");
79
80        for positive in [true, false] {
81            // swapping the roles of the two on-axis edges: each takes a turn holding the base point,
82            // and the clasp goes to the other.
83            let ds = axis.iter().map(|&base| {
84                let kb = k.clone().with_base_pt(base);
85                InvLink::whitehead_double_at(&kb, positive, 0, other(&axis, base))
86            }).collect_vec();
87            let canon = |k: &InvLink| k.inner().reindexed_canon();
88            assert_eq!(canon(&ds[0]), canon(&ds[1]), "the two clasp placements differ");
89        }
90    }
91
92    fn other(axis: &[Edge], e: Edge) -> Edge {
93        *axis.iter().find(|&&f| f != e).unwrap()
94    }
95
96    // tw shifts the blackboard framing 2*writhe by tw, and every extra full twist costs a crossing
97    // on each of the two parallel strands.
98    #[test]
99    fn whitehead_double_twisted() {
100        let k = InvLink::test_data("3_1");
101        assert_eq!(k.writhe(), 3);
102
103        for tw in [-2, 0, 2, 4] {
104            let w = InvLink::whitehead_double(&k, true, tw);
105            let expected = 4 * k.n_crossings() + (2 * k.writhe() + tw).unsigned_abs() as usize + 2;
106            assert_eq!(w.n_crossings(), expected, "tw = {tw}");
107            assert!(w.is_knot(), "tw = {tw}");
108            assert!(w.is_strongly_invertible(), "tw = {tw}");
109        }
110    }
111
112    #[test]
113    #[should_panic(expected = "tw must be even")]
114    fn whitehead_double_rejects_an_odd_twist() {
115        // an odd twist breaks the tau-symmetry of the diagram.
116        let _ = InvLink::whitehead_double(&InvLink::test_data("3_1"), true, 1);
117    }
118
119    #[test]
120    fn whitehead_double_is_symmetric() {
121        // building succeeding ⟺ the pairing gave a valid strong inversion.
122        for name in ["3_1", "4_1"] {
123            let k = InvLink::test_data(name);
124            let w = InvLink::whitehead_double(&k, true, 0);
125            assert!(w.is_knot());
126            assert!(w.is_strongly_invertible());
127            let base = w.base_pt().expect("the double is based");
128            assert_eq!(w.inv_edge(base), base, "base point is on-axis");
129            // 4·n_crossings (cable) + |blackboard framing| (split each side) + 2 (clasp)
130            let bl = (2 * k.writhe()).unsigned_abs() as usize;
131            assert_eq!(w.n_crossings(), 4 * k.n_crossings() + bl + 2);
132        }
133    }
134}