bfh_python

A bordered Heegaard Floer computation package

Overview and getting started

About bfh_python and this documentation

bfh_python is a package for doing computations of the Heegaard Floer homology group HF-hat of closed 3-manifolds, knot Floer homology of knots in 3-manifolds, and the bordered Heegaard Floer homology of 3-manifolds with boundary (preferably connected). It is written in Python, a widely-used and user-friendly interpreted programming language. It was written primarily by Bohua Zhan, with some contributions by Robert Lipshitz.

The mathematics underlying bfh_python is mostly in "Computing HF-hat by factoring mapping classes" (MathSciNet, article, arXiv) by Robert Lipshitz, Peter Ozsváth, and Dylan Thurston, and in Bohua Zhan's Ph.D. thesis (MathSciNet, Princeton repository). Other citations are noted on the Mathematics page.

These pages document basic usage of bfh_python, as well as the structure and philosophy of the code itself, to help with extending it. Funding to support writing this documentation was provided by NSF Grant DMS-1810893. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

Getting started

To use bfh_python, you need:

If you are familiar with Python and GitHub, please skip to the next section. Otherwise, some further advice follows.

Python interpreters (or shells)

I use the free version of PyCharm for working with Python, but it took a little effort to set up and if you just want to do a few computations, it is probably overkill. For Linux or a Mac, perhaps it is easiest to just install Sage, which comes with a nice Python shell. You can then work with bfh_python from either the Sage terminal or a Sage notebook. (The former is probably easier for following the instructions below.) For Windows, download the Python installer for Windows, install it, and run Python 3.8 from the start menu.

Downloading bfh_python

From bfh_python's GitHub page, click the green "Clone or download" link and select "Download ZIP".

Alternatively, you can clone the git repository: if you have git installed, from a terminal type git clone https://github.com/bzhan/bfh_python.git

Tutorials on basic usage

Computing the Heegaard Floer homology of a branched double cover

Perhaps the easiest application of the package is to compute the total dimension (over Z/2Z) of the Heegaard Floer homology of the branched double cover of the plat closure of a braid. For example, to compute the Heegaard Floer homology of the branched double cover of the (-2,3,5) pretzel

image of (-2,3,5) pretzel

open a Python shell inside the folder (directory) containing bfh_python and type:

import braid
from braid import BridgePresentation
name = 'P(-2,3,5)'
left_closure = (6,3,2,5,4,1)
braid = [-1,-1,3,3,3,5,5,5,5,5]
right_closure = (6,3,2,5,4,1)
pretzel = BridgePresentation(name, left_closure, braid, right_closure)
pretzel.getHF()

The output should look something like:

(compose Mor 28) 1,2,3,4,7,8,11,12,15,14,10,13,16,15,11,14,17,16,14,17,20,19,17,20,23,22,20,23,26

Chain complex. d(g1) = 0

This is a bit confusing, but it means the result was a chain complex with one generator, called g1, and trivial differential. (The code already took homology.) Simpler is to ask about the length (dimension of a basis) of this complex:

len(pretzel.getHF())

gives output

(compose Mor 28) 1,2,3,4,7,8,11,12,15,14,10,13,16,15,11,14,17,16,14,17,20,19,17,20,23,22,20,23,26
1

That 1 means the result was 1-dimensional: we have an integer homology sphere L-space (in this case, the Poincaré homology sphere).

The syntax for the input is as follows. The name can be any string. The braid word uses i to denote σi and -i to denote σi-1. The bottom and top closures say how to cap off the braid on the two sides. If the braid has n endpoints they record the involution τ of {1,...,n} so that i and τ(i) are joined up.

Here's the branched double cover of the torus knot T(3,5),

image of (3,5) torus knot

name = 'T(3,5)'
bottom = (6,5,4,3,2,1)
braid = 5*[1,2]
top = bottom
T35 = BridgePresentation(name, bottom, braid, top)
T35.getHF()

