The regular barcode of persistent relative cohomology

In this notebook we illustrate the use of steenroder for the computation of the regular barcode of persistent relative cohomology with mod 2 coefficients. The filtered simplicial complex \(X\) that we consider is a model for the circle

212bc8edd59d4e9cbaa78ceafadfe915

given by the following tuple of tuple of int:

[1]:
circle = (
    (0,),
    (1,), (0,1),
    (2,), (1,2), (0,2)
    )

filtration = circle
maxdim = max([len(spx)-1 for spx in filtration])
m = len(filtration)-1

Relative cohomology

We will consider the persistent relative cohomology of \(X\)

\[H^\bullet(X) \leftarrow H^\bullet(X, X_{0}) \leftarrow H^\bullet(X, X_{1}) \leftarrow \cdots \leftarrow H^\bullet(X, X_{m})\]

as a persistence module.

Its regular barcode

For \(i < j \in \{-1,\dots,m\}\) let \(X_{ij}\) be the unique composition \(H^\bullet(X, X_{i}) \leftarrow H^\bullet(X, X_{j})\) with the convention \(X_{-1} = \emptyset\). The barcode of this persistence module is the multiset

\[Bar_X = \big\{ [p,q] \mid -1 \le p < q \le m \big\}\]

defined by the property

\[\mathrm{rank} \, X_{ij} = \mathrm{card} \big\{[p,q] \in Bar_X \mid p \le i < j \le q \big\}.\]
[2]:
from steenroder import barcodes

barcode, steenrod_barcode = barcodes(0, filtration)

print(f'Barcode of persistent relative cohomology:')
for d in range(maxdim + 1):
    print(f'dim {d}: \n{barcode[d]}')
Barcode of persistent relative cohomology:
dim 0:
[[-1  0]]
dim 1:
[[-1  5]
 [ 3  4]
 [ 1  2]]

How this is computed

Boundary matrix

Order the simplices \(a_0 < a_1 < \dots < a_m\) of \(X\) so that \(X_k = \{a_i \mid i \leq k\}\). Consider the matrix \(D\) representing the boundary map \(\partial \colon C_\bullet(X;\mathbb{F}_2) \to C_{\bullet-1}(X;\mathbb{F}_2)\) with respect to the (ordered) basis determined by the (ordered) simplices. Explicitly,

\[\partial a_j = \sum_{i=0}^m D_{ij} \, a_i.\]

Notice that the submatrix \(D_{\leq j \leq j}\) represents the boundary of \(C_\bullet(X_j;\mathbb F_2)\) for every \(k \in \{0,\dots,m\}\).

Anti-transpose

We will consider the antitranspose \(D^\perp\) of the boundary matrix \(D\). It is defined for every \(p,q \in \{0, \dots, m\}\) by

\[D^\perp_{p,\, q} = D_{\overline q,\ \overline p}\]

where \(\overline j = m-j\) for \(j \in \{0, \dots, m\}\)

For any \(j \in \{0,\dots,m\}\) the submatrix \(D^\perp_{\leq j, \leq j}\) represents the coboundary map \(\delta \colon C^\bullet(X, X_{\overline j-1}; \mathbb{F}_2) \to C^{\bullet+1}(X, X_{\overline j-1}; \mathbb{F}_2)\) with respect to the (ordered) dual basis \(a_m^\bullet < \dots < a_{m-j}^\bullet\).

The \(R = D^\perp V\) decomposition

Here \(V\) is an invertible upper triangular matrix and \(R\) is reduced, i.e., no two columns have the same pivot row.

Denoting the \(i\)-th column of \(R\) by \(R_{i}\) where \(i \in \{0,\dots,m\}\), let

\[P = \{ i \ |\ R_i = 0\}, \qquad N = \{ i \ |\ R_i \neq 0\}, \qquad E = P \setminus \{\text{pivots of } R\}.\]

The regular barcode

There exists a canonical bijection between the union of \(N\) and \(E\) and the persistence relative cohomology barcode of the filtration given by

\begin{align*} N \ni j &\mapsto \Big[\, \overline j, \overline{\mathrm{pivot}\,R_j}\, \Big] \in Bar^{\dim(a_j)+1}_X \\ E \ni j &\mapsto \big[\! -1, \overline j \, \big] \in Bar^{\dim(a_j)}_X \end{align*}

Representatives

Additionally, a persistent cocycle representative is given by \begin{equation*} [i,j] \mapsto \begin{cases} V_{\overline j}, & i = -1, \\ R_{\overline i}, & i \neq -1. \end{cases} \end{equation*}

