-
Notifications
You must be signed in to change notification settings - Fork 0
Algebra of Persistence Modules
The Barcode Algebra component of the library implements a number of algebraic operations on persistence modules. This page is an attempt to explain what the operations are and how they are implemented. This text will also be reused in a paper MVJ is currently working on.
Suppose
The various barcode decomposition theorems (Carlsson-Zomorodian, Crawley-Boevey, and numerous other versions) all basically prove that under sufficiently nice conditions, we can make simultaneous basis changes in all degrees so that these presentation maps are indeed diagonal. While working over a field, we can thus WLOG assume that each presentation can be given as a projection matrix (0-1 matrix with at most one 1 per row and exactly one 1 per column; any relations that don't have any influence on the generators can safely be dropped). However, it may often be useful to track the column reduced echelon form even if it is "merely" triangular and not fully a projection matrix. We shall throughout call such a matrix reduced, and will get ample use of the operation of reducing a matrix. In a persistence setting, we can replace later occurring basis elements with linear combinations with older basis elements -- but we can not replace earlier basis elements using elements that don't yet exist. This requirement gives a direction for any column reduction moves.
In the presence of an ambient module in which both generators and relations embed, there may well be a reason to track all these elements as they are represented in the ambient module. (for an example, think of the chains in a homology computation) These can all be understood as separate maps of free persistence modules and thus represented by separate matrices.
For the following exposition, we shall assume we have persistence modules
-
$G_X = \{x_1,\dots,x_{n_X}\}$ , -
$R_X = \{\xi_1,\dots,\xi_{m_X}\}$ , -
$G_Y = \{y_1,\dots,y_{n_Y}\}$ , $R_Y = \{\eta_1,\dots,\eta_{m_Y}\}$
and maps
$i_X: R_X\to G_X$ $i_Y: R_Y\to G_Y$ -
$f: X\to Y$ , which in turn is represented by $\phi: G_X\to G_Y$
For each map we can assume we have access to a matrix that expresses that linear map in the basis sets we have started with.
We sometimes choose in the TDA4j library to represent formal linear sums indexed by a type VectorT
and taking coefficients in a type CoefficientT
.
In this setting, we may think of such formal linear sums as being of type Map[VectorT,CoefficientT]
-- with some extra handling to implement arithmetic on these associative maps.
Then, the matrices may be (sparsely) represented by lists or sequences of these maps -- or if we have keys of some type enumerating the columns as maps.
So a matrix may show up either as a List[Map[VectorT,CoefficientT]]
or as a Map[VectorS, Map[VectorT,CoefficientT]]]
.
We will try to give scala commentary in each section below as well.
For a map
-
$|x_i| \geq |y_j|$ whenever$\phi_{i,j} \neq 0$ . (anything that the bar$x_i$ hits has to already exist when$x_i$ comes into existence) -
$|\xi_i| \geq |\eta_j|$ whenever$\phi_{i,j} \neq 0$ (assuming the presentation maps are essentially diagonal projection maps as described above -- what we really need is the condition that whenever the images of the$\xi_i$ and$\eta_j$ have a non-zero coefficient in any compositions of maps that sends them into$FG_Y$ ) -- (for as long as$y_j$ is alive,$x_i$ also has to be alive)
The simplest of algebraic operations to track and implement is the direct sum of two modules. Observe that for free modules, the direct sum is achieved by taking the disjoint union of the basis sets.
Hence, the direct sum of a pair of persistence modules
$G_{X\oplus Y} = G_X\sqcup G_Y = \{x_1,\dots,x_{n_X},y_1,\dots,y_{n_Y}\}$ $R_{X\oplus Y} = R_X\sqcup R_Y = \{\xi_1,\dots,\xi_{m_X},\eta_1,\dots,\eta_{m_Y}\}$ $i_{X\oplus Y} = \begin{psmallmatrix}i_X & 0 \\\ 0 & i_Y\end{psmallmatrix}$
If we are sure that there is no overlap between the keys addressing any of the basis sets the direct sum can be done by simply taking a union (or concatenating) the corresponding lists and maps. Otherwise, we may want to do something to ensure disjointness of all keys. This would mean something like:
def directSum(iX: Map[BasisX, Map[VectorX, CoefficientT]], iY: Map[BasisY, Map[VectorY, CoefficientT]]):
Map[Either[BasisX, BasisY], Map[Either[VectorX,VectorY], CoefficientT]]
Canonical functions Right
or Left
) any map keys to map into the direct sum -- or by ignoring any map keys of one of the sides to map out of the direct sum.
The real work-horse in this part of the library is the cokernel -- especially since by dualizing we can express kernels as cokernels, so by preparing the input we can reuse any code written for cokernels to achieve the kernel computations as well.
The cokernel of a function
Generators of
However, when
Translating into operations on free modules, we get that the cokernel has generators
Now that we have established
To get this map, we need to send each basis element in
For an example, consider the persistence modules
We have the generators in degrees
The cokernel has generators
If we track our operations here, we have a new basis for the relations given by
So for the cokernel, we retain the generating set as is, and have a 2-element relations set in the same degrees as the first two elements of the initial relations set: our cokernel relations are in degrees
Now, having cleared everything out in the oldest coefficients at each step, we pair each relationship with the youngest generator in its support that is not already paired with a younger relation.
This yields the pairings
To understand the map from
There are two approaches to computing the kernel of a map. Either we can first figure out how to compute kernels of maps between free modules, and then set up the computation of the relations for the kernel module as a second such kernel computation. This is the approach in [Skraba - Vejdemo-Johansson] on arXiv.
Alternatively, the kernel dualizes to the cokernel. So we can move to the dual, compute a cokernel there, and translate everything back again.
Moving to the dual means:
- Either replacing each barcode endpoint
$t$ by$-t$ , or replacing the total order of the filtration with its opposite order. Note that in Scala, the opposite order can be quite efficiently accessed by using the way we are using the implicit objects programming techniques to handle orderings to begin with. We make both thekernel
andcokernel
methods expect an implicit ordering by adding(using ord: Ordering[FiltrationT])
to their call signatures. (this is done implicitly if you declare theFiltrationT
type variable asFiltrationT : Ordering
) Then we can callcokernel
fromkernel
and give an explicit ordering. - Replacing the matrix representing
$\phi$ with its transpose.
Hence, our Scala code for the kernel reduces down to:
def kernel(
source: List[PersistenceBar[FiltrationT, AnnotationT]],
target: List[PersistenceBar[FiltrationT, AnnotationT]],
matrix: RealMatrix
)(using
ord: Ordering[FiltrationT]
): List[PersistenceBar[FiltrationT, AnnotationT]] = {
val dualSource: List[PersistenceBar[FiltrationT, AnnotationT]] =
target.map(pb => PersistenceBar(pb.dim, pb.upper, pb.lower))
val dualTarget: List[PersistenceBar[FiltrationT, AnnotationT]] =
source.map(pb => PersistenceBar(pb.dim, pb.upper, pb.lower))
val cokernelIntervals =
cokernel(dualSource, dualTarget, matrix.transpose())(using
ord = ord.reverse
)
cokernelIntervals.map(pb => PersistenceBar(pb.dim, pb.upper, pb.lower))
}
Notice that most of the work done here is in flipping the positions of lower and upper bounds on the persistence bar data type.
Since the transpose of an inverse is the inverse of the transpose, we can take the transpose of the inverse reduced presentation map to recover the injection map from the kernel into its ambient module.