bfh_python

A bordered Heegaard Floer computation package

The structure of the bfh_python package

The goal of this page is to try to explain the design philosophy (as I understand it) underlying bfh_python, and some of the packages and classes that are most likely to be useful.

Before that, two general pieces of advice. First, most classes and functions have docstrings, which say a little about what they are for and/or how to use them. Some are more extensive than others. Second, all of the modules have a corresponding test module. For example, the pmc.py module (which encodes pointed matched circles and the algebras associated to them) has an associated test module pmctest.py. Looking at the test module is often the easiest way to figure out how to use the various classes, and how they behave. (Of course, you can also use these test modules to test that bfh_python is installed and working prperly; you can run all the test modules by running python regression.py.)

Short description of the modules

Here is a brief summary of each of the modules in bfh_python:

  • algebra.py has classes for chain complexes, generators of chain complexes, morphisms of chain complexes, and tensor products of chain complexes.
  • arcslide.py has the class Arcslide which encodes an arcslide diffeomorphism. Arcslide.getDDStructure() returns the type DD bimodule associated to the arcslide.
  • arcslideda.py has the class ArcslideDA is a class for computing the local type DA structure associated to an arcslide diffeomorphism. The input to ArcslideDA is an Arcslide (from arcslide.py). To get the local DA structure, call ArcslideDA.getLocalDAStructure().
  • autocompleteda.py is a utility module related to local type DA bimodules.
  • braid.py has convenience functions for computing HF-hat of branched double covers of plat closures of braids and the Ozsváth-Szabó spectral sequence.
  • cobordism.py has the class Cobordism which represents a 3-dimensional 1-handle or 2-handle cobordism between pointed matched circles. To get the type DD bimodule associated to a cobordism, use Cobordism.getDDStructure().
  • cobordismda.py computes the local type DA bimodule associated to a 3-dimensional 2-handle cobordism between pointed matached circles. The relevant classes are CobordismDALeft and CobordismDARight, depending on whether the larger-genus pointed matched circle is on the left or right.
  • dastructure.py encodes the algebra of type DA structures, their elements, and certain kinds of tensor products involving them.
  • ddstructure.py encodes the algebra of type DD structures, their elements, and morphism complexes between them and from them to type D structures.
  • dehntwist.py has the class DehnTwist which encodes one of the basic Dehn twists on a linear pointed matched circle and its type DD bimodule, via DehnTwist.getDDStructure().
  • dehntwistda.py encodes the local DA bimodule for the branched double cover of the anti-braidlike resolution of a braid generator, and the local DA bimodule for a basic Dehn twist on a linear pointed matched circle, as the mapping cone of a map between the anti-braidlike resolution and the identity bimodule.
  • digraph.py is a utility package for labeled directed graphs and tensor products of various kinds of modules and bimodules.
  • dstructure.py encodes the algebra of type D structures, their elements, and morphisms between them; certain specific type D structures for handlebodies; and how type D invariants behave under boundary sum (which it calls connectSumTypeD).
  • experimental.py is actually a test module for some experimental features, particularly related to the absolute grading, which probably do not work at present.
  • extendbyid.py encodes the algebra of local type DA bimodules and morphism spaces between them.
  • grading.py encodes the non-commutative grading groups for bordered Floer homology, how to convert between them via grading refinement, and the algebra of G-sets needed for the gradings on modules and bimodules.
  • hdiagram.py encodes bordered Heegaard diagrams, as well as certain specific diagrams, like those for arcslides and a few handlebodies. This is mainly, or perhaps exclusively, used for tracking gradings on bordered bimodules.
  • identityaa.py encodes the type AA bimodules for the identity cobordism of a pointed matched circle.
  • involutive.py has the extra structures needed to compute involutive HF-hat of 3-manifolds: the bimodules associated to the diagram AZ and utility functions for working with morphisms of type D structures (applying them, composing them, checking if they are quasi-isomorphisms, tensoring them with the identity map). It also has utility functions for computing involutive HF-hat of branched double covers.
  • latex.py has code for computing tikz representations of strand diagrams and pretty printing of type DA bimodules.
  • linalg.py has code for row reduction.
  • localpmc.py has classes for local pointed matched circles, for local versions of strand diagrams, and for splitting a genuine pointed matched circle into two local pointed matched circles.
  • minusalg.py has the beginning of code for an attempt to extend bordered Floer homology to HF-. This is archaic, and probably not compatible with work-in-progress on the subject.
  • pmc.py has classes for working with pointed matched circles (PMC) and their algebras (StrandDiagram, StrandAlgebraElement, StrandAlgebra), and utility functions for creating certain specific ones (splitPMC, linearPMC, antipodalPMC, connectSumPMC).
  • regression.py runs the package's self-tests.
  • signs.py has code for computing an absolute ℤ/2 grading on the strands algebra, and an attempt to give a lift of the strands algebra to Z-coefficients. This is experimental, and I am not sure if it reached a useful stage.
  • utility.py has some low-level algebraic classes (rings, the integers, Z/n, etc) and some low-level Python functions related to caching answers.