(The 5*[1,2] produces the list [1,2,1,2,1,2,1,2,1,2].) The result is a chain complex with one generator and trivial differential again (not a coincidence).

There's actually a faster method for the last step, using Zhan's notion of "local DA structures." Instead of getHF() use:

pretzel.getHFByLocalDA()
T35.getHFByLocalDA()

The Heegaard Floer homology of some lens spaces and the Poincaré homology sphere

The following code computes the dimension of HF-hat of the lens space L(5,2):

from dstructure import infTypeD, zeroTypeD
from pmc import splitPMC
from arcslide import Arcslide
from arcslideda import ArcslideDA

Z = splitPMC(1)
solid_torus = infTypeD(1)
tau_m = ArcslideDA(Arcslide(Z,1,2))
tau_l = ArcslideDA(Arcslide(Z,2,1))
slides = [tau_m, tau_m, tau_l, tau_l]
for s in slides:
    solid_torus = s.tensorD(solid_torus)
oth_side = zeroTypeD(1)
cf = oth_side.morToD(solid_torus)
cf.simplify()
len(cf)

The output of the final command should be 5, the dimension of HF-hat of L(5,2).

The steps in the code are:

  1. Import statements: tell Python to read in the pieces of the package that are needed for the computation.
  2. Let Z be the genus 1 pointed matched circle. (splitPMC(n) gives the genus n split pointed matched circle.)
  3. Let solid_torus be a solid torus with the standard "infinity framing" (conventions are from "Computing HF-hat by factoring mapping classes".)
  4. Let tau_m and tau_l be the type DA bimodules associated to the two standard Dehn twists on the torus, which are the same as two arcslides in this case. (See the figure below.)
  5. Tensor with these arcslide bimodules to change the framing on the solid torus to a (5,2) (or maybe (2,5)) framing.
  6. Create a zero-framed solid torus.
  7. Let cf be the chain complex of morphisms between the two. This is equivalent to gluing one of the solid tori to the mirror of the other.
  8. Simplify cf and compute the dimension of the result.

The bordered Heegaard diagrams for the solid tori at the different stages are:
3. solid_torus: bordered Heegaard diagram for the infinity-framed solid torus
4. tau_m and tau_l, respectively: the arcslide tau_m the arcslide tau_l
5. solid_torus after tensoring each slide: diagrams for arcslides glued to a solid torus
6. oth_side: bordered Heegaard diagram for the zero-framed solid torus
7. Heegaard diagram for L(5,2)

You can inspect the type D structure for the framed solid tori. For example, to see the type D structures for the solid torus at the last stage, type:

solid_torus.reindex()
solid_torus.generators
solid_torus.delta_map

