ford_horocycle.py

10.4 kB · python · 279 lines

1import argparse2import math3import sys4import time5from fractions import Fraction67import numpy as np89DELTA = {2: 0.5312805062772051416, 3: 0.705660908028}1011# TOTIENTS1213def totients(n):14    phi = np.arange(n + 1, dtype=np.int64)15    for p in range(2, n + 1):16        if phi[p] == p:17            phi[p::p] -= phi[p::p] // p18    return phi1920# QUOTIENTS2122def quotients(a, b):23    out = []24    while b:25        q, r = divmod(a, b)26        out.append(q)27        a, b = b, r28    return out[1:]2930def member(a, b, m):31    qs = quotients(a, b)32    if not qs:33        return True34    alt = qs[:-1] + [qs[-1] - 1, 1]35    return max(qs) <= m or max(alt) <= m3637def rule(a, b, m):38    qs = quotients(a, b)39    if not qs:40        return True41    return max(qs[:-1], default=1) <= m and qs[-1] <= m + 14243# CROSSINGS4445def egcd(a, b):46    if b == 0:47        return a, 1, 048    g, x, y = egcd(b, a % b)49    return g, y, x - (a // b) * y5051def mobius_image(a, ap, b, d, z):52    x, y = z53    num = (Fraction(a) * x + ap, Fraction(a) * y)54    den = (Fraction(b) * x + d, Fraction(b) * y)55    n2 = den[0] ** 2 + den[1] ** 256    re = (num[0] * den[0] + num[1] * den[1]) / n257    im = (num[1] * den[0] - num[0] * den[1]) / n258    return re, im5960def crossings(bound, heights):61    phi = totients(bound)62    checked = 063    for b in range(1, bound + 1):64        for a in range(b):65            if math.gcd(a, b) != 1:66                continue67            g, s, t = egcd(a, b)68            ap, d = -t, s69            assert a * d - ap * b == 170            centre = (Fraction(a, b), Fraction(1, 2 * b * b))71            r2 = Fraction(1, 4 * b ** 4)72            for x in (Fraction(0), Fraction(1, 3), Fraction(-7, 2), Fraction(5), Fraction(-d, b)):73                re, im = mobius_image(a, ap, b, d, (x, Fraction(1)))74                assert (re - centre[0]) ** 2 + (im - centre[1]) ** 2 == r275                assert im <= Fraction(1, b * b)76                if x == Fraction(-d, b):77                    assert im == Fraction(1, b * b) and re == Fraction(a, b)78                checked += 179    print(f"horocycle images checked exactly: {checked} points on {int(phi[1:bound + 1].sum())} Ford circles, b <= {bound}, every image on the Ford circle over a/b, top point 1/b^2 at x = -d/b")80    print("Q  height  closed-disk count  sum_{b<=Q} phi  open-disk count  sum_{b<Q} phi  count at 1/(2Q^2)  sum_{b<=floor(Q sqrt2)} phi")81    for Q in heights:82        h = Fraction(1, Q * Q)83        closed = sum(int(phi[b]) for b in range(1, bound + 1) if Fraction(1, b * b) >= h)84        opened = sum(int(phi[b]) for b in range(1, bound + 1) if Fraction(1, b * b) > h)85        half = sum(int(phi[b]) for b in range(1, bound + 1) if Fraction(1, b * b) >= h / 2)86        mQ = int(phi[1:Q + 1].sum())87        mQ1 = int(phi[1:Q].sum())88        m2 = int(phi[1:math.isqrt(2 * Q * Q) + 1].sum())89        assert closed == mQ and opened == mQ1 and half == m290        print(f"{Q}  1/{Q * Q}  {closed}  {mQ}  {opened}  {mQ1}  {half}  {m2}")9192# BRIDGE9394def bump_smooth(u):95    u = np.asarray(u, dtype=float)96    out = np.zeros_like(u)97    inside = (u > 1) & (u < 2)98    w = u[inside]99    out[inside] = np.exp(4 - 1 / ((w - 1) * (2 - w)))100    return out101102def bump_c2(u):103    u = np.asarray(u, dtype=float)104    out = np.zeros_like(u)105    inside = (u > 1) & (u < 2)106    w = u[inside]107    out[inside] = 64 * (w - 1) ** 3 * (2 - w) ** 3108    return out109110def gl(nodes):111    x, w = np.polynomial.legendre.leggauss(nodes)112    return x, w113114def integrate(f, lo, hi, x, w):115    mid, half = (lo + hi) / 2, (hi - lo) / 2116    return half * np.dot(w, f(mid + half * x))117118def g_transform(f, t, x, w):119    if t >= 1:120        return 0.0121    hi = math.sqrt(1 / (t * t) - 1)122    lo = math.sqrt(1 / (2 * t * t) - 1) if t * t <= 0.5 else 0.0123    return 2 * integrate(lambda v: f(1 / (t * t * (1 + v * v))), lo, hi, x, w)124125def horocycle_integral(f, h, x, w):126    Q = math.isqrt(int(1 / h))127    total = 0.0128    bumps = 0129    for c in range(1, Q + 1):130        s = h - c * c * h * h131        if s < 0:132            continue133        width = math.sqrt(s) / c134        inner = math.sqrt(max(h / 2 - c * c * h * h, 0.0)) / c135        dlo = math.ceil(-c * (1 + width))136        dhi = math.floor(c * width)137        for d in range(dlo, dhi + 1):138            if math.gcd(c, abs(d)) != 1:139                continue140            centre = -d / c141            pieces = ((centre - width, centre - inner), (centre + inner, centre + width))142            hit = False143            for lo, hi in pieces:144                lo, hi = max(0.0, lo), min(1.0, hi)145                if lo >= hi:146                    continue147                total += integrate(lambda t: f(h / ((c * t + d) ** 2 + c * c * h * h)), lo, hi, x, w)148                hit = True149            bumps += hit150    return total, bumps151152def bridge(kmax, nodes):153    x, w = gl(nodes)154    for name, f in (("smooth bump", bump_smooth), ("C2 bump", bump_c2)):155        main = 3 / math.pi * integrate(lambda u: f(u) / (u * u), 1, 2, x, w)156        print(f"{name}: main term (3/pi) int f(w) w^-2 dw = {main:.15f}")157        print("k  h=4^-k  Q  bumps  horocycle integral  h S_g(h^1/2)  abs gap  error E  E/h^(3/4)")158        Qmax = 2 ** kmax159        phi = totients(Qmax)160        for k in range(2, kmax + 1):161            h = 4.0 ** (-k)162            Q = 2 ** k163            t0 = time.time()164            lhs, bumps = horocycle_integral(f, h, x, w)165            rhs = h * sum(int(phi[c]) * g_transform(f, c * math.sqrt(h), x, w) for c in range(1, Q + 1))166            err = lhs - main167            print(f"{k}  {h:.3e}  {Q}  {bumps}  {lhs:.15f}  {rhs:.15f}  {abs(lhs - rhs):.1e}  {err:+.3e}  {err / h ** 0.75:+.4f}  ({time.time() - t0:.1f}s)")168169# CENSUS170171def census_walk(m, qmax):172    hist = np.zeros(qmax + 1, dtype=np.int64)173    hist[1] = 1174    stack = [(1, 2, 0, 1, 1, 1, 0, 1)]175    while stack:176        p, q, p1, q1, p2, q2, last, run = stack.pop()177        hist[q] += 1178        ql = q + q1179        if ql <= qmax:180            r = run + 1 if last == 0 else 1181            if r <= m:182                stack.append((p + p1, ql, p1, q1, p, q, 0, r))183        qr = q + q2184        if qr <= qmax:185            r = run + 1 if last == 1 else 1186            if r <= m:187                stack.append((p + p2, qr, p, q, p2, q2, 1, r))188    return np.cumsum(hist)189190def census_nodes(m, qmax):191    out = []192    stack = [(1, 2, 0, 1, 1, 1, 0, 1)]193    while stack:194        node = stack.pop()195        p, q, p1, q1, p2, q2, last, run = node196        out.append(node)197        ql, qr = q + q1, q + q2198        if ql <= qmax:199            r = run + 1 if last == 0 else 1200            if r <= m:201                stack.append((p + p1, ql, p1, q1, p, q, 0, r))202        if qr <= qmax:203            r = run + 1 if last == 1 else 1204            if r <= m:205                stack.append((p + p2, qr, p, q, p2, q2, 1, r))206    return out207208def census(jmax2, jmax3, jcontrol, jcheck):209    qc = 2 ** jcontrol210    t0 = time.time()211    cum = census_walk(10 ** 9, qc)212    phi = totients(qc)213    tot = np.cumsum(phi)214    assert all(int(cum[2 ** j]) == int(tot[2 ** j]) for j in range(0, jcontrol + 1))215    print(f"control A = N to Q = 2^{jcontrol}: the walk's count is sum_(b <= Q) phi(b) at every Q = 2^j, {int(cum[qc])} at the top ({time.time() - t0:.1f}s)")216    qk = 2 ** jcheck217    for m in (2, 3):218        walked = {(p, q) for p, q, *_ in census_nodes(m, qk)} | {(0, 1)}219        brute = {(a, b) for b in range(1, qk + 1) for a in range(b) if math.gcd(a, b) == 1 and member(a, b, m)}220        ruled = {(a, b) for b in range(1, qk + 1) for a in range(b) if math.gcd(a, b) == 1 and rule(a, b, m)}221        assert walked == brute == ruled222        both = one = 0223        for p, q, p1, q1, p2, q2, last, run in census_nodes(m, qk):224            qs = quotients(p, q)225            an = qs[-1]226            assert run == an - 1227            older, younger = ((p1, q1), (p2, q2)) if last == 0 else ((p2, q2), (p1, q1))228            assert older[1] < younger[1] or (p, q) == (1, 2)229            with_younger = member(p + younger[0], q + younger[1], m)230            with_older = member(p + older[0], q + older[1], m)231            assert with_younger232            assert with_older == (an <= m)233            if with_older:234                both += 1235            else:236                one += 1237        print(f"m = {m}, b <= 2^{jcheck}: walk, either-expansion test and the rule a_1..a_(n-1) <= m, a_n <= m+1 agree on {len(walked)} fractions; mediant with the younger neighbour stays in {both + one} of {both + one}, with the older in {both}, exactly the nodes with a_n <= m, and refused in {one}, exactly a_n = m + 1")238    for m, jmax in ((2, jmax2), (3, jmax3)):239        qmax = 2 ** jmax240        t0 = time.time()241        cum = census_walk(m, qmax)242        elapsed = time.time() - t0243        d = 2 * DELTA[m]244        print(f"m = {m}, A = {{1..{m}}}, 2 delta = {d:.13f}, walk to Q = 2^{jmax}: {int(cum[qmax])} fractions in [0, 1) ({elapsed:.1f}s)")245        print("j  Q  N(Q)  Q^(2 delta)  N/Q^(2 delta)  octave exponent log2(N(2Q)/N(Q))  octave exponent minus 2 delta  two-octave exponent log4(N(4Q)/N(Q))  min and max of N/Q^(2 delta) on the quarter octaves of [j, j+1)")246        for j in range(0, jmax + 1):247            Q = 2 ** j248            n = int(cum[Q])249            power = Q ** d250            one = f"{math.log2(int(cum[2 * Q]) / n):.4f}" if j + 1 <= jmax else "-"251            dev = f"{math.log2(int(cum[2 * Q]) / n) - d:+.4f}" if j + 1 <= jmax else "-"252            two = f"{math.log2(int(cum[4 * Q]) / n) / 2:.4f}" if j + 2 <= jmax else "-"253            band = [int(cum[int(round(2 ** (j + k / 4)))]) / 2 ** ((j + k / 4) * d) for k in range(4) if j + k / 4 <= jmax]254            print(f"{j}  {Q}  {n}  {power:.3f}  {n / power:.5f}  {one}  {dev}  {two}  {min(band):.5f} {max(band):.5f}")255256# MAIN257258def main():259    ap = argparse.ArgumentParser()260    ap.add_argument("verb", choices=["crossings", "bridge", "census", "all"])261    ap.add_argument("--bound", type=int, default=60)262    ap.add_argument("--kmax", type=int, default=10)263    ap.add_argument("--nodes", type=int, default=200)264    ap.add_argument("--jmax2", type=int, default=24)265    ap.add_argument("--jmax3", type=int, default=18)266    ap.add_argument("--jcontrol", type=int, default=10)267    ap.add_argument("--jcheck", type=int, default=9)268    args = ap.parse_args()269    t0 = time.time()270    if args.verb in ("crossings", "all"):271        crossings(args.bound, [1, 2, 3, 10, 32, 42])272    if args.verb in ("bridge", "all"):273        bridge(args.kmax, args.nodes)274    if args.verb in ("census", "all"):275        census(args.jmax2, args.jmax3, args.jcontrol, args.jcheck)276    print(f"total {time.time() - t0:.1f}s", file=sys.stderr)277278if __name__ == "__main__":279    main()