Basic algebra

The module utility.py has code for the rings ℤ and ℤ/nℤ and elements of them. Since bordered Floer homology is only defined over 𝔽2 so far, one mostly only needs the convenience object F2, which is the ring 𝔽2. (Reasonably often, one does need to import F2 and give F2 as an input to a function.) Typically, you can just write elements of F2 as 0 or 1. (The object ZZ, which is ℤ, is also needed for some grading computations.)

Other basic algebra is in algebra.py. The philosophy is as follows:

  • There are classes for different algebraic structures -- FreeModule, ChainComplex, DGAlgebra and so on.
  • Just about everything is a free module over the ground ring. Since the ground ring for bordered Floer homology is 𝔽2, no loss there.
  • In some cases, particularly ChainComplex, there's a very general class that records what functions a ChainComplex should implement (specifically, a function diff that takes the differential of an element). Then there's another class, SimpleChainComplex, extending this, that's designed for handling chain complexes given in terms of generators and relations. We'll see this in practice more in the section on (bi)modules, below.
  • Everything is a subclass of whatever it logically should be. For example, SimpleChainComplex is a subclass of ChainComplex is a subclass of FreeModule.
  • Free modules (and their subclasses) have Generators, which are basis elements, and Elements, which are linear combinations of generators. The code will automatically coerce linear combinations of Generators (written as a*x+b*y, say) into Elements.
  • Every generator or element knows which algebraic object it belongs to: it has a parent instance variable that points to the specific object that it's an element of. Operations like multiplication actually call the parent's multiplication function. So, if you want to write a new DGAlgebra, you write a differential and a multiplication in your subclass of DGAlgebra, and those are automatically inherited and used by Elements of that new class.
  • Any intellectually distinct structure has its own class and its own kinds of elements. For example, a tensor product of free modules is not just a free module, because you might (and very often do) want to talk about the individual tensor factors. So, it has its own class Tensor, its generators are TensorGenerators, and its elements are TensorElements.

One of the most useful objects is E0, which acts as the zero element of anything that has one. This is especially useful in for loops. For instance, here's the sum of all the basis elements of the bordered algebra of the torus (not that you'd ever need this):

from algebra import E0
from pmc import splitPMC
x = E0
A = splitPMC(1).getAlgebra() #This is an instance of StrandAlgebra, which extends DGAlgebra
for a in A.getGenerators():
    x += 1*a #Multiplying by 1 converts the Generator to an Element.

(Replacing the for loop with sum(A.getGenerators()) would also work.)

Pointed matched circles and their algebras

Pointed matched circle and their algebras are in the module pmc.py. There are several different classes:

  • PMC, which encodes a pointed matched circle,
  • Idempotent, which encodes idempotents in the bordered algebras,
  • StrandDiagram, which encodes a strand diagram (basis element for a bordered algebra),
  • StrandAlgebra, which is the algebra associated to a pointed matched circle, and
  • StrandAlgebraElement, which is an arbitrary element of a StrandAlgebra, or equivalently a formal linear combination of StrandDiagrams.

To create a pointed matched circle, you input a list of pairs of non-negative integers which are matched. Each point on the circle should only appear once. So, for example, the split pointed matched circle of genus 2 is:

Z=PMC([(0,2),(1,3),(4,6),(5,7)])

The easiest way to create Idempotents, StrandDiagrams, and StrandAlgebras is using the convenience functions Z.idem, Z.sd, and Z.getAlgebra in a PMC Z. These behave the way you would expect. For example, to create the idempotent in which the pairs (0,2) and (5,7) are occupied, you type