Using steenroder

Splitting by dimension

In steenroder all computations are done dimension by dimension. We start by splitting the filtration into collection of simplices of the same dimension.

[3]:
from steenroder import sort_filtration_by_dim

filtration_by_dim = sort_filtration_by_dim(filtration)

The output of sort_filtration_by_dim is as follows:

For each dimension d, a list of 2 aligned int arrays: the first is a 1D array containing the (ordered) positional indices of all d-dimensional simplices in filtration; the second is a 2D array whose i-th row is the (sorted) collection of vertices defining the i-th d-dimensional simplex.

[4]:
print(f'Simplices:')
for d in range(maxdim + 1):
    positions, simplices = filtration_by_dim[d]
    print(f'dim {d}:\n{simplices}\nin positions {positions}.')
Simplices:
dim 0:
[[0]
 [1]
 [2]]
in positions [0 1 3].
dim 1:
[[0 1]
 [1 2]
 [0 2]]
in positions [2 4 5].

A note on anti-transposition

The anti-transposition operation \((-)^\perp\) is determined by composing the usual trnasposition

\[(-)^\mathrm{T} \colon (i, j) \mapsto (j, i),\]

and the horizontal and vertical flips

\[(-)^\mathrm{A} \colon (i,j) \mapsto (\overline i, \overline j).\]

We remark that

\[(M N)^\mathrm{A} = M^\mathrm{A} N^\mathrm{A}.\]

for any pair of square matrices \(M\) and \(N\).

The \(R = D^\perp V\) decomposition

To compute the matrices \(R\) and \(V\) in the decomposition \(R = D^\perp V\) steenroder uses the method compute_reduced_triangular. It actually produces \(R^\mathrm{A}\) and \(V^\mathrm{A}\). More precisely, the output reduced is a tuple of numba.typed.List with reduced[d] the d-dimensional part of \(R^\mathrm{A}\). More explicitly, for an integer \(j\) we have that an integer \(i\) is in the tuple reduced[d][j] if and only if \(R^\mathrm{A}_{ij} = 1\). Similarly triangular[d] is the d-dimensional part of the \(V^\mathrm{A}\). There are also two other auxiliary outputs that will be discussed later, these are spx2idx and idxs.

[5]:
from steenroder import compute_reduced_triangular

spx2idx, idxs, reduced, triangular = compute_reduced_triangular(filtration_by_dim)

We will verify for our running example that \(R = D^\perp V\) by checking that the coboundary \(D^\mathrm{T}\) of this filtration is equal to \(R^\mathrm{A} U^\mathrm{A}\) where \(U\) is the inverse of \(V\).

Let us start by contructing the coboundary matrix \(D^\mathrm{T}\).

[6]:
import numpy as np

coboundary = np.zeros((m+1,m+1), dtype=int)
for j, x in enumerate(filtration):
    for i, y in enumerate(filtration):
        if len(x)+1 == len(y) and set(x).issubset(set(y)):
            coboundary[i,j] = 1
print(f'D^T =\n{coboundary}')
D^T =
[[0 0 0 0 0 0]
 [0 0 0 0 0 0]
 [1 1 0 0 0 0]
 [0 0 0 0 0 0]
 [0 1 0 1 0 0]
 [1 0 0 1 0 0]]

We now represent \(R^{\mathrm{A}}\) and \(V^{\mathrm{A}}\) as instances of np.array, compute \(U^{\mathrm{A}}\) and verify that \(U^{\mathrm{A}} R^{\mathrm{A}}\) is equal to \(D^{\mathrm{T}}\) as claimed.

[7]:
def to_array(matrix, e=0):
    """Transforms reduced (e=1) and triangular (e=0) to numpy arrays"""
    array = np.zeros((m+1,m+1), dtype=int)
    for d in range(maxdim+1):
        for i, col in enumerate(matrix[d]):
            for j in col:
                array[idxs[d+e][j], idxs[d][i]] = 1
    return array

antiR, antiV = to_array(reduced, e=1), to_array(triangular, e=0)
antiU = np.array(np.linalg.inv(antiV), dtype=int)
product = np.matmul(antiR, antiU) % 2
print(f'D^T = R^AU^A: {(coboundary == product).all()}')
D^T = R^AU^A: True

Regular barcode and persistent cocycle representatives

[8]:
from steenroder import compute_barcode_and_coho_reps

