Skip to content
The Lumenary

Math laboratory

Exterior calculus on R^3: wedge, d, and d d = 0

Implements differential forms on R^3 as dictionaries of SymPy coefficients, with the wedge product and the exterior derivative. With arbitrary coefficient functions it verifies d(dw) = 0 in degrees 0 to 2, graded anticommutativity, the Leibniz rule, and that d is grad, curl and div; it then checks the divergence theorem exactly on the unit cube for one field. Symbolic verification on examples.

Back to the labSymbolic computation
Exterior calculus on R^3: wedge, d, and d d = 0PythonSymbolic computationComputationally tested
"""Exterior calculus on R^3, symbolically.

A differential form on R^3 is stored as a dictionary from increasing index
tuples to SymPy expressions: {(0,): P, (1,): Q, (2,): R} is P dx + Q dy + R dz.
We implement the wedge product (with the sign of the sorting permutation) and
the exterior derivative d, and let SymPy check, for forms with arbitrary
smooth coefficient functions: d(d w) = 0 in every degree, the graded rule
a ^ b = (-1)^(kl) b ^ a, the Leibniz rule for d, and the dictionary between
d and grad, curl, div (so curl grad = 0 and div curl = 0 are d d = 0). Finally
we check Stokes' theorem (the divergence theorem) exactly on the unit cube for
one vector field. Evidence: symbolic verification, relying on SymPy's
equality of mixed partial derivatives.
"""

import re
from itertools import product

import sympy

X = sympy.symbols("x y z")
NAMES = ("dx", "dy", "dz")

def sort_sign(indices):
    """(sign, sorted tuple) of a tuple of indices; sign 0 if one repeats."""
    if len(set(indices)) < len(indices):
        return 0, ()
    sign, idx = 1, list(indices)
    for i in range(len(idx)):
        for j in range(len(idx) - 1 - i):
            if idx[j] > idx[j + 1]:
                idx[j], idx[j + 1] = idx[j + 1], idx[j]
                sign = -sign
    return sign, tuple(idx)

def clean(form):
    """Drop zero coefficients after expanding."""
    out = {}
    for key, c in form.items():
        c = sympy.expand(c)
        if c != 0:
            out[key] = c
    return out

def add(*forms):
    """Sum of forms."""
    out = {}
    for f in forms:
        for key, c in f.items():
            out[key] = out.get(key, 0) + c
    return clean(out)

def scale(c, form):
    """Multiply a form by a function or number."""
    return clean({key: c * v for key, v in form.items()})

def wedge(a, b):
    """The wedge product of two forms."""
    out = {}
    for (ka, ca), (kb, cb) in product(a.items(), b.items()):
        sign, key = sort_sign(ka + kb)
        if sign:
            out[key] = out.get(key, 0) + sign * ca * cb
    return clean(out)

def d(form):
    """The exterior derivative: d(c dx_I) = sum_i (dc/dx_i) dx_i ^ dx_I."""
    out = {}
    for key, c in form.items():
        for i, xi in enumerate(X):
            sign, new = sort_sign((i,) + key)
            if sign:
                out[new] = out.get(new, 0) + sign * sympy.diff(c, xi)
    return clean(out)

def generic(degree, name):
    """A k-form whose coefficients are arbitrary functions of x, y, z."""
    keys = [k for k in product(range(3), repeat=degree) if list(k) == sorted(set(k))]
    return {k: sympy.Function(f"{name}{''.join(map(str, k))}")(*X) for k in keys}

def short(expr):
    """Write Derivative(f(x, y, z), x) as f_x and f(x, y, z) as f."""
    text = str(expr)
    text = re.sub(r"Derivative\((\w+)\(x, y, z\), (\w)\)", r"\1_\2", text)
    return re.sub(r"(\w+)\(x, y, z\)", r"\1", text)

def show(form):
    """Readable text for a form."""
    if not form:
        return "0"
    parts = []
    for key in sorted(form, key=lambda k: (len(k), k)):
        basis = "^".join(NAMES[i] for i in key)
        parts.append(f"({short(form[key])}){' ' + basis if basis else ''}")
    return " + ".join(parts)

def main():
    print(f"Exterior calculus on R^3 (SymPy {sympy.__version__})")
    for k in range(3):
        w = generic(k, "w")
        print(f"  d(d w) = {show(d(d(w)))} for a general {k}-form w")
    a, b, c = generic(1, "a"), generic(1, "b"), generic(2, "c")
    print("  a^b + b^a = 0 for 1-forms:", add(wedge(a, b), wedge(b, a)) == {})
    print("  a^c - c^a = 0 for a 1-form a and a 2-form c:",
          add(wedge(a, c), scale(-1, wedge(c, a))) == {})
    leibniz = add(d(wedge(a, b)), scale(-1, wedge(d(a), b)), wedge(a, d(b)))
    print("  d(a^b) = da^b - a^db for 1-forms:", leibniz == {})
    print()
    f = sympy.Function("f")(*X)
    P, Q, R = (sympy.Function(n)(*X) for n in "PQR")
    A, B, C = (sympy.Function(n)(*X) for n in "ABC")
    print("Vector calculus inside d:")
    print("  d f =", show(d({(): f})), " (grad)")
    curl = d({(0,): P, (1,): Q, (2,): R})
    print("  d(P dx + Q dy + R dz): dy^dz:", short(curl[(1, 2)]),
          " dz^dx:", short(-curl[(0, 2)]))
    print("                         dx^dy:", short(curl[(0, 1)]), " (curl)")
    div = d({(1, 2): A, (0, 2): -B, (0, 1): C})
    print("  d(A dy^dz + B dz^dx + C dx^dy) =", show(div), " (div)")
    print("  so curl(grad f) = 0 and div(curl F) = 0 are both d(d w) = 0")
    print()
    x, y, z = X
    field = {(1, 2): x * y ** 2, (0, 2): -(y * z), (0, 1): x * z ** 2}
    volume = sympy.integrate(d(field)[(0, 1, 2)], (x, 0, 1), (y, 0, 1),
                             (z, 0, 1))
    P, Q, R = field[(1, 2)], -field[(0, 2)], field[(0, 1)]
    flux = sum(
        sympy.integrate(F_comp.subs(v, 1) - F_comp.subs(v, 0), *lim)
        for F_comp, v, lim in [(P, x, [(y, 0, 1), (z, 0, 1)]),
                               (Q, y, [(x, 0, 1), (z, 0, 1)]),
                               (R, z, [(x, 0, 1), (y, 0, 1)])])
    print("Stokes on the unit cube for F = (x y^2, y z, x z^2):")
    print(f"  integral of d(omega) = {volume}, flux through boundary = {flux}")
    print()
    print("Epistemic status: symbolic verification. The identities hold for")
    print("arbitrary coefficient functions because SymPy treats mixed partials")
    print("as equal (true for C^2 functions); the cube check is one example of")
    print("Stokes' theorem, which is proved in the lesson, not here.")

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

Used in