diff --git a/assets/splice-h-cases.png b/assets/splice-h-cases.png new file mode 100644 index 0000000..888ebe2 Binary files /dev/null and b/assets/splice-h-cases.png differ diff --git a/assets/splice-v-cases.png b/assets/splice-v-cases.png new file mode 100644 index 0000000..25b1237 Binary files /dev/null and b/assets/splice-v-cases.png differ diff --git a/assets/standard-unknot-construction.jpg b/assets/standard-unknot-construction.jpg new file mode 100644 index 0000000..fe9b4a6 Binary files /dev/null and b/assets/standard-unknot-construction.jpg differ diff --git a/main.pdf b/main.pdf index 3a672e6..ea7750c 100644 Binary files a/main.pdf and b/main.pdf differ diff --git a/main.typ b/main.typ index 478f462..81904d1 100644 --- a/main.typ +++ b/main.typ @@ -7,6 +7,7 @@ #let kL = $L$ +#let dotss = $space dots.c space$ #let draw-strand(polyline, style: (:)) = { import cetz.draw: * @@ -317,213 +318,239 @@ Output: List> - 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 + - +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. +=== 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%))) -- Crossing switch is just two sign swaps on each of the two occurrences of the over and under strand +- *Switch* -- 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: + - *SG codes* -- This is just two _sign swaps_ on each of the two occurrences of the over and under strand. - - 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 + - *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. - $ - 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 - ), - $ +- *Splicing* - here by "$... thin ell_i^+ thin ...$" we mean a part of the crossing list and "$... thin ell_i^- thin ...$" is the same list reversed. + - *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. - - Otherwise if the splice happens on a crossing *between strands of different curves* + - *PD codes* -- This can be approached in various ways more or less performant. - $ - 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 - ) - $ + 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. - or in case of the other orientation + 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. - $ - 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 - ) - $ +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 more simple 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( @@ -565,9 +592,11 @@ For example the _Knot Theory code for the Kauffman polynomial_ converts the *pd // ), // $ + + === 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. +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 @@ -607,7 +636,7 @@ 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 + #figure(image("assets/standard-unknot-construction.jpg")) ] #definition[ @@ -629,7 +658,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 ... union K_n$. +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 @@ -687,18 +716,15 @@ Let now $K$ be an oriented link with $n$ components so $K = K_1 union ... union $ ] +#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. -This initial implementation uses `SignedGaussCodes` as they are easier to work with when working with crossing switches and splices but with some modifications the code could be adapted to work directly on `PDCode` provided of some efficient implementations of `splice_h` and `splice_v` methods. - -#pagebreak() - === Signed Gauss Codes -We are now going to walk thorough the class that lets use work nicely with *Signed Gauss Codes*. The the classes we are going to use are all _frozen data-classes_ to ensure immutability and enforce a more functional programming style. +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) @@ -800,62 +826,93 @@ The `std_unknot_switching_sequence` method just walks along each component in it ```python def apply_switching_sequence(self, seq: list[int]) -> SignedGaussCode: - return SignedGaussCode( - [ + return SignedGaussCode([ [ - crossing.opposite() if crossing.id in switching_sequence else crossing + crossing.opposite(invert_handedness=True) + if crossing.id in seq else crossing for crossing in component ] for component in self.components - ] - ) + ]) ``` -Applying a switching sequence is just a matter of walking along the crossings and flipping the crossings that are in the sequence. This is also how the `switch_crossing(id: int)` method works. +Applying a switching sequence is just a matter of flipping the crossings that are in the sequence. This is also how the `switch_crossing(id: int)` method works. ==== 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 isotopy). So we have $2$ cases for the crossing sign +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 -#{ - set align(center) +- Splice type: horizontal or vertical - grid( - columns: 4, - grid( - // - columns: 5, - gutter: 1.5em, - align: horizon, +- Crossing sign: left-handed or right-handed - skein-generic(kind: "over", direction: (+1, +1)), - skein-generic(kind: "over", direction: (+1, -1)), - [$arrow.squiggly$], - skein.h, - [$arrow.squiggly$], - - skein-generic(kind: "over", direction: (+1, +1)), - skein-generic(kind: "over", direction: (+1, -1)), - [$arrow.squiggly$], - skein.v, - [$arrow.squiggly$], - ), - ) -} +- 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/splice-h-cases.png", width: 125%), + caption: [Cases for horizontal splicing], +) -So the final code is just a conversion of all this cases to list slicing and re-joining with the appropriate crossings removed. +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. -```python -def splice_h(self, id: int): - raise NotImplementedError("Splicing not implemented yet") +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 -def splice_v(self, id: int): - raise NotImplementedError("Splicing not implemented yet") -``` +$ + [ 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/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() diff --git a/theme.typ b/theme.typ index cd5294f..59b1eef 100644 --- a/theme.typ +++ b/theme.typ @@ -42,7 +42,6 @@ set text(size: normal-size, font: "New Computer Modern") // set text(size: normal-size, font: "Fira Sans", weight: 400) - // show math.equation: set text(font: "Fira Math") // set strong(delta: 100) // Configure the page. @@ -112,9 +111,9 @@ smallcaps(it.body) v(normal-size * 1.5, weak: true) } else { - set text(size: 10pt, fill: luma(20%)) + set text(size: 10pt, fill: luma(15%)) v(normal-size * 2, weak: true) - [$thin diamond.medium space$] + [$thin diamond.medium.filled space$] strong(it.body) v(normal-size * 1.5, weak: true) } @@ -139,7 +138,7 @@ } // Configure equations. - show math.equation: set block(below: 8pt, above: 9pt) + show math.equation: set block(below: normal-size * 1.5, above: normal-size * 1.5) show math.equation: set text(weight: 400) show raw: set text(font: "JetBrains Mono")