Skip to content
The Lumenary

Math laboratory

Mapping cones over F_p: exactness and non-unique fill-ins

Builds random cochain complexes and chain maps over F_101, forms mapping cones, and checks d^2 = 0, the dimension formula equivalent to the long exact cohomology sequence, and 'f is a quasi-isomorphism iff its cone is acyclic', exactly, on 200 random examples (a quarter of them identity maps). Then counts the p fill-ins completing an identity square of triangles in D(F_p), by brute force.

Back to the labExact computationExperimental evidenceExhaustive finite check
Mapping cones over F_p: exactness and non-unique fill-insPythonExact computationExperimental evidenceExhaustive finite checkComputationally tested
"""Mapping cones over a finite field: the long exact sequence, checked exactly.

In the homotopy category K(k) every chain map f : X -> Y sits in a
distinguished triangle X -> Y -> C(f) -> X[1], where the mapping cone has
C(f)^n = X^(n+1) + Y^n and d(x, y) = (-dx, f(x) + dy). Applying cohomology
gives a long exact sequence, which is equivalent to the dimension formula

    dim H^n(C) = (dim H^n(Y) - rank H^n(f)) + (dim H^(n+1)(X) - rank H^(n+1)(f)).

We build random complexes and random chain maps over F_101 (exact modular
arithmetic), check that d_C o d_C = 0, check the formula in every degree, and
check that f is a quasi-isomorphism exactly when C(f) is acyclic. Then we count
by brute force the morphisms of triangles that complete the identity square
for the triangle X --0--> Y -> Y + X[1] -> X[1] in D(F_p): there are p of them,
so the third map in TR3 is not unique. Exact computations on random examples
and a finite count: evidence for the theorems proved in the lesson.
"""

import random
from itertools import product

P = 101

def rank(rows, p=P):
    """Rank of a matrix (list of rows) over F_p by Gaussian elimination."""
    m = [[v % p for v in r] for r in rows if r]
    if not m:
        return 0
    r, cols = 0, len(m[0])
    for c in range(cols):
        piv = next((i for i in range(r, len(m)) if m[i][c]), None)
        if piv is None:
            continue
        m[r], m[piv] = m[piv], m[r]
        inv = pow(m[r][c], p - 2, p)
        m[r] = [v * inv % p for v in m[r]]
        for i in range(len(m)):
            if i != r and m[i][c]:
                t = m[i][c]
                m[i] = [(a - t * b) % p for a, b in zip(m[i], m[r])]
        r += 1
    return r

def nullspace(rows, ncols, p=P):
    """A basis of {v : rows v = 0} over F_p."""
    m = [[v % p for v in r] for r in rows]
    pivots, r = [], 0
    for c in range(ncols):
        piv = next((i for i in range(r, len(m)) if m[i][c]), None)
        if piv is None:
            continue
        m[r], m[piv] = m[piv], m[r]
        inv = pow(m[r][c], p - 2, p)
        m[r] = [v * inv % p for v in m[r]]
        for i in range(len(m)):
            if i != r and m[i][c]:
                t = m[i][c]
                m[i] = [(a - t * b) % p for a, b in zip(m[i], m[r])]
        pivots.append(c)
        r += 1
    basis = []
    for free in (c for c in range(ncols) if c not in pivots):
        v = [0] * ncols
        v[free] = 1
        for i, c in enumerate(pivots):
            v[c] = -m[i][free] % p
        basis.append(v)
    return basis

def matmul(a, b, p=P):
    """Product of matrices given as lists of rows (handles empty shapes)."""
    if not a or not b:
        return [[0] * (len(b[0]) if b else 0) for _ in a]
    return [[sum(x * y for x, y in zip(row, col)) % p for col in zip(*b)]
            for row in a]

def zeros(r, c):
    """The r x c zero matrix."""
    return [[0] * c for _ in range(r)]