Z.idem((0,5))

(Typing, for instance, Z.idem((0,7)) would also work.) For a strand diagram in which the left idempotent has the pairs (0,2) and (5,7) occupied and there's a single strand going from 0 to 4, type:

a = Z.sd([(0,4),5])

The 5 indicates that 5 and its pair 7 are occupied by horizontal lines, while the (0,4) says there's a strand from 0 to 4. You could also create this directly, via:

A = Z.getAlgebra()
a = StrandDiagram(parent=A, left_idem=[0,5], strands=[(0,4)])

(You could also just write a = StrandDiagram(A, [0,5], [(0,4)]).)

Most of the rest of the behavior is self-explanatory (and documented briefly in docstrings), with one exception. Recall that the bordered algebras decompose as a direct sum A(Z)=⊕i=02kA(Z,i). The code is somewhat focused on computing HF-hat of closed 3-manifolds, which only depends on A(Z,k). (I've shifted the indexing slightly from some of our papers.) So, by default the whole package just assumes you're interested in A(Z,k). Sometimes, you can access the rest of the algebra and bimodules by using the variable idem_size, but this is not implemented consistently, so beware.

Various kinds of (bi)modules

As discussed in the Mathematics tab, the bordered invariants take the form of various kinds of modules and bimodules. In this section, we'll look at how the code handles two of them: “type D structures” and “type DA structures”.

Recall that a type D structure consists of a vector space X and a linear map δ1: X→A⊗X. bfh_python has two different classes for recording a type D structure (in dstructure.py): and DStructure and its subclass SimpleDStructure. The class DStructure is really generic: it knows it's supposed to have a ground ring (𝔽2 for bordered Floer) and an algebra (A(Z) in the case of bordered Floer), and that someone should implement the function SimpleDStructure.delta (the map δ1), but actually creating a type D structure this way is a pain, and only relevant if one were to extend the algebraic parts of the package to something other than bordered Floer homology.

The more useful class is SimpleDStructure. A SimpleDStructure P consists of a set of generators (stored as P.generators) and a dictionary P.delta_map representing the map δ1. (Generally, as we'll see, you don't interact with the dictionary delta_map directly.) The keys of delta_map are the generators of the SimpleDStructure, and the values are elements of the tensor product A⊗X.

For example, the following creates the invariant CFD of the (-1)-framed solid torus:

from dstructure import SimpleDStructure, SimpleDGenerator
from pmc import splitPMC, StrandDiagram
from utility import F2
Z = splitPMC(1) #The genus 1 pointed matched circle.
A = Z.getAlgebra() #Its algebra
P = SimpleDStructure(F2,A) #The arguments are the ground ring F2 and the algebra A acting on the left.
iota0 = Z.idem([0]) #Idempotent where 0 and its matched pair 2 are occupied.
iota1 = Z.idem([1]) #Idempotent where 1 and its matched pair 2 are occupied.
x = SimpleDGenerator(P,iota0,'x') #Inputs are (parent, idempotent, name for this generator)
y = SimpleDGenerator(P,iota1,'y')
P.addGenerator(x) #Add these as generators to P.
P.addGenerator(y)
rho1 = StrandDiagram(A,iota0,[(0,1)]) #The chord rho1, an element of A. Arguments are (parent, left idempotent, strands)
rho3 = StrandDiagram(A,iota0,[(2,3)]) 
P.addDelta(x,y, rho1, 1) # delta(x) has 1*rho1*y as a term.
P.addDelta(x,y, rho3, 1) # delta(x) has 1*rho1*y as a term.

To evaluate the map δ1 on the generator x, type:

P.delta(x)

The result is [0->1]**y+[2->3]**y, i.e., (ρ13)⊗y: the ** indicates tensor product. You can also see the whole map from P.delta_map.

Although its typically not necessary, here's how to unwrap the output. The output is a linear combination (handled by the Element class) of TensorGenerators. If we let z=P.delta(x) then you can get the terms in z by list(z.items()). Each entry is a pair of a TensorGenerator and its coefficient in 𝔽2. So, w=list(z.items())[0][0] is a TensorGenerator; for me, it's ρ1⊗y though in principle the ordering is arbitrary. The algebra coefficient is w[0] and the generator of the type D structure is w[1].

To find a homotopy equivalent type D structure with fewer generators (by canceling pairs of generators x and y where δ1(x)=y+...) use P.simplify(). This also works for most other classes where it makes sense, like type SimpleDDStructure and SimpleChainComplex; it's not implemented so far for SimpleDAStructure. The morphism complex between type D structures is given by P.morToD. So, for example, the following computes HF^(S2×S1):

C = P.morToD(P) #C is a SimpleChainComplex
C.simplify() #Compute homology
C

Generators of P.morToD(Q) are of class MorDtoDGenerator. In particular, it's possible to apply them to elements of P, and to compose them, using f.apply and f.compose. (This is used, for example, to compute HFI^.)

Type DA (and DD) structures work similarly to type D structures. Again, there's a generic class DAStructure, but the concrete class is SimpleDAStructure. A SimpleDAStructure has a list of generators (of class SimpleDAGenerator) and a dictionary encoding the map δ11+n. Unlike for type D structures, representing the map δ11+n in this concrete, finite way imposes a significant limitation on the type DA structures one can construct: in the language of “Bimodules in bordered Floer homology” they must be right bounded. Because of this limitation, the basic class DAStructure is more fleshed-out and useable than DStructure was. There's even a method DAStructure.toSimpleDAStructure that will convert a DAStructure to a SimpleDAStructure when possible. Still, we'll focus on SimpleDAStructure.

The following creates a type DA structure representing the mapping cylinder of a (particular) Dehn twist on the torus; this bimodule was computed in Section 10.2 of “Bimodules in bordered Heegaard Floer homology”, and is given by

Bimodule for a Dehn twist on the torus.

from dastructure import SimpleDAStructure, SimpleDAGenerator
from pmc import splitPMC, StrandDiagram
from utility import F2
Z = splitPMC(1) #The genus 1 pointed matched circle.
A = Z.getAlgebra() #Its algebra
iota0 = Z.idem([0]); iota1 = Z.idem([1])
rho1 = StrandDiagram(A,iota0,[(0,1)]); rho2 = StrandDiagram(A,iota1,[(1,2)]); rho3 = StrandDiagram(A,iota0,[(2,3)])
rho12 = StrandDiagram(A,iota0,[(0,2)]); rho23 = StrandDiagram(A,iota1,[(1,3)]); rho123 = StrandDiagram(A,iota0,[(0,3)])
Q = SimpleDAStructure(F2,A,A) #The arguments are the ground ring F2, the algebra A acting on the left, and the algebra B acting on the right.   
p = SimpleDAGenerator(Q,iota0,iota0,'p') #Inputs are (parent, left idempotent, right idempotent, name)
q = SimpleDAGenerator(Q,iota1,iota1,'q')
r = SimpleDAGenerator(Q,iota1,iota0,'r')
Q.addGenerator(p); Q.addGenerator(q); Q.addGenerator(r) #Add these as generators to P.
Q.addDelta(p,q,rho1,[rho1,],1) #This is delta^1_2(p,rho1) = rho1 tensor q.
Q.addDelta(p,q,rho123,[rho123,],1) 
Q.addDelta(p,q,rho3,[rho3,rho23],1) #This is delta^1_2(p,rho3,rho23) = rho3 tensor q.
Q.addDelta(p,r,rho123,[rho12,],1) 
Q.addDelta(p,r,rho3,[rho3,rho2],1) 
Q.addDelta(q,q,rho23,[rho23,],1) 
Q.addDelta(q,r,rho23,[rho2,],1) 
Q.addDelta(r,p,rho2,[],1) #This is delta^1_1(r) = rho2 tensor p.
Q.addDelta(r,q,iota1.toAlgElt(A),[rho3],1) #This is delta^1_2(r,rho3) = iota1 tensor p.

The main use of type DA bimodules is to tensor with type D structures, via Q.tensorD(). For example, here is the homology of the lens space L(3,1):

R = Q.tensorD(P)
S = Q.tensorD(R)
T = Q.tensorD(S)
C = P.morToD(T)
C.simplify()
len(C)

The package does not really know how to tensor two type DA bimodules together. Instead, it has a class, ComposedDAStructure, which does that formally: it just remembers a list of type DA bimodules, but can be tensored with a type D structure in a single operation. So, the computation above is equivalent to:

from dastructure import ComposedDAStructure
U = ComposedDAStructure([Q,Q,Q])
T = U.tensorD(P)
C = P.morToD(T)
C.simplify()
len(C)

The package also has partly implemented the complex of (bimodule) morphisms between two type DA bimodules; in the case of the morphism complex from the identity type DA bimodule to itself, this complex is the one computing Hochschild cohomology. The relevant class is MorDAtoDAComplex. The code does not actually produce a list of generators for this complex (which is typically infinite), but does know how to compute the differential of a given element. The reason this was implemented is in order to compute mapping cones (with MorDAtoDAComplex.getMappingCone), which are used to describe the Ozsváth-Szabó spectral sequence. Similarly, there is code for morphism complexes of type D and DD bimodules, and for local type DA bimodules.

Local bimodules

Classes like LocalDAStructure (in extendbyid.py), getLocalDAStructure (in dehntwistda.py), and so on, refer to Zhan's notion of local DA bimodules. This is a way of encoding only the part of the type DA structure that is different from the identity type DA bimodule, which leads to much faster computations in genus bigger than 3 (and sometimes in genus 3). The mathematics is described a little more in the Mathematics tab. Functionally, if you are using the code, you can usually just treat these local DA bimodules as you would type DA bimodules, and computations will generally work faster using them. (In parts, like braid.py, the code decides for itself which way it thinks things will be faster.)

Braids and spectral sequences

There is convenience code for computing HF-hat of branched double covers of links, and the Ozsváth-Szabó spectral sequence, in braid.py. There are several examples about how to use this on the main page.

One interesting further point is about the spectral sequence: spectral sequences are encoded via cancelation constraints, which tell a chain complex or type DA structure which terms in the differential may not be canceled. So, for example, given a SimpleChainComplex C you can take its total homology by calling C.simplify(). Suppose you had a filtration F on C, and you wanted the E2-page of the associated spectral sequence. Then, you want to ignore terms in the differential that change the filtration by 2 or more or, equivalently, only cancel generators that that are related by a differential changing the filtration by 0 or 1. So, you would write a little function filling in the following pseudo-code:

def my_cancelation_constraint(x,y):
    if abs(filtration(y) - filtration(x))>1:
        return False #Don't cancel these terms.
    else:
        return True #It's okay to cancel these terms.

Then, if you call C.simplify(my_cancelation_constraint), it will ignore all terms in the differential where the filtration changes by 2 or more. The docstring for simplifyComplex in algebra.py explains this in more detail.

To see this in action, try BridgePresentation.getSpecSeq in braids.py (or, perhaps even better, read the code for that function).

Heegaard diagrams and gradings

Heegaard diagrams are used to compute the relative gradings between generators, and are recorded inside bordered modules and bimodules with the registerHDiagram functions. The Heegaard diagram class itself is in hdiagrams.py. I haven't sorted out the syntax for creating Heegaard diagrams so far (because I haven't needed it), but if you do need it, send me an e-mail.

