explaination of splicing and strand reversals parts, more on pd vs sg codes

main
parent becfb9450f
commit 3d42203c89

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

@ -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<List<(Int, Int)>>
- 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()

@ -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")

Loading…
Cancel
Save