Skip to content
The Lumenary

Math laboratory

Fusion rings from groups, and the Fibonacci ring

Builds S3, D4 and Q8 as matrix groups, finds all one-dimensional characters by brute force, adds the trace of a 2-dimensional representation, checks orthonormality and computes the fusion rings exactly: D4 and Q8 are non-isomorphic groups with isomorphic fusion rings. Then approximates the Frobenius-Perron dimensions of the Fibonacci and Ising rings (golden ratio, sqrt 2) by power iteration.

Back to the labExact computationExhaustive finite checkNumerical approximation
Fusion rings from groups, and the Fibonacci ringPythonExact computationExhaustive finite checkNumerical approximationComputationally tested
"""Fusion rings: decategorifying Rep(G), and a fusion ring that is not Rep(G).

The Grothendieck ring of Rep(G) has a basis of irreducible representations,
with V_i (x) V_j = sum_k N_ij^k V_k, and N_ij^k = <chi_i chi_j, chi_k>. We
build S3, D4 and Q8 as groups of matrices, find their one-dimensional
characters by brute force, take traces of a faithful two-dimensional
representation, check that these characters are orthonormal and exhaust the
group (sum of squares of dimensions = |G|), and compute the fusion rings
exactly. D4 and Q8 are not isomorphic (they have 5 and 1 elements of order 2)
but their fusion rings are isomorphic - and Rep(D4), Rep(Q8) are not even
equivalent as tensor categories (Tambara and Yamagami, 1998): the
Grothendieck ring forgets. Finally the Fibonacci fusion ring,
tau (x) tau = 1 + tau, has Frobenius-Perron dimension the golden ratio, so it
is not the Grothendieck ring of any Rep(G); the Ising ring has FPdim sqrt 2.
Exact for the groups; the FP dimensions are floating-point approximations.
"""

from fractions import Fraction
from itertools import permutations, product
import math

# Matrices have Gaussian-integer entries (re, im), so all arithmetic is exact.
ONE = ((1, 0), (0, 0)), ((0, 0), (1, 0))

def g_mul(a, b):
    """Product of Gaussian integers a = (re, im), b = (re, im)."""
    return (a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0])

def m_mul(x, y):
    """Product of 2 x 2 matrices of Gaussian integers."""
    def entry(i, j):
        p, q = g_mul(x[i][0], y[0][j]), g_mul(x[i][1], y[1][j])
        return (p[0] + q[0], p[1] + q[1])
    return ((entry(0, 0), entry(0, 1)), (entry(1, 0), entry(1, 1)))

def generate(gens):
    """The finite group generated by some invertible 2 x 2 matrices."""
    group, frontier = {ONE}, [ONE]
    while frontier:
        new = []
        for g in frontier:
            for s in gens:
                h = m_mul(g, s)
                if h not in group:
                    group.add(h)
                    new.append(h)
        frontier = new
    return [ONE] + sorted(group - {ONE})  # identity first

def trace(m):
    """Trace, as a Gaussian integer."""
    return (m[0][0][0] + m[1][1][0], m[0][0][1] + m[1][1][1])

def element_order(m):
    """Order of a group element."""
    k, x = 1, m
    while x != ONE:
        x, k = m_mul(x, m), k + 1
    return k

def linear_characters(group):
    """All homomorphisms group -> {1, -1}, by checking every function."""
    index = {g: i for i, g in enumerate(group)}
    table = [[index[m_mul(a, b)] for b in group] for a in group]
    found = []
    for signs in product((1, -1), repeat=len(group)):
        if all(signs[table[i][j]] == signs[i] * signs[j]
               for i in range(len(group)) for j in range(len(group))):
            found.append(list(signs))
    return found

def character_table(gens):
    """Linear characters plus the trace of the defining 2-dim representation.

    For S3, D4 and Q8 the characters here are real integers.
    """
    group = generate(gens)
    chars = linear_characters(group)
    two = [trace(g) for g in group]
    assert all(t[1] == 0 for t in two), "real-valued for these examples"
    chars.append([t[0] for t in two])
    return group, chars

def inner(f, g):
    """<f, g> = average of f * g over the group (real characters)."""
    return Fraction(sum(a * b for a, b in zip(f, g)), len(f))

def fusion(chars):
    """N[i][j][k] = <chi_i chi_j, chi_k>."""
    r = range(len(chars))
    return [[[inner([x * y for x, y in zip(chars[i], chars[j])], chars[k])
              for k in r] for j in r] for i in r]

