You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

719 lines
24 KiB
Plaintext

#import "theme.typ": ams-article, definition, theorem, proposition, proof, todo
#import "@preview/algorithmic:0.1.0"
#import algorithmic: algorithm
#import "@preview/cetz:0.3.4"
#let kL = $L$
#let draw-strand(polyline, style: (:)) = {
import cetz.draw: *
set-style(
..cetz.styles.resolve(
(stroke: (paint: white, thickness: 5pt, cap: "butt")),
base: style,
),
)
polyline
set-style(
..cetz.styles.resolve(
style,
base: (stroke: (paint: black, thickness: 0.75pt, cap: "round")),
),
)
polyline
set-style(stroke: (paint: black, thickness: 0.75pt, cap: "round"))
}
#let skein-canvas = body => cetz.canvas(
length: 0.25cm,
padding: 0.25,
{
import cetz.draw: *
rect((-1, -1), (1, 1), fill: white, stroke: none)
body
},
)
#let arrow-size = 0.35
#let draw-arrow((x, y), angle: 0, ..style) = {
import cetz.draw: *
{
let len = arrow-size / calc.sqrt(2)
set-style(..style)
translate(x: x, y: y)
rotate(z: angle)
line((0, 0), (-len, +len), ..style)
line((0, 0), (-len, -len), ..style)
}
}
#let skein = (
unit: skein-canvas({
import cetz.draw: *
circle((0, 0), radius: 1, stroke: (paint: black, thickness: 0.75pt))
}),
over: skein-canvas({
import cetz.draw: *
draw-strand({ line((-1, -1), (1, 1)) })
draw-strand({ line((-1, 1), (1, -1)) })
}),
under: skein-canvas({
import cetz.draw: *
draw-strand({ line((-1, 1), (1, -1)) })
draw-strand({ line((-1, -1), (1, 1)) })
}),
h: skein-canvas({
import cetz.draw: *
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
}),
v: skein-canvas({
import cetz.draw: *
draw-strand({ hobby((-1, -1), (-0.61, 0), (-1, 1), omega: 1) })
draw-strand({ hobby((1, -1), (+0.61, 0), (1, 1), omega: 1) })
}),
strand: skein-canvas({
import cetz.draw: *
rect((-1, -1), (1, 1), fill: white, stroke: none)
draw-strand({ hobby((-1, 0), (0, 0.25), (1, 0), omega: 1) })
}),
over-twist: skein-canvas({
import cetz.draw: *
draw-strand({ hobby((1.5, +1), (1, +1), (-0.5, 0), (-0.1, -1), (0, -1)) })
draw-strand({ hobby((-1.5, +1), (-1, +1), (0.5, 0), (0.1, -1), (0, -1)) })
}),
under-twist: skein-canvas({
import cetz.draw: *
draw-strand({ hobby((-1.5, +1), (-1, +1), (0.5, 0), (0.1, -1), (0, -1)) })
draw-strand({ hobby((1.5, +1), (1, +1), (-0.5, 0), (-0.1, -1), (0, -1)) })
}),
)
#let skein-generic(
kind: "over",
direction: (+1, +1),
arrows: (true, true),
styles: ((:), (:)),
) = {
skein-canvas({
import cetz.draw: *
if kind == "over" {
draw-strand({ line((-1, -1), (1, 1)) }, style: styles.at(1))
draw-strand({ line((-1, 1), (1, -1)) }, style: styles.at(0))
}
if kind == "under" {
draw-strand({ line((-1, 1), (1, -1)) }, style: styles.at(0))
draw-strand({ line((-1, -1), (1, 1)) }, style: styles.at(1))
}
if arrows.at(0) {
if direction.at(0) == +1 {
line((1 - arrow-size, -1), (1, -1), (1, -1 + arrow-size), ..styles.at(0))
} else {
line((-1, 1 - arrow-size), (-1, 1), (-1 + arrow-size, 1), ..styles.at(0))
}
}
if arrows.at(1) {
if direction.at(1) == +1 {
line((1 - arrow-size, 1), (1, 1), (1, 1 - arrow-size), ..styles.at(1))
} else {
line((-1, -1 + arrow-size), (-1, -1), (-1 + arrow-size, -1), ..styles.at(1))
}
}
})
}
#show: ams-article.with(
paper-size: "a4",
title: [Implementation of the Kauffman Polynomial in SageMath],
authors: (
(
name: "Antonio De Lucreziis",
organization: [Dipartimento di Matematica],
location: [Pisa, Italia],
email: "antonio.delucreziis@gmail.com",
url: "https://poisson.phc.dm.unipi.it/~delucreziis/",
),
),
abstract: [
In this project we implement the Kauffman polynomial in SageMath (Python).
],
)
#outline()
= Introduction
Actually we don't like Python so we will be using Rust and then write bindings for Python that can be used in SageMath.
== The Kauffman Polynomial
The Kauffman polynomial $kL$ is a two-variable polynomial invariant of unoriented knots and links in 3-dimensional space. It is defined using _Skein relations_, more precisely an implicit functional equation.
The defining axioms of the Kauffman polynomial are the following, given a link diagram $K$ we have $kL_K(a,z) in ZZ[a, a^(-1), z, z^(-1)]$ and:
1. If $K$ and $K'$ are two equivalent up to regular isotopy, then $kL_(K)(a,z) = kL_(K')(a,z)$.
2. We have the following identities:
- $kL(#skein.over) + kL(#skein.under) = z (kL(#skein.h) + kL(#skein.v))$
- $kL(#skein.unit) = 1$
- $kL(#skein.over-twist) = a kL(#skein.strand)$
- $kL(#skein.under-twist) = a^(-1) kL(#skein.strand)$
We will later be seeing that the Kauffman polynomial can be defined in a more explicit way, using a recursive definition that is the one we will be using to derive our algorithm.
== Computational Knot Theory
The first problem in computational knot theory is to find a good representation for knots and links. There are various common representations, such as:
- *Gauss codes*:
This is very simple to generate, we just need to label each crossing with a number and then write down the sequence of numbers in the order they appear by walking along the knot. This has the problem that it is not unique, for example it does not distinguish between the trefoil knot and its mirror image.
- *Signed Gauss codes*:
This is an improvement over the previous representation, on the second occurrence of a number we use a $+$ or $-$ sign to indicate the handedness of the crossing.
- *Planar diagram codes (PD codes)*, this is the one we will be using in this project:
The PD code for a link is generated by labelling each arc of the link with a number. Then we choose a starting point and walk along the link, when we pass at an over-crossing for the current strand we write down a $4$-uple of counter-clockwise numbers for the arc incident to the crossing.
There are also other codes like *Braid representations* and *DT (Dowker-Thistlethwaite) Codes* we will not be using in this project.
=== PD codes
Reference: #link("https://knotinfo.math.indiana.edu/descriptions/pd_notation.html")[PD notation article from KnotInfo]
The PD code for a link is generated by labelling each arc of the link with a number. Then we choose a starting point for each component and process each component in order.
For each component we walk along it from the starting point in the component direction.
#figure(image("assets/pd-code-crossing-ordering.svg"))
When we pass at a crossing that is an over-crossing for the current strand we write down a $4$-uple of counter-clockwise numbers for the arc incident to the crossing starting from the _entering under-crossing_ arc.
*Algorithm*:
```
Input: An oriented link diagram with starting points on each component
Output: List<(Nat, Nat, Nat, Nat)>
- Choose an ordering for the components and starting point for each component
- Label each arc with a number
- For each component:
- Walk along it from the starting point in its orientation
- At each crossing, when at an over-crossing for the current strand
- Write down a 4-uple of counter-clockwise numbers for the arc incident
to the crossing starting from the entering under-crossing arc
```
Let's see an example of how to construct the PD code for the following link diagram
#figure(image("assets/pd-code-0.svg"))
First let's choose a starting point for each component and label accordingly the arcs of the link. We will use the following convention:
#figure(
image("assets/pd-code-labelling.svg"),
caption: [Oriented link with starting points and edge labels.],
)
Now we can start processing each component of the link by walking along it in its orientation and writing down the over-crossings we encounter.
#{
align(
center,
grid(
columns: 2,
align: horizon,
column-gutter: 2em,
row-gutter: 1em,
image("assets/pd-code-crossing-1.svg"),
block[
#set align(left)
First link component \
First over-crossing \
$=>$ `(6,1,7,2)`
],
image("assets/pd-code-crossing-2.svg"),
block[
#set align(left)
First link component \
Second over-crossing \
$=>$ `(8,3,5,4)`
],
image("assets/pd-code-crossing-3.svg"),
block[
#set align(left)
Second link component \
First over-crossing \
$=>$ `(2,5,3,6)`
],
image("assets/pd-code-crossing-4.svg"),
block[
#set align(left)
Second link component \
Second over-crossing \
$=>$ `(4,7,1,8)`
],
),
)
}
Every directed crossing appears only once as an over-crossing so this algorithm terminates when all crossings have been visited. So the final PD code for this link is
#{
set align(center)
set text(size: 1.125em)
`[(6,1,7,2), (8,3,5,4), (2,5,3,6), (4,7,1,8)]`
}
=== Signed Gauss Codes
Gauss originally developed a notation called *Gauss codes* based on labelling each crossing of a knot with a number and keeping track of when we walk an over-crossing or an under-crossing using a sign. This produces a list of numbers where each number appears exactly twice with different signs. This has a few problems like the fact that this doesn't distinguish a knot vs its mirror.
This is solved by *Signed Gauss Codes*#footnote[#link("https://en.wikipedia.org/wiki/Gauss_notation")[Gauss Notation on Wikipedia]] where we also store the information about the handedness of a crossing.
More precisely this is constructed by the following: for each component we walk along it, when we pass at a crossing we write a tuple $(plus.minus i, plus.minus 1)$ where the first component is the index of the crossing with a sign indicating if this is an over-crossing or under-crossing, the second sign (that can be added in a second pass over the loop) is given by the handedness of the crossing using the following convention
$
epsilon(#skein-generic(direction: (+1, +1))) = +1
wide
epsilon(#skein-generic(direction: (+1, -1))) = -1
$
*Algorithm*:
```
Input: An oriented link diagram with starting points on each component
Output: List<List<(Int, Int)>>
- Label each crossing with a number in order
- For each component:
- Walk along it from the starting point in its orientation
- At each crossing, write a tuple with components
- $+i$ or $-i$ if this is an over-crossing or under-crossing
- $+1$ or $-1$ if this is a left-handed or right-handed
```
Converting one code to the other is not too much work as one just need to first do a labelling step to convert crossing labels and then convert the over/under-strand and left/right-handedness relations between the two notations.
For example the _Knot Theory code for the Kauffman polynomial_ converts the *pd notation* to *signed gauss notation* as this is better suited for doing manipulations directly on the crossings.
#let style-stroke-green = (stroke: (paint: green.darken(20%)))
- Crossing switch is just two sign swaps on each of the two occurrences of the over and under strand
- Splicing is just a matter of splitting and rejoining lists correctly, for example let's see what happens in the case of an _horizontal splice_. There are two cases based on the orientation of the strands:
- If the splice happens on a *self-crossing* (crossing with a part of the same curve) then we apply the following modification to the link, the rest remains the same except for the curve containing the spliced crossing that is changed as follows
$
lr(
[
... thin ell^+_1 thin ...
#skein-generic(styles: (style-stroke-green, (:)))
... thin ell^+_2 thin ...
#skein-generic(styles: ((:), style-stroke-green))
... thin ell^+_3 thin ...
],
size: #1.75em
)
& mapsto &&
lr(
[
... thin ell^+_1 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) }, style: style-stroke-green)
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
draw-arrow((0.1, 0.61), ..style-stroke-green)
},
)
... thin ell^+_3 thin ...
],
size: #1.75em
),
lr(
[
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) }, style: style-stroke-green)
draw-arrow((0.1, -0.61), ..style-stroke-green)
},
)
... thin ell^+_2 thin ...
],
size: #1.75em
) \
lr(
[
... thin ell^+_1 thin ...
#skein-generic(styles: (style-stroke-green, (:)), direction: (1, -1))
... thin ell^+_2 thin ...
#skein-generic(styles: ((:), style-stroke-green), direction: (1, -1))
... thin ell^+_3 thin ...
],
size: #1.75em
)
& mapsto &&
lr(
[
... thin ell^+_1 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) }, style: style-stroke-green)
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
draw-arrow((0.1, +0.61), ..style-stroke-green)
},
)
... thin ell^-_2 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) }, style: style-stroke-green)
draw-arrow((-0.1, -0.61), angle: 180deg, ..style-stroke-green)
},
)
... thin ell^+_3 thin ...
],
size: #1.75em
),
$
here by "$... thin ell_i^+ thin ...$" we mean a part of the crossing list and "$... thin ell_i^- thin ...$" is the same list reversed.
- Otherwise if the splice happens on a crossing *between strands of different curves*
$
lr(
[
...
[
... thin ell^+_1 thin ...
#skein-generic(styles: (style-stroke-green, (:)))
... thin ell^+_2 thin ...
]
...
[
... thin ell^+_3 thin ...
#skein-generic(styles: ((:), style-stroke-green))
... thin ell^+_4 thin ...
]
...
],
size: #1.75em
) \
#rotate(90deg)[$mapsto$] \
lr(
[
...
[
... thin ell^+_1 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) }, style: style-stroke-green)
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
draw-arrow((0.1, +0.61), ..style-stroke-green)
},
)
... thin ell^+_4 thin ... thin ell^+_3 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) }, style: style-stroke-green)
draw-arrow((0.1, -0.61), ..style-stroke-green)
},
)
... thin ell^+_2 thin ...
]
...
],
size: #1.75em
)
$
or in case of the other orientation
$
lr(
[
...
[
... thin ell^+_1 thin ...
#skein-generic(styles: (style-stroke-green, (:)), direction: (1, -1))
... thin ell^+_2 thin ...
]
...
[
... thin ell^+_3 thin ...
#skein-generic(styles: ((:), style-stroke-green), direction: (1, -1))
... thin ell^+_4 thin ...
]
...
],
size: #1.75em
) \
#rotate(90deg)[$mapsto$] \
lr(
[
...
[
... thin ell^+_1 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) }, style: style-stroke-green)
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
draw-arrow((0.1, +0.61), ..style-stroke-green)
},
)
... thin ell^-_3 thin ... thin ell^-_4 thin ...
#skein-canvas(
{
import cetz.draw: *
draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) }, style: style-stroke-green)
draw-arrow((0.1, -0.61), ..style-stroke-green)
},
)
... thin ell^+_2 thin ...
]
...
],
size: #1.75em
)
$
// $
// lr(
// [
// ... thin ell^+_1 thin ...
// #skein-generic(styles: (style-stroke-green, (:)), direction: (1, -1))
// ... thin ell^+_2 thin ...
// #skein-generic(styles: ((:), style-stroke-green), direction: (1, -1))
// ... thin ell^+_3 thin ...
// ],
// size: #1.75em
// )
// & mapsto &
// lr(
// [
// ... thin ell^+_1 thin ...
// #skein-canvas(
// {
// import cetz.draw: *
// draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) }, style: style-stroke-green)
// draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) })
// draw-arrow((0.1, +0.61), ..style-stroke-green)
// },
// )
// ... thin ell^-_2 thin ...
// #skein-canvas(
// {
// import cetz.draw: *
// draw-strand({ hobby((-1, 1), (0, +0.61), (1, 1), omega: 1) })
// draw-strand({ hobby((-1, -1), (0, -0.61), (1, -1), omega: 1) }, style: style-stroke-green)
// draw-arrow((-0.1, -0.61), angle: 180deg, ..style-stroke-green)
// },
// )
// ... thin ell^+_3 thin ...
// ],
// size: #1.75em
// ),
// $
=== Link reconstruction from code
We briefly mention that reconstructing a link from a PD code is not trivial and there are various approaches that can be used for this task.
==== Linear Integer Programming
For example the #link("https://doc.sagemath.org/html/en/reference/knots/sage/knots/link.html")[KnotTheory package in Sage] has a #link("https://doc.sagemath.org/html/en/reference/knots/sage/knots/link.html#sage.knots.link.Link.plot")[`Link.plot()`] method that thats a link that can be constructed using a PD code and then plots it as follows
#[
#set align(center)
```python
# The "monster" unknot
L = Link([[ 3, 1, 2, 4], [ 8, 9, 1, 7], [ 5, 6, 7, 3], [ 4, 18, 6, 5],
[17, 19, 8, 18], [ 9, 10, 11, 14], [10, 12, 13, 11], [12, 19, 15, 13],
[20, 16, 14, 15], [16, 20, 17, 2]])
L.plot()
```
#box(
fill: luma(93%),
radius: 0.5em,
inset: 1em,
image("assets/sage-monster-unknot.svg", width: 40%),
)
]
Sage internally uses a #link("https://github.com/sagemath/sage/blob/e0cf1e41d419feb9ddbc0e3c54823928a01587dc/src/sage/knots/link.py#L3639")[_mixed integer linear programming_ (MILP)] solver to generate a knot diagram from a PD code. Another library called #link("https://github.com/3-manifolds/Spherogram/blob/725086a1d8c5d1381ff6a70315047efd8e0dac3f/spherogram_src/links/orthogonal.py")[Spherogram] instead uses _network flows_. The problem here is to find an orthogonal presentation for the link with the _minimum number of left and right bends_
#footnote[#link("https://www.sciencedirect.com/science/article/pii/S0096300305002778")[A better heuristic for area-compaction of orthogonal representations]].
==== Planar Graph Embeddings
Another approach used by #link("https://knotfol.io/")[KnotFolio] is based on #link("https://en.wikipedia.org/wiki/Tutte_embedding")[Tutte embeddings]. A *Tutte embedding or barycentric embedding* of a simple, 3-vertex-connected, planar graph is a crossing-free straight-line embedding with the properties that the outer face is a convex polygon and that each interior vertex is at the average (or barycenter) of its neighbors' positions.
This condition that every point is the average of its neighbors can be easily expressed as a system of linear equations where some points on a chosen outer face have been fixed. When the graph is planar and 3-vertex-connected the linear system is non degenerate and has a unique solution.
== Algorithm for computing the Kauffman Polynomial
Let's now recap the main formal algorithm for computing the Kauffman polynomial.
#definition[
Let $K$ be an un-oriented link. We denote by $hat(K)(p)$ the *standard unknot* for $K$ where $p$ is a _directed starting point_. This is built by considering the planar shadow $U$ of $K$ and walking along $U$ starting from $p$ and making each crossing an over-crossing when passing on it the first time.
TODO: Disegnini
]
#definition[
Let's give a name to the following knot modifications for $K$ near a specific crossing $i$
#align(
center,
grid(
columns: 4,
column-gutter: 2em,
row-gutter: 1em,
skein.over, skein.under, skein.h, skein.v,
$K$, $S_i K$, $E_i K$, $e_i K$,
[ #set text(size: 9pt); _original_],
[ #set text(size: 9pt); _switch_],
[ #set text(size: 9pt); _splice_],
[ #set text(size: 9pt); _splice_],
),
)
]
Let now $K$ be an oriented link with $n$ components so $K = K_1 union ... union K_n$.
#definition[
Let $K$ abd $lambda = (lambda_n, ..., lambda_0)$ a sequence of indices of crossing of $K$ and let $i$ be an index of one of the crossings, let's define the following actions
- $A_i^lambda colon.eq E_i S_(lambda_i) ... S_(lambda_0)$
- $B_i^lambda colon.eq e_i S_(lambda_i) ... S_(lambda_0)$
- Then let $lambda$ be a sequence of indices that bring $K$ to $hat(K)$ so that $hat(K)(lambda) colon.eq S_(lambda_n) dots.c space S_(lambda_0) K$ and define
$
sum_K (lambda) =
sum_(i=0)^n (-1)^i (kL(A_i^lambda K) + kL(B_i^lambda K)) \
Omega_K (lambda) =
(-1)^(|lambda| + 1) L_(hat(K)(lambda)) + z sum_K (lambda)
$
]
*Algorithm.* Here follows the algorithm that computes $kL_(K)(a,z)$.
1. If $K = hat(K)$ is a _standard unknot_ then $kL_K (a, z) colon.eq a^w(K)$
2. If $K_1$ _overlies_ $K_2$, let $d colon.eq (a + a^(-1)) slash z - 1$ and then
$
kL(K_1 union K_2) colon.eq d kL(K_1) kL(K_2)
$
3. If $K = K_1 union dots.c union K_n$
- If a $K_i$ _overlies_ another another component than apply (ii).
- If no $K_i$ _overlies_ all others let $p_1, ..., p_n$ be _directed starting points_ on $K_1, ..., K_n$ and let $overline(p)_i$ be the same _directed starting point_ with the opposite direction of $p_i$ on $K_i$. Let $lambda(p_i)$ the sequence of under-crossings of $K_i$ with $K - K_i$ so that $hat(K)(lambda(p_i)) = K_i union.sq (K - K_i)$ so that $K_i$ _overlies_ the rest of the components. At this point we can define $kL_K$ as
#[
#set text(size: 11pt)
$
kL_K (a, z) colon.eq
1 / (2n) [
sum_(q = p_i, overline(p)_i) sum_(i=1)^(|lambda(q)|) (-1)^(|lambda(q)|+1) d kL_(K_i) kL_(K - K_i) + z sum_K (lambda(q))
]
$
]
- If $K$ is a single component then let $p$ be a directed starting point on $K$ and $overline(p)$ the one with opposite direction. Let $lambda(p)$ the switching sequence that brings it to the standard unknot $hat(K)$ and define
#[
#set text(size: 11pt)
$
kL_K (a, z) colon.eq
1 / 2 [
sum_(q = p, overline(p)) sum_(i=1)^(|lambda(q)|) (-1)^(|lambda(q)|+1) kL(hat(K)(lambda(q))) + z sum_K (lambda(q))
]
$
]
#pagebreak()
= Appendix
All combinations of `skein-generic` typst function
#[
#set align(center + horizon)
#for kind in ("over", "under") {
[*Kind:* #raw(repr(kind))]
grid(
columns: 4,
gutter: 1.5em,
..(
((+1, +1), (+1, -1), (-1, +1), (-1, -1)).map(direction => {
((true, true), (false, true), (true, false), (false, false)).map(arrows => {
skein-generic(
kind: kind,
direction: direction,
arrows: arrows,
)
[#direction \ #arrows]
})
})
).flatten()
)
}
]