Skip to content
The Lumenary

Math laboratory

Fox colourings and knot determinants

Builds the colouring equations 2x = y + z of six knot diagrams from their PD codes, counts Fox p-colourings for p = 3, 5, 7 by linear algebra mod p (cross-checked by brute force on small cases) and computes each determinant exactly. It shows tricolourability matches 3 | det, and that 4_1 and 5_1 have the same counts although they are different knots. Exact, for these diagrams.

Back to the labExact computationExhaustive finite check
Fox colourings and knot determinantsPythonExact computationExhaustive finite checkComputationally tested
"""Fox colourings and the determinant of a knot.

From a planar diagram (PD) code we find the arcs of the diagram (edges joined
where they pass over a crossing) and the colouring equations: at a crossing
whose over-arc is x and whose under-arcs are y and z, 2x = y + z mod p. The
number of Fox p-colourings is p to the power of the dimension of the solution
space, found by Gaussian elimination mod p. The determinant of the knot is
the absolute value of any (n-1) x (n-1) minor of the colouring matrix,
computed with exact fractions. A knot has a non-constant p-colouring (p an
odd prime) exactly when p divides its determinant. Brute force over all
colourings checks the linear algebra on the smallest cases. Evidence: exact
computation for these diagrams.
"""

from fractions import Fraction
from itertools import product

KNOTS = {
    "unknot (1 kink)": [(1, 2, 2, 1)],
    "3_1": [(1, 5, 2, 4), (3, 1, 4, 6), (5, 3, 6, 2)],
    "4_1": [(4, 2, 5, 1), (8, 6, 1, 5), (6, 3, 7, 4), (2, 7, 3, 8)],
    "5_1": [(1, 6, 2, 7), (3, 8, 4, 9), (5, 10, 6, 1), (7, 2, 8, 3),
            (9, 4, 10, 5)],
    "5_2": [(1, 4, 2, 5), (3, 8, 4, 9), (5, 10, 6, 1), (9, 6, 10, 7),
            (7, 2, 8, 3)],
    "6_1": [(1, 4, 2, 5), (7, 10, 8, 11), (3, 9, 4, 8), (9, 3, 10, 2),
            (5, 12, 6, 1), (11, 6, 12, 7)],
}

def arcs(pd):
    """Map each edge label to its arc number (edges j, l continue over)."""
    parent = {}

    def find(x):
        while parent.setdefault(x, x) != x:
            x = parent[x]
        return x

    for (i, j, k, l) in pd:
        for e in (i, k):
            find(e)
        parent[find(j)] = find(l)
    roots = sorted({find(e) for c in pd for e in c})
    return {e: roots.index(find(e)) for c in pd for e in c}

def colouring_matrix(pd):
    """Rows 2x - y - z, one per crossing, columns indexed by arcs."""
    arc = arcs(pd)
    n = len(set(arc.values()))
    rows = []
    for (i, j, k, l) in pd:
        row = [0] * n
        row[arc[j]] += 2
        row[arc[i]] -= 1
        row[arc[k]] -= 1
        rows.append(row)
    return rows

def rank_mod_p(rows, p):
    """Rank of an integer matrix over the field Z/p (p prime)."""
    m = [[x % p for x in row] for row in rows]
    rank, cols = 0, len(m[0]) if m else 0
    for c in range(cols):
        pivot = next((r for r in range(rank, len(m)) if m[r][c]), None)
        if pivot is None:
            continue
        m[rank], m[pivot] = m[pivot], m[rank]
        inv = pow(m[rank][c], p - 2, p)
        m[rank] = [x * inv % p for x in m[rank]]
        for r in range(len(m)):
            if r != rank and m[r][c]:
                f = m[r][c]
                m[r] = [(x - f * y) % p for x, y in zip(m[r], m[rank])]
        rank += 1
    return rank

def count_colourings(pd, p):
    """Number of Fox p-colourings = p^(arcs - rank mod p)."""
    rows = colouring_matrix(pd)
    return p ** (len(rows[0]) - rank_mod_p(rows, p))

def brute_force_count(pd, p):
    """Count Fox p-colourings by trying every assignment (small cases)."""
    rows = colouring_matrix(pd)
    n = len(rows[0])
    return sum(all(sum(a * c for a, c in zip(row, col)) % p == 0
                   for row in rows)
               for col in product(range(p), repeat=n))

def determinant(rows):
    """Exact determinant of a square matrix by Gaussian elimination."""
    m = [[Fraction(x) for x in row] for row in rows]
    det = Fraction(1)
    for c in range(len(m)):
        pivot = next((r for r in range(c, len(m)) if m[r][c] != 0), None)
        if pivot is None:
            return Fraction(0)
        if pivot != c:
            m[c], m[pivot] = m[pivot], m[c]
            det = -det
        det *= m[c][c]
        for r in range(c + 1, len(m)):
            f = m[r][c] / m[c][c]
            m[r] = [x - f * y for x, y in zip(m[r], m[c])]
    return det

def knot_determinant(pd):
    """|det| of the colouring matrix with its last row and column deleted."""
    rows = colouring_matrix(pd)
    if len(rows) == 1:
        return 1
    minor = [row[:-1] for row in rows[:-1]]
    return abs(int(determinant(minor)))

def main():
    print("Fox colourings: 2 * over = under + under (mod p) at each crossing")
    print("  knot            arcs  det  3-col  5-col  7-col  tricolourable")
    for name, pd in KNOTS.items():
        n = len(colouring_matrix(pd)[0])
        det = knot_determinant(pd)
        counts = [count_colourings(pd, p) for p in (3, 5, 7)]
        tri = "yes" if counts[0] > 3 else "no"
        print(f"  {name:<15} {n:4d} {det:4d} {counts[0]:6d} {counts[1]:6d}"
              f" {counts[2]:6d}  {tri}")
    checks = [(name, p) for name in ("3_1", "4_1", "5_1") for p in (3, 5)]
    agree = all(count_colourings(KNOTS[k], p) == brute_force_count(KNOTS[k], p)
                for k, p in checks)
    print(f"  brute force agrees on {len(checks)} small cases: {agree}")
    print()
    print("Constant colourings always work, giving p of them. A non-constant")
    print("p-colouring exists exactly when p divides the determinant. 3_1 is")
    print("tricolourable and the unknot is not, so 3_1 is knotted. 4_1 and 5_1")
    print("share det = 5 and the colouring counts above, yet they are different")
    print("knots: their Jones polynomials differ. Invariants can fail to")
    print("distinguish; they never wrongly separate.")
    print()
    print("Epistemic status: exact computation for these PD codes, with brute")
    print("force confirming the linear algebra on small cases. That colouring")
    print("counts are knot invariants is proved in the lesson (Reidemeister).")

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

Used in