Skip to content
The Lumenary

Math laboratory

Representable functors on finite sets and groups, by brute force

Checks naturality of subsets versus characteristic functions for all maps between sets of size <= 3; counts all homomorphisms Z/n -> S3 and V4 -> S3 by brute force and matches them with solutions of the defining relations (the representing property of presentations); and applies the counting test |F(n)| = n^a or a^n to show that several functors on finite sets are not representable. Exhaustive in these finite cases only.

Back to the labExhaustive finite checkExact computation
Representable functors on finite sets and groups, by brute forcePythonExhaustive finite checkExact computationComputationally tested
"""Representable functors in finite examples: checking and counting.

Three exact, exhaustive checks on small finite sets and groups.
(1) Subsets are characteristic functions: for all sets X, Y with at most 3
elements and all maps f : Y -> X, the characteristic function of f^-1(S) is
chi_S composed with f (naturality of P(X) = Hom(X, 2)).
(2) Homomorphisms out of a presented group are solutions of its relations: we
count all homomorphisms Z/n -> S3 and V4 -> S3 by brute force over every
function and compare with the number of elements g with g^n = 1, and of
commuting pairs of involutions.
(3) A counting test: a representable functor on finite sets has |F(n)| = n^a
(covariant) or a^n (contravariant). Sequences that fit no such formula
belong to functors that cannot be representable.
"""

import itertools

def maps(dom, cod):
    """All functions dom -> cod, as tuples indexed by dom = range(len)."""
    return list(itertools.product(range(cod), repeat=dom))

def subsets(n):
    """All subsets of range(n), as frozensets."""
    return [frozenset(c) for k in range(n + 1)
            for c in itertools.combinations(range(n), k)]

def chi(s, n):
    """The characteristic function of s ⊆ range(n), as a tuple of 0/1."""
    return tuple(1 if x in s else 0 for x in range(n))

def preimage(f, s):
    """f^-1(s) for f given as a tuple."""
    return frozenset(y for y, fy in enumerate(f) if fy in s)

def powerset_naturality(max_size=3):
    """Check chi(f^-1(S)) = chi(S) ∘ f for all f : Y -> X, S ⊆ X; return the
    number of checks and the number of failures."""
    checks = failures = 0
    for n in range(max_size + 1):
        for m in range(max_size + 1):
            for f in maps(m, n):
                for s in subsets(n):
                    checks += 1
                    lhs = chi(preimage(f, s), m)
                    rhs = tuple(chi(s, n)[f[y]] for y in range(m))
                    failures += lhs != rhs
    return checks, failures

# Groups as (elements, multiplication function, identity).

def symmetric_group_3():
    """S3 as permutations of (0, 1, 2); (p*q)(i) = p(q(i))."""
    elems = list(itertools.permutations(range(3)))
    return elems, (lambda p, q: tuple(p[q[i]] for i in range(3))), (0, 1, 2)

def cyclic_group(n):
    """Z/n under addition."""
    return list(range(n)), (lambda a, b: (a + b) % n), 0

def klein_four():
    """V4 = Z/2 x Z/2."""
    elems = [(a, b) for a in range(2) for b in range(2)]
    return elems, (lambda x, y: ((x[0] + y[0]) % 2, (x[1] + y[1]) % 2)), (0, 0)

def count_homs(source, target):
    """Brute force: how many of the functions source -> target respect
    multiplication? (Such functions automatically preserve 1 and inverses.)"""
    s_el, s_mul, _ = source
    t_el, t_mul, _ = target
    count = 0
    for images in itertools.product(t_el, repeat=len(s_el)):
        h = dict(zip(s_el, images))
        if all(h[s_mul(a, b)] == t_mul(h[a], h[b]) for a in s_el for b in s_el):
            count += 1
    return count

def power(g, n, mul, one):
    """g^n in a group."""
    result = one
    for _ in range(n):
        result = mul(result, g)
    return result

def nth_roots_of_one(n, group):
    """Number of g with g^n = 1: the size of Hom(Z/n, G) by representability."""
    elems, mul, one = group
    return sum(power(g, n, mul, one) == one for g in elems)

def commuting_involution_pairs(group):
    """Pairs (x, y) with x^2 = y^2 = 1 and xy = yx: Hom(V4, G)."""
    elems, mul, one = group
    return sum(mul(x, x) == one and mul(y, y) == one and mul(x, y) == mul(y, x)
               for x in elems for y in elems)

def fits_covariant(seq):
    """Return a with seq[n] == n**a for all n (0**0 = 1), or None."""
    for a in range(len(seq) + 3):
        if all(s == n ** a for n, s in enumerate(seq)):
            return a
    return None

def fits_contravariant(seq):
    """Return a with seq[n] == a**n for all n, or None."""
    if len(seq) < 2:
        return None
    a = seq[1]
    return a if all(s == a ** n for n, s in enumerate(seq)) else None

def bell(n):
    """Number of equivalence relations on an n-element set."""
    row, total = [1], 1
    for _ in range(n):
        new = [row[-1]]
        for x in row:
            new.append(new[-1] + x)
        row, total = new, new[0]
    return total if n else 1

def main():
    checks, failures = powerset_naturality(3)
    print("1. Subsets versus maps to {0, 1}: chi(f^-1 S) = chi(S) ∘ f")
    print(f"   all f : Y -> X, S ⊆ X with |X|, |Y| <= 3: {checks} checks,"
          f" {failures} failures")
    print()
    s3 = symmetric_group_3()
    print("2. Homomorphisms into S3: brute force versus the relations")
    for n in range(1, 5):
        print(f"   |Hom(Z/{n}, S3)| = {count_homs(cyclic_group(n), s3):<3}"
              f"  #{{g in S3 : g^{n} = 1}} = {nth_roots_of_one(n, s3)}")
    print(f"   |Hom(V4, S3)|  = {count_homs(klein_four(), s3):<3}"
          f"  #{{(x, y) : x^2 = y^2 = 1, xy = yx}} = "
          f"{commuting_involution_pairs(s3)}")
    print()
    print("3. Counting test on sets of size n = 0..5 (representable => fits)")
    rows = [
        ("contravariant  X -> P(X), preimage", [2 ** n for n in range(6)], "contra"),
        ("contravariant  X -> equivalence relations",
         [bell(n) for n in range(6)], "contra"),
        ("covariant      X -> X × X", [n * n for n in range(6)], "co"),
        ("covariant      X -> P(X), direct image", [2 ** n for n in range(6)], "co"),
        ("covariant      X -> X ⊔ X", [2 * n for n in range(6)], "co"),
    ]
    for label, seq, kind in rows:
        a = fits_contravariant(seq) if kind == "contra" else fits_covariant(seq)
        verdict = f"fits with |A| = {a}" if a is not None else "fits no |A|"
        print(f"   {label:<42} {verdict}")
        print(f"      sizes {seq}")
    print()
    print("Epistemic status: exhaustive, exact checks in these finite cases. "
          "Part 2\nconfirms Hom(Z/n, G) = {g : g^n = 1} for G = S3 only. In "
          "part 3 a\nsequence that fits proves nothing by itself (naturality "
          "is also needed),\nbut one that fits no formula proves that the "
          "functor is not representable.")

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

Used in