def random_complex(rng, length=4, max_dim=3):
    """A random complex: dims[n] and differentials d[n] : X^n -> X^(n+1).

    Built as a sum of pieces k (in one degree) and k --1--> k, then hidden by
    random changes of basis, so that d o d = 0 holds by construction.
    """
    pieces = []
    for n in range(length):
        pieces += [("H", n)] * rng.randint(0, max_dim - 1)
        if n + 1 < length:
            pieces += [("A", n)] * rng.randint(0, 1)
    dims = [0] * length
    where = []
    for kind, n in pieces:
        where.append((kind, n, dims[n], dims[n + 1] if kind == "A" else None))
        dims[n] += 1
        if kind == "A":
            dims[n + 1] += 1
    d = [zeros(dims[n + 1], dims[n]) for n in range(length - 1)]
    for kind, n, i, j in where:
        if kind == "A":
            d[n][j][i] = 1
    g = [random_invertible(rng, k) for k in dims]
    ginv = [inverse(m) for m in g]
    return dims, [matmul(matmul(g[n + 1], d[n]), ginv[n])
                  for n in range(length - 1)]

def random_invertible(rng, k):
    """A uniformly random invertible k x k matrix over F_p."""
    while True:
        m = [[rng.randrange(P) for _ in range(k)] for _ in range(k)]
        if rank(m) == k:
            return m

def inverse(m):
    """Inverse of an invertible matrix over F_p (Gauss-Jordan)."""
    k = len(m)
    aug = [row[:] + [int(i == j) for j in range(k)] for i, row in enumerate(m)]
    for c in range(k):
        piv = next(i for i in range(c, k) if aug[i][c] % P)
        aug[c], aug[piv] = aug[piv], aug[c]
        inv = pow(aug[c][c], P - 2, P)
        aug[c] = [v * inv % P for v in aug[c]]
        for i in range(k):
            if i != c and aug[i][c]:
                t = aug[i][c]
                aug[i] = [(a - t * b) % P for a, b in zip(aug[i], aug[c])]
    return [row[k:] for row in aug]

def random_chain_map(rng, x, y):
    """A random solution f of d_Y f^n = f^(n+1) d_X (all degrees)."""
    (dx, ddx), (dy, ddy) = x, y
    length = len(dx)
    offs, tot = [], 0
    for n in range(length):
        offs.append(tot)
        tot += dy[n] * dx[n]
    eqs = []
    for n in range(length - 1):
        for i in range(dy[n + 1]):
            for j in range(dx[n]):
                row = [0] * tot
                for k in range(dy[n]):  # (d_Y f^n)[i][j]
                    row[offs[n] + k * dx[n] + j] += ddy[n][i][k]
                for k in range(dx[n + 1]):  # (f^(n+1) d_X)[i][j]
                    row[offs[n + 1] + i * dx[n + 1] + k] -= ddx[n][k][j]
                eqs.append(row)
    basis = nullspace(eqs, tot) if eqs else [
        [int(i == j) for j in range(tot)] for i in range(tot)]
    v = [0] * tot
    for b in basis:
        c = rng.randrange(P)
        v = [(a + c * bb) % P for a, bb in zip(v, b)]
    return [[[v[offs[n] + i * dx[n] + j] for j in range(dx[n])]
             for i in range(dy[n])] for n in range(length)]

def cone(x, y, f):
    """Mapping cone, with C^n = X^(n+1) + Y^n, as (dims, differentials).

    Degrees run from -1 (where C^-1 = X^0) to length - 1.
    """
    (dx, ddx), (dy, ddy) = x, y
    length = len(dx)
    xd = lambda n: dx[n] if 0 <= n < length else 0
    yd = lambda n: dy[n] if 0 <= n < length else 0
    dims = [xd(n + 1) + yd(n) for n in range(-1, length)]
    diffs = []
    for n in range(-1, length - 1):
        m = zeros(xd(n + 2) + yd(n + 1), xd(n + 1) + yd(n))
        for i in range(xd(n + 2)):
            for j in range(xd(n + 1)):
                m[i][j] = -ddx[n + 1][i][j] % P
        for i in range(yd(n + 1)):
            for j in range(xd(n + 1)):
                m[xd(n + 2) + i][j] = f[n + 1][i][j]
            for j in range(yd(n)):
                m[xd(n + 2) + i][xd(n + 1) + j] = ddy[n][i][j]
        diffs.append(m)
    return dims, diffs

