Skip to content

Commit b4d4d81

Browse files
Bands small additions (#64)
* CI: update cibuildwheel and add Python 3.14 * verify_ribbon_to_unknot: increase robustness
1 parent 4fe5e33 commit b4d4d81

5 files changed

Lines changed: 63 additions & 12 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ jobs:
88
runs-on: ${{ matrix.os }}
99
strategy:
1010
matrix:
11-
os: [ubuntu-latest, macos-13, macos-14, windows-2022]
11+
os: [ubuntu-latest, ubuntu-latest-arm, macos-15-intel, macos-latest, windows-2022]
1212

1313
steps:
1414
- uses: actions/checkout@v4
1515

1616
- name: Build wheels
17-
uses: pypa/cibuildwheel@v2.22
17+
uses: pypa/cibuildwheel@v3.3
1818

1919
- uses: actions/upload-artifact@v4
2020
with:

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
version: [9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 10.1, 10.2, 10.3, 10.4, latest]
12+
version: [9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, latest]
1313
fail-fast: false
1414

1515
container:

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ requires = ["setuptools", "wheel", "cython"]
33
build-backend = "setuptools.build_meta"
44

55
[tool.cibuildwheel]
6-
build = "cp38* cp39* cp310* cp311* cp312* cp313*"
7-
skip = "*musllinux*"
6+
build = "cp38* cp39* cp310* cp311* cp312* cp313* cp314*"
7+
skip = "*musllinux* cp313t* cp314t*"
88
test-command = "python -m spherogram.test"
99

1010
[tool.cibuildwheel.macos]

spherogram_src/links/bands/merge_links.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ def are_isometric_as_links(A, B):
3333
return False
3434

3535

36+
def are_same_triangulations_and_links(A, B):
37+
if hasattr(A, 'exterior'):
38+
A = A.exterior(with_hyperbolic_structure=False)
39+
40+
if hasattr(B, 'exterior'):
41+
B = B.exterior(with_hyperbolic_structure=False)
42+
43+
isos = A.isomorphisms_to(B)
44+
return any(iso.extends_to_link() for iso in isos)
45+
46+
3647
def link_isotopy_classes(links_with_manifolds):
3748
"""
3849
Given a collection of links, try to select one from each isotopy

spherogram_src/links/bands/search.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from ..links import Link
2-
from .merge_links import link_isotopy_classes, are_isometric_as_links
2+
from .merge_links import (link_isotopy_classes,
3+
are_isometric_as_links,
4+
are_same_triangulations_and_links)
35
from .core import Band, add_one_band, banded_links, normalize_crossing_labels
46

57

@@ -81,6 +83,33 @@ def remove_reidemeister_I(link):
8183
link._build_components(component_starts)
8284
return success
8385

86+
def are_same_link(L0, L1, tries=10):
87+
"""
88+
Try to determine whether the given links are isotopic *or* one is
89+
isotopic to the mirror of the other.
90+
91+
If it returns True, then the links are definitely equivalent. If
92+
it returns False, then they may still be the same.
93+
94+
When both links are hyperbolic, this is function is robust.
95+
Otherwise, whether it succeeds is pretty stochastic.
96+
"""
97+
L0, L1 = L0.copy(), L1.copy()
98+
for i in range(tries):
99+
if (L0.PD_code() == L1.PD_code() or
100+
are_isometric_as_links(L0, L1) or
101+
are_same_triangulations_and_links(L0, L1)):
102+
return True
103+
104+
L0.backtrack(30)
105+
L0.simplify()
106+
L0.simplify('global')
107+
L1.backtrack(30)
108+
L1.simplify()
109+
L1.simplify('global')
110+
111+
return False
112+
84113

85114
def verify_ribbon_to_unknot(link, certificate):
86115
"""
@@ -92,22 +121,29 @@ def verify_ribbon_to_unknot(link, certificate):
92121
>>> verify_ribbon_to_unknot(L, cert) #doctest: +SNAPPY
93122
True
94123
95-
TODO: Doesn't work when some intermediate link is non-hyperbolic,
96-
in this case a connected sum of two trefoils. The below should
97-
return True
124+
TODO: Sometimes doesn't work when some intermediate link is
125+
non-hyperbolic. in this case a connected sum of two trefoils.
126+
The below should return True but might fail occasionally.
98127
99128
>>> L = Link([(0,28,1,27),(10,1,11,2),(2,11,3,12),(36,3,37,4),(19,5,20,4),(5,23,6,22),(15,6,16,7),(34,8,35,7),(8,25,9,26),(26,9,27,10),(12,36,13,35),(13,20,14,21),(21,14,22,15),(23,17,24,16),(17,31,18,30),(31,19,32,18),(24,33,25,34),(28,0,29,37),(29,33,30,32)])
100129
>>> cert = [[(0,28,1,27),(10,1,11,2),(2,11,3,12),(36,3,37,4),(19,5,20,4),(5,23,6,22),(15,6,16,7),(34,8,35,7),(8,25,9,26),(26,9,27,10),(12,36,13,35),(13,20,14,21),(21,14,22,15),(23,17,24,16),(17,31,18,30),(31,19,32,18),(24,33,25,34),(28,0,29,37),(29,33,30,32)],'483d_0_0',[(5,2,6,3),(3,6,4,7),(1,4,2,5),(7,11,8,10),(11,9,0,8),(9,1,10,0)],'1603_0_1','unknot']
101130
>>> verify_ribbon_to_unknot(L, cert) #doctest: +SNAPPY
102-
False
131+
True
103132
"""
104-
assert certificate[-1] == 'unknot'
133+
import snappy
134+
if certificate[-1] != 'unknot':
135+
try:
136+
snappy.RibbonLinks[certificate[-1]]
137+
except KeyError:
138+
raise ValueError('ribbon_cert does not end with known ribbon link')
139+
105140
L = link.copy()
106141
E = L.exterior()
107142
for i in range(0, len(certificate) - 1, 2):
108143
L_cert = Link(certificate[i])
109144
E_cert = L_cert.exterior()
110-
if not (L.PD_code() == L_cert.PD_code() or are_isometric_as_links(E, E_cert)):
145+
146+
if not are_same_link(L, L_cert):
111147
return False
112148

113149
band = Band(certificate[i + 1])
@@ -123,6 +159,10 @@ def verify_ribbon_to_unknot(link, certificate):
123159
L.unlinked_unknot_components = 0
124160
E = L.exterior()
125161

162+
if certificate[-1] != 'unknot':
163+
E_cert = snappy.RibbonLinks[certificate[-1]]
164+
return are_isometric_as_links(E, E_cert)
165+
126166
return len(L.link_components) == 0 or is_unlink_exterior(E)
127167

128168

0 commit comments

Comments
 (0)