Skip to content
The Lumenary

Math laboratory

Clock arithmetic: which residues have inverses?

Prints the addition and multiplication tables mod 6, finds by search which residues have multiplicative inverses mod 6 and mod 7, and checks for every k from 2 to 30 that these are exactly the a with gcd(a, k) = 1. The table proves the pattern only for k <= 30; for every k it is a theorem (from Bezout's identity) proved elsewhere.

Back to the labExhaustive finite checkExact computation
Clock arithmetic: which residues have inverses?PythonExhaustive finite checkExact computationComputationally tested
"""Clock arithmetic: tables mod k and which residues can be undone.

On a k-hour clock we only keep remainders 0, 1, ..., k-1. We print the
addition and multiplication tables mod 6, find which residues have a
multiplicative inverse (some b with a * b = 1 mod k) by searching, and compare
the answer with the rule "gcd(a, k) = 1" for every modulus k from 2 to 30.

Runs unchanged in the browser (Pyodide) using only the standard library.
"""

from math import gcd
import textwrap

def add_table(k):
    """The addition table mod k as a list of rows: row a, column b holds (a + b) % k."""
    if k < 1:
        raise ValueError("the modulus k must be at least 1")
    return [[(a + b) % k for b in range(k)] for a in range(k)]

def mul_table(k):
    """The multiplication table mod k: row a, column b holds (a * b) % k."""
    if k < 1:
        raise ValueError("the modulus k must be at least 1")
    return [[(a * b) % k for b in range(k)] for a in range(k)]

def inverse_by_search(a, k):
    """Return the b in 0..k-1 with a * b = 1 (mod k), found by trying every b; None if none.

    Note that mod 1 every number is 0 and 0 * 0 = 0 = 1 (mod 1), so 0 counts as invertible.
    """
    if k < 1:
        raise ValueError("the modulus k must be at least 1")
    for b in range(k):
        if (a * b) % k == 1 % k:
            return b
    return None

def invertible_residues(k):
    """The residues a in 0..k-1 that have an inverse mod k, found by search."""
    return [a for a in range(k) if inverse_by_search(a, k) is not None]

def coprime_residues(k):
    """The residues a in 0..k-1 with gcd(a, k) = 1."""
    return [a for a in range(k) if gcd(a, k) == 1]

def inverse_by_pow(a, k):
    """Inverse via Python's pow(a, -1, k) (the extended Euclidean algorithm), or None."""
    try:
        return pow(a, -1, k)
    except ValueError:
        return None

def compare_up_to(k_max, k_min=2):
    """For k = k_min..k_max compare search, gcd rule and pow(a, -1, k).

    Returns a list of moduli where the three methods disagree (empty if they always agree).
    """
    disagreements = []
    for k in range(k_min, k_max + 1):
        searched = invertible_residues(k)
        by_pow = [a for a in range(k) if inverse_by_pow(a, k) is not None]
        same_inverses = all(inverse_by_search(a, k) == inverse_by_pow(a, k) for a in searched)
        if not (searched == coprime_residues(k) == by_pow and same_inverses):
            disagreements.append(k)
    return disagreements

def format_side_by_side(k):
    """Return the lines of the + and x tables mod k printed next to each other."""
    head = " ".join(f"{b:>2}" for b in range(k))
    lines = [f" + |{head}      x |{head}", "---+" + "-" * (3 * k) + "    ---+" + "-" * (3 * k)]
    for a, (add_row, mul_row) in enumerate(zip(add_table(k), mul_table(k))):
        add_s = " ".join(f"{v:>2}" for v in add_row)
        mul_s = " ".join(f"{v:>2}" for v in mul_row)
        lines.append(f"{a:>2} |{add_s}     {a:>2} |{mul_s}")
    return lines

def main():
    print("Addition and multiplication on a 6-hour clock (mod 6):")
    for line in format_side_by_side(6):
        print("  " + line)
    print()
    for k in (6, 7):
        units = invertible_residues(k)
        pairs = " ".join(f"{a}*{inverse_by_search(a, k)}" for a in units if a <= inverse_by_search(a, k))
        print(f"Mod {k}: invertible residues {str(units):<20} inverse pairs: {pairs}")
    print("Mod 6, the residue 2 has no inverse: row 2 of the x table never contains a 1.")
    print()
    k_max = 30
    bad = compare_up_to(k_max)
    print(f"For every k = 2..{k_max}, compare three answers to 'which a have inverses?':")
    print("  search every b,  the rule gcd(a, k) = 1,  and pow(a, -1, k)")
    print(f"  moduli where they disagree: {bad if bad else 'none'}")
    counts = " ".join(str(len(coprime_residues(k))) for k in range(2, 17))
    print(f"  how many residues are invertible, k = 2..16: {counts}")
    print()
    print(textwrap.fill(
        f"Epistemic status: every residue for every k up to {k_max} was checked, so "
        "the gcd rule is verified for those moduli only. That it holds for every "
        "k is a theorem (it follows from Bezout's identity) proved elsewhere, not "
        "by this table.",
        width=78,
    ))

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

Used in