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