The algebra of the noncommutative gradings is handled by grading.py. The grading groups themselves are BigGradingGroup and SmallGradingGroup, while elements of them are encoded with the class BigGradingElement and SmallGradingElement. Each element remembers which group it's in, in its parent variable. You can multiply elements g and h by just typing g*h (which actually uses the parent.multiply() function) and get the inverse by g.inverse().

Grading sets are encoded by a bunch of classes. For a set with just a single action (as associated to a type D structure), the relevant classes are SimpleGradingSet (the set itself) and SimpleGradingSetElement (an element of the grading set). Again, an element g of the grading group acs on an element s of the grading set by the group acts by s*g. Here, “simple” means the grading set has the form G/H for H a subgroup of G; H is specified by a collection of generators, its periodic_domains (which are, in practice, the gradings of the periodic domains in a Heegaard diagram). A grading set with two commuting actions is encoded by SimpleDbGradingSet, and its elements by SimpleDbGradingSetElement. Again, “simple” means that the set is a quotient of G1×G2.

Mixing grading sets, i.e., S1×GS2 is handled by the class GeneralGradingSet, and its elements GeneralGradingSetElement.

The main thing one usually wants to do with grading elements is compute the difference between two of them. You do that with eltDiffShortForm in GeneralGradingSet. See Further Tutorials for some examples.

There's some code that refers to absolute gradings, but as far as I know that code is vestigial, not functioning. (I don't think the mathematics of absolute gradings in bordered Floer homology has been worked out yet.)