final touches

main
parent 4917f609c3
commit 658c45a6b7

Binary file not shown.

@ -147,17 +147,16 @@
),
),
abstract: [
In this project we write implement from scratch the Kauffman polynomial in Python. We start with a brief detour 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, finding an error for the knot $10_125$.
In this project we write implement from scratch the Kauffman polynomial in Python. We start with a brief detour in computational knot theory and describe various representations of knots and links and find a good one to use for the algorithm. We then describe two approaches 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, finding an error for the knot $10_125$.
],
)
#pagebreak()
= Introduction
== 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 Kauffman polynomial $kL$ is a two-variable polynomial invariant of regular isotopy for 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:
@ -173,33 +172,36 @@ The defining axioms of the Kauffman polynomial are the following, given a link d
- $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.
We will later be seeing that the Kauffman polynomial can be defined in a more explicit way, using a closed form recursive definition that we will be using to derive the first approach for our algorithm.
#pagebreak()
= 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:
The first problem in computational knot theory is to find a good representation for knots and links. There are various 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*:
- *Signed Gauss codes (S.G. 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:
- *Planar diagram codes (P.D. 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.
- *D.T. (Dowker-Thistlethwaite) Codes* Is another representation present in KnotInfo based on crossing labels. We label each crossing twice in order, each crossing will get an even and odd label and we take only the even labels in order #footnote[Check this is correct].
There are also other codes like *Braid representations* we will not be using in this project.
== PD codes
For our program we are going to use the P.D. code as input for our algorithm so let's explain how it is derived from a knot or link.
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.
For each component we walk along it from the starting point in the component orientation. 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"))
@ -298,7 +300,6 @@ $
epsilon(#skein-generic(direction: (+1, -1))) = -1
$
#pagebreak()
*Algorithm*: The input is an oriented link diagram with starting points on each component and the output is a list of components where each component is a list of pairs of numbers
@ -315,7 +316,7 @@ $
- $+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.
Converting one code to the other is fairly easy 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
@ -325,10 +326,12 @@ We will now see how SG codes are better suited for the manipulations (switching
- *Switch*
- *SG codes* -- This is just two _sign swaps_ on each of the two occurrences of the over and under strand.
- *SG codes* -- To do this we need to swap the two over/under signs for the crossings for each of the two occurrences of the over and under strand. Then we also need to flip the handedness of the crossing as the switch causes an handedness change.
- *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.
#pagebreak()
- *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.
@ -337,15 +340,15 @@ We will now see how SG codes are better suited for the manipulations (switching
One can add a meaning to pairs $(i, j)$ symbols to the original sequence of 4-tuples 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 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 do splices very often.
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
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, this would be ok for knots/links for up to 128 crossings), 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 + 1.5) + k times ceil(log_2(n))$ bits of information. Each crossing appears twice and we store its id and over/under and handedness (each pair has the same handedness so we can store it only once per crossing) information, we also need to store the structure of the list with $k times ceil(log_2(n))$ more bits.
- SG codes use $approx 2n times (N + 1.5) + k times ceil(log_2(n))$ bits of information. Each crossing appears twice and we store its id, over/under and handedness (each pair has the same handedness so we can store it only once per crossing) 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.
@ -611,7 +614,7 @@ For example the #link("https://doc.sagemath.org/html/en/reference/knots/sage/kno
)
]
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_
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 solved here is finding 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
@ -620,11 +623,9 @@ Another approach used by #link("https://knotfol.io/")[KnotFolio] is based on #li
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.
#pagebreak()
= Computing the Polynomial
Let's now recap the main formal algorithm for computing the Kauffman polynomial.
We tried two approaches for our algorithm, the first based on the closed form algorithm present in Kauffman's paper and another "naive one" based directly on applying the skein relation. Let's first recap the main formal closed form 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.
@ -656,7 +657,7 @@ Let's now recap the main formal algorithm for computing the Kauffman polynomial.
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
Let $K$ and $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 operations
- $A_i^lambda K colon.eq E_i S_(lambda_i) dots.c space S_(lambda_0) K$
@ -725,6 +726,8 @@ After a first implementation of the algorithm we noticed that it is not very eff
kL(K_1 union.sq dotss union.sq K_n) = d^(n-1) kL(K_1) dotss kL(K_n)
$
This is borrowed from the closed form algorithm and let's us solve the case of disjoint components.
3. If $K$ is a linked component then we find $hat(K)$, the standard unlink for $K$, and pick the first available crossing $c$ to switch, then we have some cases based on the crossing over/under type and handedness that can be reduced to the following two cases
$
@ -738,17 +741,14 @@ After a first implementation of the algorithm we noticed that it is not very eff
actually due to the symmetry of the Kauffman polynomial we can just use the same rule for both cases, so we can write
$
kL(K) = z (kL(E_c K) + kL(e_c K)) - kL(S_c K)
kL_K(a,z) = z (kL_(E_c K)(a,z) + kL_(e_c K)(a,z)) - kL_(S_c K)(a,z)
$
where $E_c K$ and $e_c K$ have one less crossing and $S_c K$ has the same crossings as $K$ but is one step closer to the standard unlink.
#pagebreak()
= Python Implementation
The approach has been a mix of bottom-up and top-down. First we defined a couple of classed `SGCode` and `PDCode` to work with these codes and easily convert between each other.
The approach has been a mix of bottom-up and top-down, we first wrote the code for the main algorithm and then wrote the missing implementation for `SGCode` writing many tests along the way. First we defined a couple of classed `SGCode` and `PDCode` to work with these codes and easily convert between each other.
== SG Codes
@ -762,7 +762,6 @@ We are now going to walk thorough the class that lets use work nicely with *SG c
}
```python
@dataclass(frozen=True)
class SGCodeCrossing:
id: int
over_under: typing.Literal[+1, -1]
@ -770,16 +769,12 @@ class SGCodeCrossing:
def is_over(self) -> bool:
def is_under(self) -> bool:
def is_left(self) -> bool:
def is_right(self) -> bool:
def opposite(self) -> SGCodeCrossing:
def switch(self) -> SGCodeCrossing:
def flip_handedness(self) -> SGCodeCrossing:
```
```python
@dataclass(frozen=True)
class SGCode:
components: list[list[SignedGaussCodeCrossing]]
@ -880,13 +875,13 @@ Now we need to be careful when applying these switches as they do not preserve t
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.
First we wrote two methods, one called `SGCodeCrossing.switch()` that creates a new switched crossing from another crossing and then another one for `SGCode` that does the switches for the _two_ occurrences.
#align(
center,
grid(
columns: 2,
gutter: 1em,
gutter: 1.5em,
align: center + horizon,
[
```py
@ -928,7 +923,7 @@ def apply_switching_sequence(self, seq: list[int]) -> SGCode:
])
```
In the actual Kauffman polynomial computation we just compute the first available crossing id in the sequence and apply just that one.
In the actual Kauffman polynomial computation we just compute the first available switch in the switching sequence and apply just that one.
```py
def first_switch_to_std_unknot(self) -> (int | bool):
@ -952,7 +947,7 @@ Then we have the following cases
- Crossing sign: left-handed or right-handed
- Crossing type: self-crossing or crossing between two different strands
- Crossing type: self-crossing or crossing between two different components
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_.
@ -1012,7 +1007,9 @@ Handling the reversed part is actually more involved than just reversing the lis
) <splice-signs-problem>
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. Let's see for example the code for the negative crossing horizontal splice case
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. Let's see for example the code for the negative crossing horizontal splice case#footnote[The python operator `^` is the symmetry difference of sets]
#pagebreak()
```py
over_crossing_ids = set(c.id for c in l2 if c.is_over())
@ -1052,7 +1049,7 @@ So the final code is just a conversion of all this cases to list slicing and re-
== Code for computing the Kauffman polynomial
The final code for computing the Kauffman polynomial is the following, the main idea of the algorithm was already described previously. The code is implemented in a functional style and uses the `SGCode` class defined above to represent the links. At the top level we define globals variables using the `sympy` library for working with polynomials.
The final code for computing the Kauffman polynomial is the following, the main idea of the algorithm was already described previously. The code uses the `SGCode` class defined above to represent links. At the top level we define globals variables for `a`, `z` using the `sympy` library for working with polynomials.
```py
a, z = symbols("a z")
@ -1269,7 +1266,7 @@ $
Then we noticed that they are related by the substitution $(a mapsto 1 slash a, z mapsto z)$. The Kauffman polynomial has this property that inverts the $a$ when computing the mirror image of a knot, in this sense it is able to distinguish chiral variants of knots.
So we checked using the implementation of the Kauffman polynomial using one in the `KnotTheory` Mathematica package. We used the PD code present in the KnotInfo database and found that the result of Mathematica and of our algorithm *match*. This makes us believe that there is a problem in KnotInfo and that the column for the PD code or for the Kauffman polynomial are not correctly synchronized.
So, we checked using the implementation of the Kauffman polynomial using one in the `KnotTheory` Mathematica package: using the PD code from the KnotInfo database, both Mathematica and our algorithm give the *same result*. This makes us believe that there is a mismatch in KnotInfo between the PD code and Kauffman polynomial columns, meaning they are not correctly synchronized.
=== Performance Analysis

@ -144,22 +144,25 @@
show raw: set text(font: "JetBrains Mono")
show raw: it => box(
fill: luma(92%),
radius: 3pt,
outset: (x: 2pt, y: 3pt),
it,
)
show raw.where(block: false): it => {
set text(size: 7.25pt, fill: luma(7%))
box(
outset: (x: 2pt, y: 3pt),
fill: luma(92%),
radius: 3pt,
it,
)
}
show raw.where(block: true): it => box(
show raw.where(block: true): it => block(
outset: (x: 2pt, y: 3pt),
fill: luma(92%),
radius: 4pt,
inset: 4pt,
it,
)
show raw.where(block: false): set text(size: 7.25pt, fill: luma(7%))
// Configure citation and bibliography styles.
set std.bibliography(style: "springer-mathphys", title: [References])
@ -239,6 +242,8 @@
// set text(script-size)
show: pad.with(x: 35pt)
smallcaps[Abstract. ]
set text(size: 9pt)
abstract
}

Loading…
Cancel
Save