Skip to content
The Lumenary

Math laboratory

The figure eight: free group words and covering spaces

Reduces words in the free group on a and b, the fundamental group of the figure eight; counts reduced words, shows ab and ba differ, and detects this with a homomorphism to S3. It then counts n-sheeted covering spaces as pairs of permutations: connected ones correspond to transitive pairs, giving 1, 3, 13, 71, 461 subgroups of index n = 1 to 5 and 1, 3, 7, 26 coverings up to isomorphism for n = 1 to 4.

Back to the labExact computationExhaustive finite check
The figure eight: free group words and covering spacesPythonExact computationExhaustive finite checkComputationally tested
"""The fundamental group of the figure eight, and its covering spaces.

By van Kampen's theorem the fundamental group of the figure eight (two
circles joined at a point) is the free group F2 on two loops a and b. We
compute with reduced words (A and B stand for the inverse loops), check that
ab and ba are different, and detect the difference with a homomorphism to
the symmetric group S3. Then we count covering spaces: an n-sheeted covering
of the figure eight is the same as a pair of permutations (where a and b
send each sheet), it is connected exactly when the pair acts transitively,
and connected coverings with a chosen base sheet correspond to subgroups of
index n in F2. Evidence: exact and exhaustive for the finite cases shown.
"""

from itertools import permutations, product

INVERSE = {"a": "A", "A": "a", "b": "B", "B": "b"}

def reduce_word(word):
    """Cancel adjacent inverse pairs (aA, Aa, bB, Bb) until none remain."""
    out = []
    for letter in word:
        if out and out[-1] == INVERSE[letter]:
            out.pop()
        else:
            out.append(letter)
    return "".join(out)

def inverse_word(word):
    """The inverse of a word: reverse it and invert each letter."""
    return "".join(INVERSE[c] for c in reversed(word))

def reduced_words(length):
    """All reduced words of the given length."""
    return [w for w in map("".join, product("aAbB", repeat=length))
            if reduce_word(w) == w]

def compose(p, q):
    """The permutation 'first p, then q' (tuples of images)."""
    return tuple(q[p[i]] for i in range(len(p)))

def act(word, sigma, tau):
    """Image of a word under a -> sigma, b -> tau (letters act left to right)."""
    n = len(sigma)
    inv = {
        "a": sigma, "b": tau,
        "A": tuple(sorted(range(n), key=lambda i: sigma[i])),
        "B": tuple(sorted(range(n), key=lambda i: tau[i])),
    }
    result = tuple(range(n))
    for letter in word:
        result = compose(result, inv[letter])
    return result

def is_transitive(sigma, tau):
    """Whether the group generated by sigma and tau moves 0 to every point."""
    seen, frontier = {0}, [0]
    while frontier:
        i = frontier.pop()
        for j in (sigma[i], tau[i]):
            if j not in seen:
                seen.add(j)
                frontier.append(j)
    return len(seen) == len(sigma)

def transitive_pairs(n):
    """All transitive pairs (sigma, tau) of permutations of {0, ..., n-1}."""
    perms = list(permutations(range(n)))
    return [(s, t) for s in perms for t in perms if is_transitive(s, t)]

def subgroups_of_index(n):
    """Subgroups of index n in F2 = transitive pairs / (n - 1)!."""
    count = len(transitive_pairs(n))
    factorial = 1
    for k in range(2, n):
        factorial *= k
    return count // factorial

def connected_covers(n):
    """Connected n-sheeted coverings up to isomorphism: transitive pairs up to
    simultaneous relabelling of the sheets."""
    perms = list(permutations(range(n)))
    seen, classes = set(), 0
    for s, t in transitive_pairs(n):
        if (s, t) in seen:
            continue
        classes += 1
        for g in perms:
            g_inv = tuple(sorted(range(n), key=lambda i: g[i]))
            seen.add((compose(compose(g_inv, s), g), compose(compose(g_inv, t), g)))
    return classes

def main():
    print("Words in F2 = pi_1(figure eight); A, B are the inverse loops.")
    for word in ("abBA", "aAbaB", "ab", "ba", "abAB"):
        shown = reduce_word(word) or "(empty word = constant loop)"
        print(f"  {word:<6} reduces to {shown}")
    counts = [len(reduced_words(k)) for k in range(1, 7)]
    print(f"  reduced words of length 1..6: {counts}  (4 * 3^(k-1))")
    print()
    sigma, tau = (1, 0, 2), (0, 2, 1)
    print("Homomorphism to S3: a -> (0 1), b -> (1 2) as permutations of 0,1,2")
    for word in ("ab", "ba", "abAB"):
        print(f"  {word:<5} -> {act(word, sigma, tau)}")
    print("  ab and ba have different images, so ab != ba in F2: the")
    print("  fundamental group of the figure eight is not abelian.")
    print()
    print("Coverings of the figure eight with n sheets:")
    print("  n  transitive pairs  subgroups of index n  connected covers")
    for n in range(1, 6):
        pairs = len(transitive_pairs(n))
        covers = connected_covers(n) if n <= 4 else "-"
        print(f"  {n} {pairs:17d} {subgroups_of_index(n):21d} {covers:>17}")
    print()
    print("Epistemic status: exact and exhaustive for words of length at most 6")
    print("and coverings with at most 5 sheets (the subgroup counts agree with")
    print("Marshall Hall's 1949 formula). That pi_1 of the figure eight is F2,")
    print("and that coverings match subgroups, are theorems quoted from the")
    print("lesson, not consequences of this computation.")

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

Used in