census.py

3.7 kB · python · 132 lines

1import sys2import time3from fractions import Fraction4from itertools import product56import numpy as np7from mpmath import mp, zeta89Q = 310D = 311PARITY = {(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)}12DIGITS = [v for v in product(range(Q), repeat=D) if tuple(c % 2 for c in v) in PARITY]13K = len(DIGITS)14WALK = 1215BASE_LOCAL = Fraction(sum(1 for v in DIGITS if any(v)), K)16BRACKET = BASE_LOCAL * Fraction(Q ** D, Q ** D - 1)17LEGAL = np.zeros((Q,) * D, dtype=bool)18for _v in DIGITS:19    LEGAL[_v] = True202122def mobius(limit):23    mu = np.ones(limit + 1, dtype=np.int64)24    mu[0] = 025    composite = np.zeros(limit + 1, dtype=bool)26    primes = []27    for i in range(2, limit + 1):28        if not composite[i]:29            primes.append(i)30            mu[i] = -131        for p in primes:32            if i * p > limit:33                break34            composite[i * p] = True35            if i % p == 0:36                mu[i * p] = 037                break38            mu[i * p] = mu[i] * mu[p]39    return mu404142def divisible_count(d, n):43    counts = np.zeros((d, d, d), dtype=np.int64)44    counts[0, 0, 0] = 145    place = 1 % d46    for _ in range(n):47        nxt = np.zeros_like(counts)48        for v in DIGITS:49            shift = (place * v[0] % d, place * v[1] % d, place * v[2] % d)50            nxt += np.roll(counts, shift, axis=(0, 1, 2))51        counts = nxt52        place = place * Q % d53    return int(counts[0, 0, 0])545556def in_design(pts, n):57    ok = np.ones(len(pts), dtype=bool)58    rest = pts59    for _ in range(n):60        dig = rest % Q61        ok &= LEGAL[dig[:, 0], dig[:, 1], dig[:, 2]]62        rest = rest // Q63    return int(np.count_nonzero(ok))646566def primitive_box(side):67    axis = np.arange(side + 1, dtype=np.int64)68    grid = np.stack(np.meshgrid(axis, axis, axis, indexing="ij"), axis=-1)69    pts = grid.reshape(-1, D)70    return pts[np.gcd.reduce(pts, axis=1) == 1]717273def visible_count(n, cutoff):74    span = Q ** n75    mu = mobius(cutoff)76    head = 077    for d in range(1, cutoff + 1):78        if mu[d]:79            head += int(mu[d]) * (divisible_count(d, n) - 1)80    partial = np.zeros(span, dtype=np.int64)81    for d in range(1, cutoff + 1):82        if mu[d]:83            partial[d::d] += mu[d]84    tail = 085    for g in range(cutoff + 1, span):86        side = (span - 1) // g87        weight = int(partial[g])88        if weight == 0:89            continue90        tail += weight * in_design(g * primitive_box(side), n)91    return head - tail929394def brute_count(n):95    digits = np.array(DIGITS, dtype=np.int64)96    total = 097    for lead in digits:98        pts = lead.reshape(1, D)99        for _ in range(n - 1):100            pts = (pts[:, None, :] * Q + digits[None, :, :]).reshape(-1, D)101        total += int(np.count_nonzero(np.gcd.reduce(pts, axis=1) == 1))102    return total103104105def ladder(n):106    return max(8, min(int(round(Q ** (n / 2))), 120))107108109def main():110    top = int(sys.argv[1]) if len(sys.argv) > 1 else 9111    mp.dps = 30112    delta = mp.mpf(BRACKET.numerator) / BRACKET.denominator / zeta(D)113    print(f"design q={Q} D={D} k={K} bracket={BRACKET} delta={mp.nstr(delta, 10)}")114    print(f"domain n=1..{top} coordinates 0..{Q ** top - 1}")115    for n in range(1, top + 1):116        cut = ladder(n)117        clock = time.time()118        a = visible_count(n, cut)119        line = f"A({n}) = {a}  G={cut}  {time.time() - clock:.1f}s"120        if n <= 6:121            line += f"  brute={brute_count(n)}"122        if n >= 7:123            gap = (delta * mp.mpf(K) ** n - a) / mp.mpf(WALK) ** n124            line += f"  gap/{WALK}^n = {mp.nstr(gap, 5)}"125        print(line, flush=True)126    if top >= 9:127        low = visible_count(9, 100)128        high = visible_count(9, 150)129        print(f"cutoff check A(9) G=100 {low} G=150 {high} agree={low == high}")130131132main()