def check_fusion_ring(n):
    """Non-negative integers, unit = basis element 0, commutative, associative."""
    r = range(len(n))
    integral = all(v.denominator == 1 and v >= 0
                   for a in n for b in a for v in b)
    unit = all(n[0][j][k] == (j == k) for j in r for k in r)
    comm = all(n[i][j][k] == n[j][i][k] for i in r for j in r for k in r)
    assoc = all(sum(n[i][j][m] * n[m][k][l] for m in r)
                == sum(n[j][k][m] * n[i][m][l] for m in r)
                for i, j, k, l in product(r, repeat=4))
    return integral and unit and comm and assoc

def isomorphic_fusion(n1, n2):
    """Is there a relabelling of basis elements (fixing the unit) with n1 = n2?"""
    size = len(n1)
    if size != len(n2):
        return False
    r = range(size)
    for perm in permutations(range(1, size)):
        s = (0,) + perm
        if all(n1[i][j][k] == n2[s[i]][s[j]][s[k]]
               for i in r for j in r for k in r):
            return True
    return False

def fp_dimension(matrix, steps=200):
    """Perron-Frobenius eigenvalue of a non-negative irreducible matrix.

    Power iteration on M + I (whose dominant eigenvalue is strictly largest in
    absolute value, so the iteration converges even when M is periodic, as
    for Ising), then subtract 1.
    """
    size = len(matrix)
    shifted = [[matrix[i][j] + (i == j) for j in range(size)]
               for i in range(size)]
    v = [1.0] * size
    lam = 0.0
    for _ in range(steps):
        w = [sum(a * b for a, b in zip(row, v)) for row in shifted]
        lam = max(w) / max(v)
        v = [x / max(w) for x in w]
    return lam - 1

Z, P, M, I, J = (0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)
GROUPS = {
    # S3 = Weyl group of A2: the two simple reflections, in a basis of roots.
    "S3": [((M, P), (Z, P)), ((P, Z), (P, M))],
    # D4: rotation by 90 degrees and a reflection.
    "D4": [((Z, M), (P, Z)), ((P, Z), (Z, M))],
    # Q8: the quaternions i and j as 2 x 2 complex matrices.
    "Q8": [((I, Z), (Z, J)), ((Z, P), (M, Z))],
}

def main():
    rings = {}
    for name, gens in GROUPS.items():
        group, chars = character_table(gens)
        n = fusion(chars)
        rings[name] = n
        dims = [c[0] for c in chars]
        orth = all(inner(a, b) == (i == j) for i, a in enumerate(chars)
                   for j, b in enumerate(chars))
        invol = sum(1 for g in group if element_order(g) == 2)
        squares = sum(d * d for d in dims)
        ok = orth and check_fusion_ring(n) and squares == len(group)
        print(f"Rep({name}): |G| = {len(group)}, dims {dims}, elements of "
              f"order 2: {invol}; checks {'pass' if ok else 'FAIL'}")
    last = len(rings["S3"]) - 1
    print("  in Rep(S3): V (x) V has multiplicities",
          [int(v) for v in rings["S3"][last][last]], "(triv, sign, V)")
    print("  in Rep(D4): E (x) E has multiplicities",
          [int(v) for v in rings["D4"][4][4]], "(four lines, E)")
    print("  fusion rings of D4 and Q8 isomorphic:",
          isomorphic_fusion(rings["D4"], rings["Q8"]))
    print()
    fib = [[0, 1], [1, 1]]  # multiplication by tau in the basis (1, tau)
    lam = fp_dimension(fib)
    print("Fibonacci ring: tau (x) tau = 1 + tau")
    print(f"  FPdim(tau) by power iteration = {lam:.12f}")
    print(f"  golden ratio (1 + sqrt 5)/2    = {(1 + math.sqrt(5)) / 2:.12f}")
    ising = [[0, 1, 0], [1, 0, 1], [0, 1, 0]]  # sigma on (1, sigma, psi)
    print("Ising ring: sigma (x) sigma = 1 + psi, sigma (x) psi = sigma")
    print(f"  FPdim(sigma) = {fp_dimension(ising):.12f}"
          f" (sqrt 2 = {math.sqrt(2):.12f})")
    print()
    print("Epistemic status: the groups, characters, fusion coefficients and")
    print("ring axioms are computed exactly and exhaustively for these three")
    print("groups. The FP dimension is a floating-point approximation; that")
    print("d^2 = 1 + d has no integer root is proved in the lesson, and that")
    print("Rep(D4), Rep(Q8) differ as tensor categories is a theorem not")
    print("checked here.")

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

Used in