def cohomology(c):
    """dim H^n for a complex (dims, diffs) indexed from its first term."""
    dims, d = c
    out = []
    for n in range(len(dims)):
        rk_out = rank(d[n]) if n < len(d) else 0
        rk_in = rank(d[n - 1]) if n > 0 else 0
        out.append(dims[n] - rk_out - rk_in)
    return out

def cycles(c, n):
    """A basis of Z^n = ker d^n (as vectors)."""
    dims, d = c
    if n >= len(d):
        return [[int(i == j) for j in range(dims[n])] for i in range(dims[n])]
    return nullspace(d[n], dims[n])

def rank_on_cohomology(x, y, f, n):
    """rank of H^n(f) = dim(f(Z^n X) + B^n Y) - dim B^n Y."""
    image = [[sum(f[n][i][j] * z[j] for j in range(len(z))) % P
              for i in range(y[0][n])] for z in cycles(x, n)]
    bound = [list(col) for col in zip(*y[1][n - 1])] if n > 0 else []
    return rank(image + bound) - rank(bound)

def fill_in_count(p):
    """Morphisms c : Z -> Z completing (id, id) on X --0--> Y -> Z -> X[1].

    Model in D(F_p) = graded vector spaces with X = F_p, Y = F_p[1], so that
    Z = Y + X[1] has both summands in degree -1, g = inclusion of Y and
    h = projection to X[1]; conditions: c g = g and h c = h.
    """
    g = [[1], [0]]
    h = [[0, 1]]
    count = 0
    for a, b, c, d in product(range(p), repeat=4):
        m = [[a, b], [c, d]]
        if matmul(m, g, p) == g and matmul(h, m, p) == h:
            count += 1
    return count

def identity_map(x):
    """The identity chain map of a complex x."""
    return [[[int(i == j) for j in range(k)] for i in range(k)] for k in x[0]]

def main():
    rng = random.Random(2024)
    trials, d2_ok, les_ok, qis_ok, quasi = 200, 0, 0, 0, 0
    for t in range(trials):
        x = random_complex(rng)
        if t % 4 == 0:  # every fourth example: the identity, a quasi-iso
            y, f = x, identity_map(x)
        else:
            y = random_complex(rng)
            f = random_chain_map(rng, x, y)
        c = cone(x, y, f)
        d2_ok += all(not any(map(any, matmul(c[1][n + 1], c[1][n])))
                     for n in range(len(c[1]) - 1))
        hx, hy, hc = cohomology(x), cohomology(y), cohomology(c)
        length = len(hx)
        rk = [rank_on_cohomology(x, y, f, n) for n in range(length)] + [0]
        hx_ = hx + [0]
        les_ok += all(hc[n + 1] == hy[n] - rk[n] + hx_[n + 1] - rk[n + 1]
                      for n in range(length))
        is_qis = hx == hy and all(rk[n] == hx[n] for n in range(length))
        quasi += is_qis
        qis_ok += is_qis == (sum(hc) == 0)
    print(f"Chain maps of random complexes over F_{P}, degrees 0..3: {trials}")
    print(f"  cones with d o d = 0:                     {d2_ok}/{trials}")
    print(f"  dimension formula (long exact sequence):  {les_ok}/{trials}")
    print(f"  'quasi-iso <=> acyclic cone' consistent:  {qis_ok}/{trials}"
          f"  ({quasi} quasi-isos)")
    print()
    print("Fill-ins c completing (id, id) for X --0--> Y -> Y + X[1] -> X[1]")
    print("in D(F_p), X = F_p, Y = F_p[1] (c = [[1, t], [0, 1]], t free):")
    print("  " + ", ".join(f"p = {p}: {fill_in_count(p)}" for p in (2, 3, 5)))
    print()
    print("Epistemic status: exact arithmetic mod 101 on 200 pseudo-random")
    print("examples (seeded), plus an exhaustive count over 2 x 2 matrices.")
    print("This is evidence that the cone construction behaves as the lesson")
    print("proves (exactness, cones detect quasi-isomorphisms, non-unique")
    print("fill-ins); random examples over one field do not prove it.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in