Skip to content
The Lumenary

Math laboratory

Stiefel-Whitney classes of projective spaces

Expands w(T RP^n) = (1 + a)^(n+1) in Z/2[a]/(a^(n+1)) for n = 1..16, lists the nonzero classes, and inverts it to find the dual classes. It recovers that RP^n can be parallelisable only if n + 1 is a power of 2 and that RP^(2^r) does not immerse in R^(2^(r+1) - 2). Exact, given the formula for w(T RP^n) and Whitney duality.

Back to the labExact computationExhaustive finite check
Stiefel-Whitney classes of projective spacesPythonExact computationExhaustive finite checkComputationally tested
"""Stiefel-Whitney classes of real projective spaces, computed exactly mod 2.

The cohomology ring is H*(RP^n; Z/2) = Z/2[a]/(a^(n+1)), and the tangent bundle
has total Stiefel-Whitney class w = (1 + a)^(n+1) (a theorem in the lesson). We
expand this polynomial mod 2, read off which classes are nonzero, and invert it
to get the dual classes w-bar with w * w-bar = 1. Consequences: if w is not 1,
RP^n is not parallelisable; if w-bar_k is nonzero, RP^n does not immerse in
R^(n+k-1) (Whitney duality). Exact arithmetic in a finite ring; the topology
enters only through the two theorems the computation takes as input.
"""

from math import comb

def total_sw(n):
    """Coefficients mod 2 of (1 + a)^(n+1), truncated at degree n."""
    return [comb(n + 1, i) % 2 for i in range(n + 1)]

def multiply(p, q, n):
    """Product of two polynomials mod 2, truncated at degree n."""
    r = [0] * (n + 1)
    for i, x in enumerate(p):
        if x:
            for j, y in enumerate(q[: n + 1 - i]):
                r[i + j] ^= y
    return r

def inverse(p, n):
    """Inverse of a polynomial with constant term 1 in Z/2[a]/(a^(n+1))."""
    inv = [1] + [0] * n
    for k in range(1, n + 1):
        inv[k] = sum(p[i] * inv[k - i] for i in range(1, k + 1)) % 2
    return inv

def nonzero_degrees(p):
    """Degrees of the nonzero coefficients, excluding degree 0."""
    return [i for i, x in enumerate(p) if x and i > 0]

def ranges(degrees):
    """Compress [1, 2, 3, 8] to '1-3,8'."""
    parts, start = [], None
    for i, d in enumerate(degrees):
        if start is None:
            start = d
        if i + 1 == len(degrees) or degrees[i + 1] != d + 1:
            parts.append(str(start) if start == d else f"{start}-{d}")
            start = None
    return ",".join(parts) or "-"

def immersion_bound(n):
    """Largest k with w-bar_k nonzero: RP^n does not immerse in R^(n+k-1)."""
    dual = inverse(total_sw(n), n)
    return max(nonzero_degrees(dual), default=0)

def main():
    print("w(T RP^n) = (1 + a)^(n+1) in Z/2[a]/(a^(n+1)), and its inverse w-bar")
    print()
    print(f"{'n':>3}  {'nonzero w_i':<22}{'w = 1?':<8}{'top w-bar':<11}"
          f"no immersion in")
    for n in range(1, 17):
        w = total_sw(n)
        dual = inverse(w, n)
        assert multiply(w, dual, n) == [1] + [0] * n
        assert w[n] == (n + 1) % 2  # w_n = Euler characteristic mod 2
        degs = nonzero_degrees(w)
        shown = ranges(degs)
        k = immersion_bound(n)
        bound = f"R^{n + k - 1}" if k > 1 else "-"
        print(f"{n:>3}  {shown:<22}{'yes' if not degs else 'no':<8}"
              f"{k:<11}{bound}")
    print()
    print("w = 1 exactly when n + 1 is a power of 2 (n = 1, 3, 7, 15): only")
    print("those RP^n can be parallelisable, and RP^1, RP^3, RP^7 are. For")
    print("n = 2^r the top dual class is w-bar_(n-1), so RP^(2^r) does not")
    print("immerse in R^(2n-2): Whitney's bound 2n - 1 is sharp.")
    print()
    print("Epistemic status: exact computation in Z/2[a]/(a^(n+1)) for n <= 16.")
    print("It takes as given the theorem w(T RP^n) = (1 + a)^(n+1) and Whitney")
    print("duality; parallelisability of RP^15 is NOT implied (it is false:")
    print("by Adams, only RP^1, RP^3 and RP^7 are parallelisable).")

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

Used in