Skip to content
The Lumenary

Math laboratory

Primitive roots and Artin's conjecture

Decides for every n up to 60 whether (Z/nZ)^x is cyclic and checks the classification n = 1, 2, 4, p^k, 2p^k; finds least primitive roots; and measures how often 2 is a primitive root modulo primes below 50000, compared with Artin's constant 0.3739558 (a conjectured density, proved only under the generalised Riemann hypothesis). Statistical for Artin.

Back to the labExact computationExhaustive finite checkStatistical evidence
Primitive roots and Artin's conjecturePythonExact computationExhaustive finite checkStatistical evidenceComputationally tested
"""Primitive roots: which moduli have them, and how often 2 is one.

A primitive root modulo n is a generator of the unit group (Z/nZ)^x. The
program finds, for every n up to 60, whether a primitive root exists, and
compares with the theorem: exactly when n is 1, 2, 4, p^k or 2p^k for an odd
prime p. It lists the least primitive root of the first primes, then counts the
primes below 50000 for which 2 is a primitive root and compares the proportion
with Artin's constant 0.3739558..., which Artin conjectured in 1927 to be the
limiting density (proved by Hooley in 1967 assuming the generalised Riemann
hypothesis; still open unconditionally).
"""

from math import gcd

ARTIN_CONSTANT = 0.3739558136

def prime_factors(n):
    """The distinct prime factors of n, by trial division."""
    factors, q = [], 2
    while q * q <= n:
        if n % q == 0:
            factors.append(q)
            while n % q == 0:
                n //= q
        q += 1
    if n > 1:
        factors.append(n)
    return factors

def totient(n):
    """Euler's phi function from the prime factorisation."""
    result = n
    for q in prime_factors(n):
        result -= result // q
    return result

def multiplicative_order(a, n):
    """Least k >= 1 with a^k = 1 mod n (requires gcd(a, n) = 1)."""
    phi = totient(n)
    order = phi
    for q in prime_factors(phi):
        while order % q == 0 and pow(a, order // q, n) == 1:
            order //= q
    return order

def primitive_root(n):
    """The least primitive root modulo n, or None if there is none."""
    if n == 1:
        return 0
    phi = totient(n)
    for a in range(1, n):
        if gcd(a, n) == 1 and multiplicative_order(a, n) == phi:
            return a
    return None

def predicted(n):
    """The classification: n = 1, 2, 4, p^k or 2 p^k with p an odd prime."""
    if n in (1, 2, 4):
        return True
    m = n // 2 if n % 2 == 0 else n
    if m % 2 == 0:
        return False
    return len(prime_factors(m)) == 1

def primes_below(n):
    """Sieve of Eratosthenes."""
    sieve = bytearray([1]) * n
    sieve[0:2] = b"\x00\x00"
    for i in range(2, int(n ** 0.5) + 1):
        if sieve[i]:
            sieve[i * i::i] = bytearray(len(sieve[i * i::i]))
    return [i for i in range(n) if sieve[i]]

def two_is_primitive_share(bound):
    """(number of odd primes p < bound with 2 a primitive root, number of odd primes)."""
    ps = primes_below(bound)[1:]
    hits = sum(multiplicative_order(2, p) == p - 1 for p in ps)
    return hits, len(ps)

def main():
    have = [n for n in range(1, 61) if primitive_root(n) is not None]
    print("n <= 60 with a primitive root:")
    print("  " + " ".join(map(str, have[:24])))
    print("  " + " ".join(map(str, have[24:])))
    agree = all((primitive_root(n) is not None) == predicted(n) for n in range(1, 61))
    print(f"matches 1, 2, 4, p^k, 2p^k for every n <= 60: {agree}")
    print("no primitive root: 8, 12, 15, ... e.g. orders mod 8:",
          [multiplicative_order(a, 8) for a in (1, 3, 5, 7)])
    first = primes_below(60)
    print("least primitive root of p:")
    print("  " + "  ".join(f"{p}:{primitive_root(p)}" for p in first[:9]))
    print("  " + "  ".join(f"{p}:{primitive_root(p)}" for p in first[9:]))
    for bound in (1000, 10000, 50000):
        hits, total = two_is_primitive_share(bound)
        print(f"2 is a primitive root for {hits:>4} of {total:>4} odd primes "
              f"below {bound:>5}: {hits / total:.4f}")
    print(f"Artin's constant (conjectured density):           {ARTIN_CONSTANT:.4f}")
    print()
    print("Epistemic status: the table for n <= 60 is exact and exhaustive, and")
    print("agrees with the classification proved in the lesson. The proportions")
    print("for 2 are statistical evidence for Artin's conjecture, which remains")
    print("unproved without the generalised Riemann hypothesis.")

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

Used in