(The first line replaces the very long names of generators with short ones. Try re-creating solid_torus and then typing solid_torus.generators without reindexing first and you'll see what I mean.) The output is:

{g2, g3, g5, g6, g7, g4, g1}
{g5: [0->3]**g6, g2: [0->3]**g1, g7: [1->2]**g5, g3: [1->2]**g2, g1: [1->3]**g4, g4: [1->3]**g7, g6: [1->3]**g3}

This means: there are severn generators, g1 through g7. The differential of g3, for instance, is rho_2 times g2: rho_2 is the chord corresponding to a strand from point 1 to point 2. The differential of g5 is rho_123 times g_6; and so on.

In particular, this agrees with a by-hand computation for this bordered Heegaard diagram.

Important note. For arcslides, the input pointed matched circle (which becomes start_pmc in the Arcslide class) is on the left. The result of doing the specified arcslide to that pointed matched circle is on the right (and stored as end_pmc). When one tensors the arcslide to a type D structure, it is the right pointed matched circle end_pmc that gets glued to the type D structure. The new boundary is given by start_pmc. (Type D structures are left modules.) So, one often creates a list of arcslides from left to right, then reverses that list before gluing it tensoring it with a type D structure.

The Heegaard Floer homology of another branched double cover

We will compute the Heegaard Floer homology of the branched double cover of the following knot:

image of (-2,3,5) pretzel

(I think this is L10n36.)

We could present this knot as a 4-bridge knot, but bfh_python is slow for wide knots. Instead, we'll live with the cups in the middle.

from cobordism import Cobordism, LEFT, RIGHT
from cobordismda import CobordismDALeft, CobordismDARight
from arcslideda import ArcslideDA
from braid import Braid, BraidCap
braid_group = Braid(6) #Really, this is the braid group on 6 strands.
left_braid_word = [2,2,-4,-4,-4]
right_braid_word = [-2,-2,4,4,4]
left_slides = braid_group.getArcslides(left_braid_word) #This is a sequence of Arcslides
right_slides = braid_group.getArcslides(right_braid_word)
closure_seq = (2,1,4,3,6,5) #The plat matching
plat = BraidCap(closure_seq) 
left_cups = plat.openCap()
answer = left_cups
for slide in right_slides:
    rev_slide = slide.inverse() #There's an orientation-reversal; don't ask.
    answer = ArcslideDA(rev_slide).tensorD(answer) #Tensor the DA bimodule for this arcslide with the answer
    answer.reindex() #Give generators simple names.
    answer.simplify() #Simplify the type D structure (cancel arrows).
    print(len(answer))
#answer is now the type D structure associated to the right half of the picture.
cap_cob = Cobordism(genus=2, c_pair=2, side=RIGHT) #The cap cobordism.
cap_cob_DA = CobordismDARight(cap_cob) #The DA bimodule for the cap.
cup_cob = Cobordism(genus=2, c_pair = 2, side=LEFT) #The cup cobordism.
cup_cob_DA = CobordismDALeft(cup_cob) #The DA bimodule for the cup.
answer = cap_cob_DA.tensorD(answer)
answer.reindex()
answer.simplify()
answer = cup_cob_DA.tensorD(answer)
answer.reindex()
answer.simplify()
for slide in left_slides: #Do the left half.
    rev_slide = slide.inverse()
    answer = ArcslideDA(rev_slide).tensorD(answer) 
    answer.reindex() #Give generators simple names.
    answer.simplify() #Simplify the type D structure (cancel arrows).
    print(len(answer))
answer = plat.closeCap(answer)  #Close it off
len(answer)

I get that the total dimension of HF-hat is 6. As a test, we can compute this the slow way, from the following bridge diagram: image of (-2,3,5) pretzel

left_closure = (2,1,6,5,4,3,8,7)
braid = [2,2,-6,-6,-6,3,-5,-2,-2,6,6,6]
right_closure = left_closure
L10n36 = BridgePresentation("L10n36", left_closure, braid, right_closure)
L10n36.getHF()

(This is substantially slower on my computer.)

It is interesting to compare with Seed's computation of Szabó's geometric spectral sequence, in KnotKit. For this knot diagram, KnotKit finds the dimension of the E-ininity page is 12=(2)(6), as expected.

Some elements of the bordered algebra and the homology of the algebra

Code for working with the bordered algebras is in pmc.py. The main classes are:

  • PMC: represents pointed matched circles
  • StrandAlgebra: represents a bordered algebra
  • StrandDiagram: represents a generator of a bordered algebra
  • StrandAlgebraElement: an element of a strand algebra.

There are also some helper functions, like splitPMC, linearPMC, and antipodalPMC to generate particular pointed matched circles.

For example, the following code computes the generators

image of (-2,3,5) pretzel

of the genus 2 pointed matched circle, their product, and the differential of each of them:

from pmc import splitPMC, StrandAlgebra, StrandDiagram, E0
spmc = splitPMC(2)
alg = StrandAlgebra(F2, spmc, idem_size=2, mult_one = False)
a = StrandDiagram(alg, left_idem = [0,1], right_idem = [1,3],strands=[(0,5),])
b = StrandDiagram(alg, left_idem=[1, 3], strands=[(3, 4), (5, 7)])
a*b
a.diff()

Here:

  • F2 is the ground field, the field with two elements.
  • idem_size is the number of pairs in an idempotent, from 0 to 2k for a genus k pointed matched circle.
  • left_idem and right_idem are lists of which alpha-arcs are occupied (see the picture above). Each is a subset of {0,...,2k-1}. It is not necessary to specify both left_idem and right_idem.
  • strands is a list of the pairs of points in {0,...,4k-1} connected by strands.
  • mult_one indicates whether to quotient by the acyclic ideal of algebra elements with multiplicity >1 somewhere.

The output of multiplication or the differential is a StrandAlgebraElement. The element 0 is represented by E0. So, for example

b*a == E0

is True. You can use pmc.pairid to find out which points on the pointed matched circles correspond to which matched pairs (for idempotents); for example, above, spmc.pairid[3] is 1.

There is a shorthand way to get strand diagrams from pmc.sd(); for example

a == spmc.sd([1,(0,5)], mult_one = False)
b == spmc.sd([(3,4),(5,7)], mult_one = False)

both are True.

Here is code which computes the homology of alg:

from algebra import SimpleChainComplex, SimpleGenerator
cx = SimpleChainComplex(F2)
alg_to_gens = dict()
for a in alg.getGenerators():
    agen = SimpleGenerator(cx,repr(a))
    cx.addGenerator(agen)
    alg_to_gens[a] = agen
for a in alg.getGenerators():
    for b in a.diff():
        cx.addDifferential(alg_to_gens[a],alg_to_gens[b],1)
cx.simplify()
len(cx)

The answer, 98, agrees with the literature.

Testing your copy of the package

The files ending "test.py" have code to test that the package is working. For example, "pmctest.py" has code to test "pmc.py" (which has classes for pointed matched circles and their algebras). To run one of these test files -- pmctest.py, for instance -- from the command line (not a Python shell) run:

python pmctest.py

To run all of the test files, run the script "testmod" or, have it display how fast the tests ran, run "testmodp".

Warning. Some of the test code -- particularly "braidtest.py" -- takes a while to run.

Finding help inside the package

Many of the classes and functions in the package have "doc strings", short explanations of how they work immediately after their declarations. For example, the Braid class in braid.py helpfully explains:

class Braid(object):
    """Represents a braid with a fix number of strands. Each braid generator is
    represented by an integer n. If 1 <= n <= num_strands-1, it represents
    moving strand n over strand n+1. If -(num_strand-1) <= n <= -1, it
    represents moving strand |n| under strand |n|+1.

    """

From inside a python shell, you can access these using the help command. For example:

from braid import Braid
help(Braid)

Another source of help -- and a big source of sample code -- are the test files (see above).

Package limitations

Most of the package's limitations come from limitations of bordered Heegaard Floer homology as currently constructed. In particular:

  • The package only computes HF-hat, not HF-minus or other variants.
  • All computations are over Z/2Z, the field with two elements.
  • The package does not compute the absolute grading. It does compute the decomposition into spinc-structures and the relative gradings; see the Further Tutorials.
  • The efficiency of the package is strongly related to the Heegaard genus; above Heegaard genus 5, computations are typically too slow to be useful, and in Heegaard genus 4 and 5, computations are somewhat slow.

There are other milder limitations, some of which would be easy to address. For example, there isn't efficient code to check if two type D structures are homotopy equivalent, and there is no code to tensor together type DA bimodules.

Further documentation

The rest of this website has:

  • A brief summary, with references, of the mathematics underlying bfh_python. This is aimed at someone familiar with Heegaard Floer homology but not with bordered Heegaard Floer homology. The summary tries to describe enough of the structure of the theory for one to understand how bfh_python's components fit together, and what they can and cannot do.
  • The structure of the bfh_python package. What the different classes encode and how they fit together.
  • Further tutorials. A few more pieces of sample code, with brief explanations, exploring other features of bfh_python or reproducing computations from the literature.

(Currently, the page about mathematics has been written, and there is one "further tutorial"; the rest is still in progress.)