Python library for computing invariants of Legendrian knots and links in contact topology. Knots are encoded as plat-closure braid words and the library computes classical invariants (tb, rot), the contact-homology DGA (over Z/2, Z[λ], or Z/p), augmentations, linearized homology, and rulings.
The algorithms originate in a Mathematica notebook by Paul Melvin, extended by Kirk Mangels, Alden Walker, Lenny Ng, Josh Sabloff, and Sumana Shrestha.
| File | Description |
|---|---|
legendrian.py |
Primary API |
test_legendrian.py |
Test suite (654 tests) |
auxiliary_data/grid_atlas.py |
XO grid-diagram representatives from the Ng Legendrian atlas |
auxiliary_data/petkova_atlas.py |
Loader for the Petkova et al. atlas CSV data |
auxiliary_data/layer{1,2,3}.csv |
Downloaded CSV files from the Petkova atlas |
from legendrian import Leg, GroundRing
# Construct from braid word or atlas name
k = Leg([2, 2, 2]) # Legendrian trefoil
k = Leg('mK3_1') # same, from atlas
# Classical invariants
print(k.tb, k.rot) # 1, 0
# DGA over Z/2 and augmentations
d = k.dga()
for aug in d.augmentations():
print(aug.lin_hom) # {grading: dimension}
# DGA over Z[λ]
d2 = k.dga('Zlambda')
d2.print_differential()
d2.check_d_squared()
# Rulings
print(k.ruling_invariant()) # Z-graded ruling polynomial as {degree: coeff}
print(k.ruling_invariant(grading_mod=2)) # Z/2-graded ruling polynomial
print(k.format_ruling_invariant()) # ruling polynomial as a string in zLeg accepts four input forms.
Braid word — a list of positive integers encoding the plat closure of a positive braid. The integer i represents σ_i, a positive crossing between strands i and i+1 (1-indexed from the top). All left cusps share one x-coordinate and all right cusps share another.
Leg([2, 2, 2]) # standard Legendrian right-handed trefoilAtlas name — a string key into the built-in ATLAS of pre-computed representatives.
Leg('mK3_1') # mirror trefoil (unique representative)
Leg('mK5_2.0') # first of two reps of mirror 5_2 (0-indexed)Tangle decomposition — a list of tuples describing a general front diagram as a left-to-right sequence of elementary moves:
| Tuple | Meaning |
|---|---|
('<', h) |
Left cusp at height h |
('>', h) |
Right cusp at height h |
('X', h) |
Positive crossing between strands h and h+1 |
Heights are 0-indexed from the top and refer to the strand configuration at that moment (the strand count changes as cusps are added or removed). The code converts the tangle to plat form internally by applying Legendrian Reidemeister II moves, so any valid front-diagram decomposition is accepted, not just plat-ordered ones.
# Trefoil described as a tangle (left cusps first, then crossings, then right cusps)
Leg([('<', 0), ('<', 0),
('X', 1), ('X', 2), ('X', 0), ('X', 2), ('X', 0), ('X', 2), ('X', 1),
('>', 0), ('>', 0)])
# Same knot with cusps interleaved among crossings — algorithm rearranges it
Leg([('<', 0), ('X', 0), ('X', 0), ('<', 0), ('>', 0), ('>', 0)])The original tangle is stored as self.tangle. The name keyword argument works the same way as for the other input forms.
Grid diagram — a 2-tuple of permutations (X_perm, O_perm). X_perm[j] is the row of the X marker in column j; O_perm[j] is the row of the O marker in column j. Rows are 0-indexed from the bottom; columns are 0-indexed from the left.
Conventions:
- Vertical strands (in each column) pass over horizontal strands (in each row) at each crossing.
- The Legendrian front projection is obtained by rotating the grid 45° counter-clockwise. Under this rotation, horizontal segments become slope +1 arcs and vertical segments become slope −1 arcs. The markers become cusps or smooth kink points.
- Left cusps arise where both the horizontal and vertical strand leave the marker going rightward (in the rotated diagram). Right cusps arise where both arrive from the left. Points where the strand transitions from slope −1 to slope +1 (or vice versa) without a cusp are kinks, handled internally.
The code produces a tangle sequence from the grid and then converts it to plat form via Legendrian Reidemeister moves. Both self.grid and self.tangle are stored.
Leg(([1, 0], [0, 1])) # 2×2 grid: Legendrian unknot, tb = -1
Leg(([0, 1, 2], [1, 2, 0])) # 3×3 grid: once-stabilised unknot, tb = -2A Legendrian knot or link.
Leg([2,2,2]) # from braid word (list of positive ints)
Leg('mK3_1') # unique atlas entry
Leg('mK3_1.0') # also works
Leg('mK5_2.0') # first of two reps, 0-indexed
Leg([('<', 0), ('X', 0), ('>', 0)]) # from tangle decomposition
Leg(([1, 0], [0, 1])) # from grid diagram (2-tuple of permutations)Classical invariants (computed once, cached):
| Property | Type | Description |
|---|---|---|
num_components |
int |
Number of link components |
grading |
List[int] |
Maslov grading of generators |
tb |
int |
Thurston-Bennequin number |
rot |
int |
Rotation number |
ruling_invariant(grading_mod=0) |
Dict[int, int] |
Ruling polynomial coefficients |
DGA and augmentations:
| Method | Returns | Description |
|---|---|---|
dga(ring=None) |
DGA |
DGA over ring; defaults to DEFAULT_GROUND_RING (Z/2). Cached per ring. |
augmentations(grading_mod, modulus) |
List[Augmentation] |
Shorthand for dga(Z/modulus).augmentations(grading_mod) |
all_lin_hom(grading_mod, modulus, format) |
List[Dict[int,int]] or List[str] |
Distinct Poincaré-Chekanov polynomials; format=True returns strings |
rulings(grading_mod) |
List[List[int]] |
All graded rulings; cached per grading_mod |
format_ruling_invariant(grading_mod=0) |
str |
ruling_invariant as a polynomial string in z |
Visualization:
k.draw() # front projection (plat form); returns matplotlib Figure
k.draw(label_generators=True) # same, with generator labels
k.draw(use_tangle=True) # draw the tangle sequence instead of the plat
# (raises AttributeError if no tangle is stored)
k.export_svg('knot.svg') # writes SVG of the plat diagram, returns filenameThe contact-homology DGA of a Leg over a chosen GroundRing. Obtain via Leg.dga(ring).
d = k.dga() # Z/2
d = k.dga('Zlambda') # Z[λ]
d = k.dga(GroundRing.Zn(3)) # Z/3| Member | Description |
|---|---|
differential |
Differential; format depends on ring. Lazy, cached. |
augmentations(grading_mod) |
All augmentations; cached. |
all_lin_hom(grading_mod, format) |
Distinct Poincaré-Chekanov polynomials; format=True returns strings. Cached. |
lin_hom_reps(grading_mod) |
(poly, representative_augmentation) pairs, one per distinct polynomial. |
check_d_squared() |
Verify d²=0 (Z/2 and Z[λ] only). |
aug_count() |
Normalized augmentation number (Z/2 and Z/p only). |
print_differential() |
Print d(a[i]) for each generator. |
The grading_mod parameter controls grading: 0 = Z-graded, 1 = ungraded, n ≥ 2 = Z/n-graded.
A single augmentation ε: (DGA generators) → (ground ring). Obtained via DGA.augmentations().
| Member | Description |
|---|---|
data |
Raw augmentation data (Z/2: list of generators sent to 1; Z/p: dict generator → value) |
dga |
The parent DGA |
lin_hom |
Poincaré-Chekanov polynomial {grading: dim}; cached |
format_poincare() |
lin_hom formatted as a polynomial string in t |
cohomology_basis |
Basis for linearized cohomology (Z/2 only); cached |
double_products |
Cup-product table (table, basis) (Z/2 only); cached |
Coefficient ring for DGA computations.
GroundRing.Z2 # Z/2 (default)
GroundRing.ZLAMBDA # Z[λ]
GroundRing.Zn(5) # Z/5 (p prime)
GroundRing('Z2') # string constructor
GroundRing('Zlambda')
GroundRing('Z3')The global default ring is DEFAULT_GROUND_RING = GroundRing.Z2. You can change it:
import legendrian
legendrian.DEFAULT_GROUND_RING = GroundRing.Zn(5)ATLAS is a dict of pre-computed Legendrian representatives for knots through ~15 crossings, keyed by name:
from legendrian import ATLAS
list(ATLAS) # all available names
ATLAS['mK5_2'] # list of two braid wordsNaming convention:
| Name | Meaning |
|---|---|
K3_1 |
Right-handed trefoil (Rolfsen table, unique rep) |
mK3_1 |
Mirror of 3_1 (unique rep) |
mK5_2.0 |
First rep of mirror of 5_2 (0-indexed) |
mK5_2.1 |
Second rep |
K11n38 |
Knot 11n38 (Hoste-Thistlethwaite table) |
Prefix m = mirror, K = knot, L = link. Legendrian index is 0-based; omit when there is only one representative.
Data sourced from the Legendrian knot atlas.
auxiliary_data/petkova_atlas.py provides a read-only loader for the separate Petkova et al. atlas, which covers 466 knot types with 2696 Legendrian representatives. It is kept separate because it uses different conventions and is less established in the literature.
from auxiliary_data.petkova_atlas import layer1, layer1_by_knot, to_leg_grid
from legendrian import Leg
# Iterate all maximal-tb representatives
for entry in layer1():
x, o = to_leg_grid(entry) # converts row-indexing convention
leg = Leg((list(x), list(o)))
assert leg.tb == entry.tb # always holds
assert abs(leg.rot) == abs(entry.r)
# Look up by knot name (HT notation)
reps = layer1_by_knot()['8a1'] # list of Layer1Entry for 8a1Knot naming: uses Hoste-Thistlethwaite notation (8a1, m10n3, 11n38) rather than the Rolfsen-style K8_19 names used in ATLAS.
Convention note: the CSV files index grid rows from the top (row 0 = top); to_leg_grid converts to the bottom-indexed convention expected by Leg. The sign of the rotation number may differ by a global orientation choice.
Layers: Layer 1 contains full grid data. Layers 2 and 3 contain stabilised representatives (only tb, r, and parent IDs — no grid diagrams).
Module-level functions that build a new Leg from an existing one:
| Function | Description |
|---|---|
whitehead_double(leg) |
Legendrian Whitehead double of leg |
twisted_2cable(leg) |
Legendrian twisted 2-cable of leg |
from legendrian import Leg, whitehead_double, twisted_2cable
k = Leg('mK3_1')
wd = whitehead_double(k) # Leg('WhiteheadDouble(mK3_1)')
tc = twisted_2cable(k) # Leg('Twisted2Cable(mK3_1)')
print(wd.tb, wd.rot)from legendrian import Leg
k = Leg('mK3_1')
print(k.tb, k.rot) # 1, 0
print(k.num_components) # 1
d = k.dga()
print(d.check_d_squared()) # True
augs = d.augmentations()
print(len(augs)) # 10
for a in augs:
print(a.lin_hom) # each: {1: 1, 0: 2}k = Leg('K4_1')
print(k.tb, k.rot) # -3, 0
d = k.dga()
print(d.aug_count())
print(k.rulings())
print(k.ruling_invariant())k = Leg([2, 2, 2])
d = k.dga('Zlambda')
d.print_differential()
print(d.check_d_squared())from legendrian import Leg, ATLAS
for i in range(len(ATLAS['mK5_2'])):
k = Leg(f'mK5_2.{i}')
print(k.name, k.tb, k.rot, k.all_lin_hom())python3 -m pytest test_legendrian.py- Python 3.8+
sympy(for Z[λ] polynomial arithmetic)matplotlib(fordrawandexport_svg)
legendrian_links (Avdek et al.) is a Python project with overlapping goals and a similar plat-diagram approach. It focuses on augmentation enumeration, bilinearized homology, and planar diagram algebras (RSFT), and includes a web application for interactive exploration. It also supports LCH differentials over the integers.
This is a conversion and extension of a Mathematica notebook by Paul Melvin, Kirk Mangels, Alden Walker, Lenny Ng, Josh Sabloff, and Sumana Shrestha. The conversion was performed by Anthropic's Claude chatbot, with assistance from Robert Lipshitz, and was supported by U.S. National Science Foundation grant DMS-2505715. RL thanks Lenhard Ng for helpful discussions about this code and the underlying mathematics.
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
You are free to use, modify, and distribute this software under the terms of the GPL v3. Any derivative work or software that incorporates this code must also be released under the GPL v3.