barcode, coho_reps = compute_barcode_and_coho_reps(idxs, reduced, triangular)

For every d we have that barcode[d] is a 2D int array of shape (n_bars, 2) containing the birth (entry 1) and death (entry 0) indices of persistent relative cohomology classes in degree d.

[9]:
for d in range(maxdim+1):
    print(f'The barcode in dim {d}:\n{barcode[d]}')
The barcode in dim 0:
[[-1  0]]
The barcode in dim 1:
[[-1  5]
 [ 3  4]
 [ 1  2]]

We will compare this barcode with its description as

\begin{align*} N \ni j &\mapsto \Big[\, \overline j, \overline{\mathrm{pivot}\,R_j} \, \Big] \in Bar_{{\dim(j)+1}}^{\mathrm{fin}} \\ E \ni j &\mapsto \big[\! -1, \overline j \, \big] \in Bar_{\dim(j)}^{\mathrm{inf}} \end{align*} where

\[P = \{ i \ |\ R_i = 0\}, \qquad N = \{ i \ |\ R_i \neq 0\}, \qquad E = P \setminus \{\text{pivots of } R\}.\]

Let us start by obtaining \(R\) from \(R^{\mathrm{A}}\).

[10]:
R = np.flip(antiR, [0,1])
print(f'R =\n{R}')
R =
[[0 0 1 0 0 0]
 [0 0 1 0 1 0]
 [0 0 0 0 0 0]
 [0 0 0 0 1 0]
 [0 0 0 0 0 0]
 [0 0 0 0 0 0]]

We can not construct the sets \(P\), \(N\) and \(E\).

[11]:
def pivot(column):
    try:
        return max(column.nonzero()[0])
    except ValueError:
        pass

P, N, E = [], [], []
for i, col in enumerate(R.T):
    if pivot(col):
        N.append(i)
        E.remove(pivot(col))
    else:
        P.append(i)
        E.append(i)

print(f'P = {P}\nN = {N}\nE = {E}')
P = [0, 1, 3, 5]
N = [2, 4]
E = [0, 5]

Finally we deduce the barcode from these.

[12]:
bcode = [[] for d in range(maxdim+1)]

for j in N:
    d = len(filtration[m-j])
    bcode[d].append([m-j, m-pivot(R[:,j])])

for j in E:
    d = len(filtration[m-j])-1
    bcode[d].append([-1, m-j])

for d in range(maxdim+1):
    print(f'The barcode in dim {d}:\n{bcode[d]}')
The barcode in dim 0:
[[-1, 0]]
The barcode in dim 1:
[[3, 4], [1, 2], [-1, 5]]

Let us now consider the other output coho_reps[d][k]. It is a list of positional indices, relative to the d-dimensional portion of the filtration, representing the bar barcode[d][k] for every k.

To express these representatives in terms of cochains, we use that idxs[d] is an int array containing the (ordered) positional indices of all d-dimensional simplices in the filtration.

[13]:
for d in range(maxdim+1):
    for bar, rel_positions in zip(barcode[d], coho_reps[d]):
        coho_rep = []
        for p in rel_positions:
            coho_rep.append(filtration[idxs[d][p]])
        print(f"{str(bar): <7} rep. by {coho_rep}")
[-1  0] rep. by [(0,), (1,), (2,)]
[-1  5] rep. by [(0, 2)]
[3 4]   rep. by [(1, 2), (0, 2)]
[1 2]   rep. by [(0, 1), (1, 2)]

We will now compare these representatives to their description as \begin{equation*} [i,j] \mapsto \begin{cases} V_{\bar j}, & i = -1, \\ R_{\bar i}, & i \neq -1. \end{cases} \end{equation*}

[14]:
V = np.flip(antiV, [0,1])

for d in range(maxdim+1):
    for bar in barcode[d]:
        i,j = bar
        c_rep = []
        if i == -1:
            nonzeros = np.nonzero(V[:,m-j])[0]
        else:
            nonzeros = np.nonzero(R[:,m-i])[0]
        for p in nonzeros:
            c_rep.append(filtration[m-p])
        print(f'{str(bar): <7} rep. by {c_rep}')
[-1  0] rep. by [(2,), (1,), (0,)]
[-1  5] rep. by [(0, 2)]
[3 4]   rep. by [(0, 2), (1, 2)]
[1 2]   rep. by [(1, 2), (0, 1)]

To-Do

Explain duality between persistent relative cohomology and persistent homology.