parity_fill.py

8.7 kB · python · 226 lines

1import time23import numpy as np4from fractions import Fraction5from math import gcd, sqrt67N_SPIN = 558ODDS_SPIN = list(range(1, N_SPIN + 1, 2))9L_SPIN = len(ODDS_SPIN)10GOLDEN = 137.50776411EXACT_TOP = 2112SPIN_R = 102413SWEEP_R = 51214CHECK_R = 409615LAYER_COUNTS = [4, 8, 14, 28]1617def lcm(a, b):18    return a // gcd(a, b) * b1920def odd_scales(top):21    return list(range(3, top + 1, 2))2223def mass_table(scales):24    period = 125    for n in scales:26        period = lcm(period, n)27    steps = [period // n for n in scales]28    counts = np.zeros(1 << len(scales), dtype=np.int64)29    block = 1 << 2030    for lo in range(0, period, block):31        j = np.arange(lo, min(lo + block, period), dtype=np.int64)32        code = np.zeros(j.shape, dtype=np.int64)33        for i, m in enumerate(steps):34            code |= ((j // m) & 1) << i35        counts += np.bincount(code, minlength=1 << len(scales))36    for i in range(len(scales)):37        bit = 1 << i38        for mask in range(1 << len(scales)):39            if not mask & bit:40                counts[mask] += counts[mask | bit]41    return period, counts4243def fill_exact(period, counts, scales, top):44    bits = [i for i, n in enumerate(scales) if n <= top]45    total = 046    for sub in range(1 << len(bits)):47        mask = 048        size = 049        for i, b in enumerate(bits):50            if sub >> i & 1:51                mask |= 1 << b52                size += 153        total += (-2) ** size * int(counts[mask]) ** 254    return Fraction(period * period - total, 2 * period * period)5556def indicator(n, period):57    j = np.arange(period, dtype=np.int64)58    return ((j // (period // n)) & 1).astype(bool)5960def fill_literal(top):61    scales = odd_scales(top)62    period = 163    for n in scales:64        period = lcm(period, n)65    acc = np.zeros((period, period), dtype=bool)66    for n in scales:67        c = indicator(n, period)68        acc ^= c[:, None] & c[None, :]69    return Fraction(int(acc.sum()), period * period)7071def fill_independent(top):72    prod = Fraction(1)73    for n in odd_scales(top):74        p = Fraction((n - 1) ** 2, 4 * n * n)75        prod *= 1 - 2 * p76    return (1 - prod) / 27778def fill_raster(top, r):79    u = (np.arange(r, dtype=np.float64) + 0.5) / r80    acc = np.zeros((r, r), dtype=bool)81    for n in odd_scales(top):82        c = parity(0.5 * n * u)83        acc ^= c[:, None] & c[None, :]84    return float(acc.mean())8586def primes_up_to_count(k):87    out = []88    c = 289    while len(out) < k:90        if all(c % d for d in range(2, int(sqrt(c)) + 1)):91            out.append(c)92        c += 193    return out9495def schedules():96    ps = primes_up_to_count(L_SPIN - 1)97    sched = {98        "unspun": [0.0] * L_SPIN,99        "degrees": [float(k + 1) for k in range(L_SPIN)],100        "primes": [0.0] + [float(p) for p in ps],101        "golden": [GOLDEN * (k + 1) for k in range(L_SPIN)],102    }103    for q in (2, 3, 4, 5):104        sched["eye 90/%d" % q] = [90.0 / q * k for k in range(L_SPIN)]105    return sched106107def parity(z):108    return z - np.floor(z) >= 0.5109110def spun_field(angles, r, layers=L_SPIN, dtype=np.float32):111    step = (np.arange(r, dtype=dtype) + dtype(0.5)) * dtype(1.0 / r)112    d = step - dtype(0.5)113    acc = np.zeros((r, r), dtype=bool)114    for k in range(layers):115        n = ODDS_SPIN[k]116        h = 0.5 * n117        t = np.float64(angles[k]) * np.pi / 180.0118        cx = dtype(h * np.cos(t))119        sx = dtype(h * np.sin(t))120        c0 = dtype(h * 0.5)121        a = c0 + cx * d[None, :] + sx * d[:, None]122        b = c0 - sx * d[None, :] + cx * d[:, None]123        acc ^= parity(a) & parity(b)124    return acc125126def disc_mask(r):127    d = (np.arange(r, dtype=np.float64) + 0.5) / r - 0.5128    return d[:, None] ** 2 + d[None, :] ** 2 <= 0.25129130def spun_fill(angles, r, layers=L_SPIN, disc=True):131    f = spun_field(angles, r, layers)132    return float(f[disc_mask(r)].mean()) if disc else float(f.mean())133134def raster_row(top, r):135    u = (np.arange(r, dtype=np.float64) + 0.5) / r136    acc = np.zeros((r, r), dtype=bool)137    out = {}138    for n in odd_scales(top):139        c = parity(0.5 * n * u)140        acc ^= c[:, None] & c[None, :]141        out[n] = float(acc.mean())142    return out143144def section_exact():145    scales = odd_scales(EXACT_TOP)146    period, counts = mass_table(scales)147    tops = list(range(3, EXACT_TOP + 1, 2))148    ras = raster_row(EXACT_TOP, CHECK_R)149    print("exact fill of the parity fold, odd scales 3..N, 1D grid period %d, %d subsets" % (period, 1 << len(scales)))150    exact = {}151    for top in tops:152        f = fill_exact(period, counts, scales, top)153        exact[top] = f154        lit = fill_literal(top) if top <= 9 else None155        print(156            " N %2d L %2d fill %s = %.9f raster %.9f gap %.2e" % (top, top // 2 + 1, f, float(f), ras[top], abs(float(f) - ras[top])),157            "" if lit is None else ("literal 2D XOR %s %s" % (lit, "match" if lit == f else "MISMATCH")),158        )159    zero = sum(1 for k in range(1 << len(scales)) if counts[k] == 0)160    print(" joint 1D masses: %d of the %d subsets have m_S = 0, the smallest being {3,5,7}" % (zero, 1 << len(scales)))161    print(" joint 1D masses m_S of the first scales", " ".join("m{%s} = %s" % (",".join(str(scales[i]) for i in range(len(scales)) if k >> i & 1), Fraction(int(counts[k]), period)) for k in (1, 2, 3, 5, 7)))162    return exact163164def section_independent(exact):165    print("exact against independent Bernoulli layers, p_n = ((n-1)/(2n))^2")166    for top in sorted(exact):167        f = exact[top]168        g = fill_independent(top)169        de = Fraction(1, 2) - f170        di = Fraction(1, 2) - g171        print(172            " N %2d exact %.9f independent %.9f difference %+.3e deviation %.6e against %.6e ratio %.6f"173            % (top, float(f), float(g), float(f - g), float(de), float(di), float(de / di))174        )175    print(" decay of the exact deviation, successive ratios", " ".join("%.6f" % float((Fraction(1, 2) - exact[t + 2]) / (Fraction(1, 2) - exact[t])) for t in sorted(exact)[:-1]))176    print(" decay of the independent deviation, successive ratios", " ".join("%.6f" % float((Fraction(1, 2) - fill_independent(t + 2)) / (Fraction(1, 2) - fill_independent(t))) for t in sorted(exact)[:-1]))177178def section_spun(exact):179    sched = schedules()180    print("spun parity fill at N %d, %d layers, R %d, inscribed disc" % (N_SPIN, L_SPIN, SPIN_R))181    rows = []182    for name, angles in sched.items():183        v = spun_fill(angles, SPIN_R)184        rows.append((name, v))185        print(" %-9s fill %.6f distance to 1/2 %.6f" % (name, v, abs(v - 0.5)))186    for name in ("unspun", "degrees", "eye 90/3"):187        print(" %-9s against resolution" % name, " ".join("R %d %.6f" % (r, spun_fill(sched[name], r)) for r in (256, 512, 1024, 2048)))188    best = min(rows, key=lambda r: abs(r[1] - 0.5))189    worst = max(rows, key=lambda r: abs(r[1] - 0.5))190    print(" closest to 1/2 %s at %.6f, furthest %s at %.6f" % (best[0], best[1], worst[0], worst[1]))191    print("fill against layer count at R %d, exact and square raster on the unit square, spun readings on the disc" % SPIN_R)192    for lc in LAYER_COUNTS:193        top = 2 * lc - 1194        ex = "%.6f" % float(exact[top]) if top in exact else "-"195        print(196            " L %2d N %2d exact %10s square %.6f disc" % (lc, top, ex, spun_fill(sched["unspun"], SPIN_R, lc, disc=False)),197            " ".join("%s %.6f" % (name, spun_fill(angles, SPIN_R, lc)) for name, angles in sched.items() if name in ("unspun", "degrees", "primes", "golden")),198        )199200def section_sweep():201    print("fixed increment sweep at N %d, R %d, disc fill against increment in whole degrees" % (N_SPIN, SWEEP_R))202    rows = []203    for deg in range(0, 91):204        angles = [float(deg) * k for k in range(L_SPIN)]205        rows.append((deg, spun_fill(angles, SWEEP_R)))206    for deg, v in rows:207        print(" increment %2d fill %.6f" % (deg, v))208    order = sorted(rows, key=lambda r: r[1])209    print(" lowest", " ".join("%d:%.6f" % r for r in order[:5]))210    print(" highest", " ".join("%d:%.6f" % r for r in order[-5:]))211    near = sorted(rows, key=lambda r: abs(r[1] - 0.5))212    print(" closest to 1/2", " ".join("%d:%.6f" % r for r in near[:5]))213    print(" eyes q = 1, 5, 3, 2 at increments 0, 18, 30, 45 and the quarter turn", " ".join("%d:%.6f" % (d, dict(rows)[d]) for d in (0, 18, 30, 45, 90)))214    v = dict(rows)215    print(" mirror check fill(d) = fill(90 - d): %d of 46 pairs equal to the bit, largest gap %.3e, quarter-turn check fill(90) = fill(0): %s" % (sum(1 for d in range(46) if v[d] == v[90 - d]), max(abs(v[d] - v[90 - d]) for d in range(46)), v[0] == v[90]))216    print(" spread of the 89 nonzero whole increments: min %.6f max %.6f, unspun %.6f" % (min(v[d] for d in range(1, 90)), max(v[d] for d in range(1, 90)), v[0]))217218def main():219    t0 = time.perf_counter()220    exact = section_exact()221    section_independent(exact)222    section_spun(exact)223    section_sweep()224    print("seconds", round(time.perf_counter() - t0, 2))225226main()