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.
1037 lines
38 KiB
Plaintext
1037 lines
38 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 dotss = $space dots.c space$
|
|
|
|
#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 write implement from scratch the Kauffman polynomial in Python. First we introduce problems in computational knot theory and describe various representations of knots and links and find a good representation to use for the algorithm. We then describe in-depth the algorithm for computing the Kauffman polynomial and how to implement it in Python. Finally we try the algorithm on various knots and links and compare the results with the ones from the KnotInfo Database.
|
|
],
|
|
)
|
|
|
|
= 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 in the literature, 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
|
|
|
|
The main source for the section is the article on #link("https://knotinfo.math.indiana.edu/descriptions/pd_notation.html")[PD notation 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. 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.
|
|
|
|
#figure(image("assets/pd-code-crossing-ordering.svg"))
|
|
|
|
*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)]`
|
|
}
|
|
|
|
=== SG 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.
|
|
|
|
=== Comparison of PD and SG codes
|
|
|
|
We will now see how SG codes are better suited for the manipulations (switching and splicing) we need to do on a link. Indeed the #link("https://katlas.org/wiki/Setup")[KnotTheory Mathematica package] when computing the Kauffman polynomial converts the input link from *PD code* into *SG code* as this is better suited for doing manipulations directly on the crossings.
|
|
|
|
#let style-stroke-green = (stroke: (paint: green.darken(20%)))
|
|
|
|
- *Switch*
|
|
|
|
- *SG codes* -- This is just two _sign swaps_ on each of the two occurrences of the over and under strand.
|
|
|
|
- *PD codes* -- This is more involved and requires _cycling_ the crossing from $(i, j, k, l)$ to one of $(l, i, j, k)$ or $(j, k, l, i)$ based on the crossing sign and _relabelling_ the whole affected components as PD codes heavily rely on the sequentiality of the indices to tell the direction and end of a component.
|
|
|
|
- *Splicing*
|
|
|
|
- *SG codes* -- This is just a matter of splitting, reversing and rejoining lists correctly, this is a bit involved and will be covered more in depth later.
|
|
|
|
- *PD codes* -- This can be approached in various ways more or less performant.
|
|
|
|
One can add a meaning to pairs $(i, j)$ symbols to the original sequence of 4-uples called "path" elements (the KnotTheory package has this extension of the PD notation with the `P[i, j]` element) that tell we joined the arc $i$ with the arc $j$. This gives an heterogeneous list of elements that is more complex to handle efficiently in classical programming languages different from Mathematica.
|
|
|
|
Another approach to keep list homogeneous is to manually splice the crossing and do a relabelling of all the arcs at each step. This causes in the worst case a continuos relabelling of all the crossings in the link at every splice and is not very efficient as our algorithm needs to be able to do splicing as fast as possible.
|
|
|
|
Another downside of PD codes is the ordering of the crossings in-memory, walking along a component might require various jumps along the list. SG codes on the other hand have already each component in the correct order and can be walked linearly without jumps.
|
|
|
|
Finally SG codes are also more "space efficient". Let $N$ be a number of bits to encode a natural number of maximum fixed size (e.g. $N = 8$ for using `uint8` to encode numbers), for a link with $n$ crossings and $k$ components
|
|
|
|
- PD codes use $approx 4n times N$ bits of information, four numbers for each item.
|
|
|
|
- SG codes use $approx 2n times (N + 2) + k times ceil(log_2(n))$ bits of information. Each crossing appears twice and we store its id and over/under and handedness information, we also need to store the structure of the list with $k times ceil(log_2(n))$ more bits.
|
|
|
|
So PD codes are simpler and compact to store (and generate from a diagram) but SG codes are more space efficient and easy to manipulate.
|
|
|
|
// - 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
|
|
// ),
|
|
// $
|
|
|
|
#pagebreak()
|
|
|
|
=== Link reconstruction from code
|
|
|
|
We briefly mention that reconstructing a link from a PD or SG code is not trivial and there are various approaches used by various softwares 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.
|
|
|
|
#figure(image("assets/standard-unknot-construction.jpg"))
|
|
]
|
|
|
|
#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); _h-splice_],
|
|
[ #set text(size: 9pt); _v-splice_],
|
|
),
|
|
)
|
|
]
|
|
|
|
Let now $K$ be an oriented link with $n$ components so $K = K_1 union dotss 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 K colon.eq E_i S_(lambda_i) dots.c space S_(lambda_0) K$
|
|
|
|
- $B_i^lambda K colon.eq e_i S_(lambda_i) dots.c space S_(lambda_0) K$
|
|
|
|
- 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_(i=1)^n sum_(q=p_i, overline(p)_i) ((-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)) ((-1)^(|lambda(q)|+1) kL(hat(K)(lambda(q))) + z sum_K (lambda(q)))
|
|
]
|
|
$
|
|
]
|
|
|
|
#pagebreak()
|
|
|
|
== Python Implementation
|
|
|
|
The approach has been a mix of bottom-up and top-down. First we defined a couple of classed `SignedGaussCode` and `PDCode` to work with these codes and easily convert between each other.
|
|
|
|
=== SG Codes
|
|
|
|
We are now going to walk thorough the class that lets use work nicely with *SG codes*. The the classes we are going to use are all _frozen data-classes_ to ensure immutability and enforce a more functional programming style.
|
|
|
|
#show raw.where(block: true): body => {
|
|
set text(size: 7pt)
|
|
set align(center)
|
|
|
|
body
|
|
}
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class SignedGaussCodeCrossing:
|
|
id: int
|
|
over_under: typing.Literal[+1, -1]
|
|
handedness: typing.Literal[+1, -1]
|
|
|
|
def is_over(self) -> bool: ...
|
|
def is_under(self) -> bool: ...
|
|
def is_left(self) -> bool: ...
|
|
def is_right(self) -> bool: ...
|
|
def opposite(self) -> SignedGaussCodeCrossing: ...
|
|
def switch(self) -> SignedGaussCodeCrossing: ...
|
|
def flip_handedness(self) -> SignedGaussCodeCrossing: ...
|
|
```
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class SignedGaussCode:
|
|
components: list[list[SignedGaussCodeCrossing]]
|
|
|
|
def writhe(self): ...
|
|
def reverse(self): ...
|
|
def mirror(self): ...
|
|
def to_std_unknot(self) -> SignedGaussCode: ...
|
|
def std_unknot_switching_sequence(self) -> list[int]: ...
|
|
def apply_switching_sequence(self, seq: list[int]) -> SignedGaussCode: ...
|
|
def splice_h(self, id: int): ...
|
|
def splice_v(self, id: int): ...
|
|
def switch_crossing(self, id: int): ...
|
|
```
|
|
|
|
==== Writhe
|
|
|
|
One of the first important things we need is to compute the *writhe* $w(K)$ of a link, this can easily be done with signed gauss codes as its a list of tuples where the second entry is the crossing sign. Let $L$ be an oriented link with components $C_1, ..., C_k$ each with crossings $c_(i, j)$ with $i = 1, ..., k$ and $j = 1, ..., |C_i|$.
|
|
|
|
Let's notice that here each crossing appears twice, once as over-crossing and once as an under-crossing this is the reason for the $1 slash 2$ in the following formula. By $epsilon(c)$ we refer to the sign (or handedness) of the crossing at $c$.
|
|
|
|
#[
|
|
#set align(center)
|
|
#grid(
|
|
columns: 3,
|
|
gutter: 1.5em,
|
|
align: horizon,
|
|
[
|
|
$
|
|
w(L) = 1 / 2 sum_(c "crossing") epsilon(c)
|
|
$
|
|
],
|
|
[$ arrow.squiggly $],
|
|
[
|
|
```python
|
|
def writhe(self):
|
|
return sum(
|
|
c.handedness # => +1 or -1
|
|
for component in self.components
|
|
for c in component
|
|
) // 2
|
|
```
|
|
],
|
|
)
|
|
]
|
|
|
|
==== Standard Unknot
|
|
|
|
The next building block for computing the Kauffman polynomial is detecting and computing the *standard unknot or unlink*. Formally this is done by taking the _planar shadow_ and a directed starting point on each component of the link. Then we can walk along the shadow of each component in order and make each crossing an over-crossing when passing on it on the first time.
|
|
|
|
#figure(
|
|
image("assets/standard-unlink-construction.png"),
|
|
caption: [Standard unknot construction for a link, in blue are highlighted all the crossings in the resulting switching sequence],
|
|
)
|
|
|
|
On the other hand our algorithm directly works with switching sequences $lambda$ that bring a link $L$ to its standard unlink $hat(L)$. We wrote methods to directly compute and apply these switching sequences.
|
|
|
|
The `std_unknot_switching_sequence` method just walks along each component in its orientation marking what switches have to be made to bring that link to its standard unknot.
|
|
|
|
```python
|
|
def std_unknot_switching_sequence(self) -> list[int]:
|
|
visited_crossings: set[int] = set() # list of ids
|
|
switched_crossings: list[int] = [] # resulting list of ids to switch
|
|
|
|
for component in self.components:
|
|
for crossing in component:
|
|
if crossing.id not in visited_crossings:
|
|
if crossing.is_under():
|
|
switched_crossings.append(crossing.id)
|
|
visited_crossings.add(crossing.id)
|
|
|
|
return switched_crossings
|
|
```
|
|
|
|
Now we need to be careful when applying these switches as they do not preserve the writhe as we can see in the following figure
|
|
|
|
#figure(
|
|
image("assets/standard-unlink-signs-switches.png", width: 80%),
|
|
caption: [Switched crossing signs after brining the previous link to its standard unlink],
|
|
)
|
|
|
|
First we wrote two functions one called `switch` that creates a new switched crossing from another crossing and then another one for `SGCode` that does the _two_ switches.
|
|
|
|
#align(
|
|
center,
|
|
grid(
|
|
columns: 2,
|
|
gutter: 1em,
|
|
align: center + horizon,
|
|
[
|
|
```py
|
|
def switch(self: SGCodeCrossing):
|
|
return SGCodeCrossing(
|
|
self.id,
|
|
-self.over_under,
|
|
-self.handedness
|
|
)
|
|
```
|
|
],
|
|
[
|
|
```py
|
|
def switch_crossing(self: SGCode, id: int):
|
|
return SGCode([
|
|
[
|
|
crossing.switch()
|
|
if crossing.id == id else crossing
|
|
for crossing in component
|
|
]
|
|
for component in self.components
|
|
])
|
|
```
|
|
],
|
|
),
|
|
)
|
|
|
|
With this infrastructure in place now applying a switching sequence is just a matter of applying all switches in a given sequence, this can be done in a single pass with the following function.
|
|
|
|
```py
|
|
def apply_switching_sequence(self, seq: list[int]) -> SGCode:
|
|
return SGCode([
|
|
[
|
|
crossing.switch()
|
|
if crossing.id in switching_sequence else crossing
|
|
for crossing in component
|
|
]
|
|
for component in self.components
|
|
])
|
|
```
|
|
|
|
==== Crossing Splices
|
|
|
|
The splicing code is more involved due to the number of cases to analyze, let's first see formally what we need to do.
|
|
|
|
We have all the following cases, first we can assume the _entering over strand_ is in the top left corner of a diagram (this can be done by applying locally a small local isotopy).
|
|
Then we have the following cases
|
|
|
|
- Splice type: horizontal or vertical
|
|
|
|
- Crossing sign: left-handed or right-handed
|
|
|
|
- Crossing type: self-crossing or crossing between two different strands
|
|
|
|
So we have a total of $2 times 2 times 2 = 8$ cases to analyze. The following diagram shows all the possible cases for horizontal splicing for _signed gauss codes_.
|
|
|
|
#figure(
|
|
image("assets/operations-splice-h-cases.png", width: 125%),
|
|
caption: [Cases for horizontal splicing],
|
|
)
|
|
|
|
Let's explain this diagram a bit, each label is a part of the list for the component, the "$-$" sign tells is that part is walked in opposite order and must reversed in the final list.
|
|
|
|
The first two cases in the top left are the ones where the splice happens on a self-crossing that is the crossing is with two parts of the same strand. We can assume the starting point is before the over-strand, the result is the same as we can just rotate the list to get in this configuration. So if this component has $n$ crossings and $i$ and $j$ are the indices of the over-strand crossing and the under-strand crossing respectively the code will be
|
|
|
|
$
|
|
[ dotss, [ space C_1, dotss, C_i, dotss, C_j, dotss, C_(2n) space ], dotss ]
|
|
$
|
|
|
|
where $C_k = (c_k, s_k)$ as a pair of crossing *id* and *sign*. To apply the splice we remove the crossings $(c_i, s_i)$ and $(c_j, s_j)$ from that component list and rejoin following the orientation of the strand starting from the starting point.
|
|
|
|
- In the *positive crossing* case the horizontal splice splits the component in two, the first one composed of the first and third part one after the other and another one composed only of the second part. More precisely we have the following two new components
|
|
|
|
$
|
|
[
|
|
space
|
|
C_1, dotss, C_(i-1),
|
|
space
|
|
C_(j+1), dotss, C_(2n)
|
|
space
|
|
] \
|
|
[
|
|
space
|
|
C_(i+1), dotss, C_(j-1)
|
|
space
|
|
]
|
|
$
|
|
|
|
- In the *negative crossing* case we first walk on the first part of the list, then we walk the second part in reverse and finally the third part. So the new component will be
|
|
|
|
$
|
|
lr(
|
|
[
|
|
space
|
|
C_1, dotss, C_(i-1),
|
|
space
|
|
underbrace(#$C_(j-1), C_(j-2), dotss, C_(i+2), C_(i+1)$, "reversed part"),
|
|
space
|
|
C_(j+1), dotss, C_(2n)
|
|
space
|
|
], size: #1.25em
|
|
)
|
|
$
|
|
|
|
Handling the reversed part is actually more involved than just reversing the list of crossings. We also need to correct all the signs of the crossings to account for the new orientation. To do this we need to flip the crossing sign of all crossing ids that occur in this part we are reversing. Notice this can end up even alter signs of crossings in other components so we need to be careful. The code that handles with reversing is the following
|
|
|
|
TODO: Code snippet
|
|
|
|
The code for the *vertical splices* is omitted as the cases are the same as for the horizontal splices with a minor change. All the cases for vertical splices are shown below in the following diagram and we can see that the output lists are the same as in the previous splice case just switched based on the crossing sign.
|
|
|
|
#figure(
|
|
image("assets/operations-splice-v-cases.png"),
|
|
caption: [Cases for vertical splicing],
|
|
)
|
|
|
|
So the final code is just a conversion of all this cases to list slicing and re-joining with the appropriate crossings removed and signs updated correctly.
|
|
|
|
#pagebreak()
|
|
|
|
= Appendix
|
|
|
|
== Enhancing SGCode rejoining code
|
|
|
|
The initial code to do horizontal and vertical splices looked something like the following
|
|
|
|
```py
|
|
...
|
|
if handedness == HANDED_LEFT:
|
|
return SGCode([
|
|
*(component[:]
|
|
for i, component in enumerate(self.components)
|
|
if i != component_index),
|
|
[
|
|
*l1,
|
|
*l3,
|
|
],
|
|
l2,
|
|
])
|
|
else:
|
|
return SGCode([
|
|
*(component[:]
|
|
for i, component in enumerate(self.components)
|
|
if i != component_index),
|
|
[
|
|
*l1,
|
|
*l2[::-1],
|
|
*l3,
|
|
],
|
|
])
|
|
```
|
|
|
|
This is actually wrong as this is not correcting the signs of all crossing from the reversed strand. The corrected code for the second case is not very readable
|
|
|
|
```py
|
|
over_crossing_ids = set(c.id for c in l2 if c.is_over())
|
|
under_crossing_ids = set(c.id for c in l2 if c.is_under())
|
|
non_self_crossing_ids = over_crossing_ids.symmetric_difference(under_crossing_ids)
|
|
|
|
update_signs = lambda strand: [
|
|
c.flip_handedness() if c.id in non_self_crossing_ids else c
|
|
for c in strand
|
|
]
|
|
|
|
SGCode([
|
|
*(
|
|
update_signs(component)
|
|
for i, component in enumerate(self.components)
|
|
if i != component_index
|
|
),
|
|
[
|
|
*update_signs(l1),
|
|
*update_signs(reversed(l2)),
|
|
*update_signs(l3),
|
|
],
|
|
])
|
|
```
|
|
|
|
== Nice Typst Stuff
|
|
|
|
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()
|
|
)
|
|
}
|
|
]
|