unequal_split.py

17.8 kB · python · 371 lines

1import math2import subprocess3import sys4import time5from bisect import bisect_right6from fractions import Fraction7from math import comb89import numpy as np1011K = 512HALF = Fraction(1, 2)13THIRD = Fraction(1, 3)14CHILDREN = (15    (HALF, (Fraction(0), Fraction(0))),16    (THIRD, (Fraction(2, 3), Fraction(0))),17    (THIRD, (Fraction(2, 3), Fraction(1, 3))),18    (THIRD, (Fraction(2, 3), Fraction(2, 3))),19    (THIRD, (Fraction(0), Fraction(2, 3))),20    (THIRD, (Fraction(1, 3), Fraction(2, 3))),21)22LN2, LN3 = math.log(2), math.log(3)23NUMER, DENOM = 65, 4124HEIGHT = 60.025TERMS = 1926UMAX = 300.027WINDOW = 10.028WINDOW_STARTS = (10.0, 20.0, 40.0, 80.0, 160.0, 290.0)29SPEC_LO = 50.030SPEC_STEP = 0.00231NBINS = 4032OMEGA_MAX = 600.033PEAKS = 1034TEXTBOOK = 0.7675115443 + 45.55415979j35TOLERANCE = 1e-436RIPPLE_BINS = 2437RIPPLE_PERIODS = 438RIPPLE_CENTRES = (((0.0, 0.0), 0.5, 2 / 3), ((1.0, 0.0), 1 / 3, 1 / 3), ((1.0, 1.0), 1 / 3, 1 / 3))39STEP = 2e-440T0 = time.time()4142def clock(label):43    print(f"[{time.time() - T0:6.1f}s] {label}")4445def gp(script):46    return subprocess.run(["gp", "-q"], input="default(realprecision, 60);\n" + script, capture_output=True, text=True, check=True).stdout4748def number(text):49    return float(text.strip().replace(" E", "E"))5051def dimension(k):52    return number(gp(f"print(solve(s = 0, 4, 2^-s + {k}*3^-s - 1))"))5354def left_edge(k):55    return number(gp(f"print(solve(s = -2, 4, {k}*3^-s - 1 - 2^-s))"))5657def overlap(a, b):58    (ra, (xa, ya)), (rb, (xb, yb)) = a, b59    return xa < xb + rb and xb < xa + ra and ya < yb + rb and yb < ya + ra6061def letter_grid():62    grid = [["." for _ in range(6)] for _ in range(6)]63    for index, (r, (x, y)) in enumerate(CHILDREN):64        n = int(r * 6)65        for i in range(n):66            for j in range(n):67                grid[int(y * 6) + j][int(x * 6) + i] = "H" if index == 0 else str(index)68    return ["".join(row) for row in reversed(grid)]6970def words(level):71    cells = [(Fraction(1), Fraction(0), Fraction(0), 0, 0)]72    for _ in range(level):73        cells = [(size * r, x + size * tx, y + size * ty, a + (r == HALF), b + (r == THIRD)) for size, x, y, a, b in cells for r, (tx, ty) in CHILDREN]74    return cells7576def render(level):77    side = 6**level78    image = np.zeros((side, side), dtype=np.int32)79    for size, x, y, _, _ in words(level):80        n, i, j = int(size * side), int(x * side), int(y * side)81        image[j:j + n, i:i + n] += 182    return image8384def census(depth):85    counts = {}86    frontier = [(Fraction(1), Fraction(0), Fraction(0), 0, 0)]87    for _ in range(depth + 1):88        nxt = []89        for size, x, y, a, b in frontier:90            counts[(a, b)] = counts.get((a, b), 0) + 191            if a + b < depth:92                nxt.extend((size * r, x + size * tx, y + size * ty, a + (r == HALF), b + (r == THIRD)) for r, (tx, ty) in CHILDREN)93        frontier = nxt94    return counts9596def patch():97    print("the letter on the 6-grid, H the 1/2 child, 1..5 the 1/3 children, . the gap")98    for row in letter_grid():99        print("  " + row)100    inside = all(0 <= x and x + r <= 1 and 0 <= y and y + r <= 1 for r, (x, y) in CHILDREN)101    disjoint = all(not overlap(CHILDREN[i], CHILDREN[j]) for i in range(6) for j in range(i + 1, 6))102    print(f"children inside the closed unit square: {inside}   open images pairwise disjoint (open set condition): {disjoint}")103    area = sum(r * r for r, _ in CHILDREN)104    print(f"area of the children {area} = 1/4 + {K}/9; gap {1 - area}")105    out = gp(f"D = solve(s = 0, 4, 2^-s + {K}*3^-s - 1); print(D); print(log(29)/log(6)); print(log(2)/log(3))").split()106    print(f"dimension D solving 2^-s + {K}*3^-s = 1: {out[0][:22]}")107    print(f"level render reading log(29)/log(6) = {out[1][:12]}   log 2/log 3 = {out[2][:12]}")108    for level in range(1, 5):109        image = render(level)110        fill = int((image > 0).sum())111        print(f"  level {level}: side 6^{level} = {6**level}, fill {fill} = (9 + 4*{K})^{level}: {fill == (9 + 4 * K) ** level}   max cover {image.max()}")112    tile = (render(1) > 0).astype(np.int32)113    kron = int((np.kron(tile, tile) != (render(2) > 0)).sum())114    print(f"level 2 render against the Kronecker square of the level 1 tile: {kron} of {36 * 36} cells differ")115    depth = 7116    counts = census(depth)117    good = all(counts[(a, b)] == comb(a + b, a) * K**b for (a, b) in counts)118    print(f"cell census to word length {depth}: {len(counts)} sizes 2^-a 3^-b, every count C(a+b, a) {K}^b: {good}")119    clock("patch")120121def moran(s, k):122    return 1 - np.exp(-s * LN2) - k * np.exp(-s * LN3)123124def moran_prime(s, k):125    return LN2 * np.exp(-s * LN2) + k * LN3 * np.exp(-s * LN3)126127def seeds(k, height):128    out = gp(f"r = polroots(1 - x^{DENOM} - {k}*x^{NUMER}); for(j = 1, #r, z = -{DENOM}*log(r[j])/log(2); print(real(z), \" \", imag(z)))")129    rows = np.array([[float(t) for t in line.split()] for line in out.strip().splitlines()])130    z = rows[:, 0] + 1j * rows[:, 1]131    period = 2 * math.pi * DENOM / LN2132    z = np.concatenate([z + 1j * period * m for m in (-1, 0, 1)])133    return z[np.abs(z.imag) <= height + 2]134135def newton(z, k):136    with np.errstate(all="ignore"):137        for _ in range(60):138            z = z - moran(z, k) / moran_prime(z, k)139    return z140141def roots_in_box(k, lo, hi, height):142    re = np.linspace(lo, hi, 9)143    im = np.linspace(-height, height, int(8 * height) + 1)144    grid = (re[:, None] + 1j * im[None, :]).ravel()145    lattice = seeds(k, height)146    z = newton(np.concatenate([lattice, grid]), k)147    with np.errstate(all="ignore"):148        good = np.isfinite(z) & (np.abs(moran(z, k)) < 1e-12) & (z.real >= lo) & (z.real <= hi) & (np.abs(z.imag) <= height)149    found = []150    for w in sorted(z[good], key=lambda w: (w.imag, w.real)):151        if all(abs(w - q) > 1e-8 for q in found):152            found.append(w)153    polished = newton(lattice, k)154    with np.errstate(all="ignore"):155        hit = np.isfinite(polished) & (np.abs(moran(polished, k)) < 1e-12) & (np.abs(polished.imag) <= height) & (polished.real >= lo) & (polished.real <= hi)156    from_lattice = sum(1 for w in found if np.min(np.abs(polished[hit] - w), initial=1.0) < 1e-8)157    return np.array(found), from_lattice158159def edge_floor(k, lo, hi, height):160    x = np.linspace(lo, hi, 4001)161    return float(np.abs(moran(x + 1j * height, k)).min())162163def winding(k, lo, hi, height, step):164    corners = [lo - 1j * height, hi - 1j * height, hi + 1j * height, lo + 1j * height]165    lipschitz = LN2 * 2 ** (-lo) + k * LN3 * 3 ** (-lo)166    turns, margin = 0.0, math.inf167    for a, b in zip(corners, corners[1:] + corners[:1]):168        n = int(math.ceil(abs(b - a) / step))169        path = a + (b - a) * np.arange(n + 1) / n170        values = moran(path, k)171        margin = min(margin, float((np.abs(values[:-1]) / (lipschitz * abs(b - a) / n)).min()))172        turns += float(np.angle(values[1:] / values[:-1]).sum())173    return int(round(turns / (2 * math.pi))), margin174175def box(k):176    d, edge = dimension(k), left_edge(k)177    lo, hi = edge - 0.25, d + 0.25178    height = HEIGHT179    while edge_floor(k, lo, hi, height) < 0.02:180        height += 0.5181    found, from_lattice = roots_in_box(k, lo, hi, height)182    count, margin = winding(k, lo, hi, height, STEP)183    return d, edge, lo, hi, height, found, from_lattice, count, margin184185def offsets(found):186    upper = found[found.imag > 1e-9]187    out = []188    for base in (2, 3, 6):189        omega = 2 * math.pi / math.log(base)190        ratio = upper.imag / omega191        out.append(math.floor(1000 * float(np.abs(ratio - np.round(ratio)).max())) / 1000)192    return out193194def convergents():195    script = "\n".join([196        f"k = {K}; D = solve(s = 0, 4, 2^-s + k*3^-s - 1); P = 2^-D; Q = k*3^-D; mu = P*log(2) + Q*log(3);",197        "mf(z) = 1 - 2^-z - k*3^-z;",198        "mg(z) = log(2)*2^-z + k*log(3)*3^-z;",199        f"c = contfrac(log(3)/log(2), , {TERMS}); p0 = 1; q0 = 0; p1 = c[1]; q1 = 1;",200        "for(j = 2, #c - 1, p2 = c[j]*p1 + p0; q2 = c[j]*q1 + q0; p0 = p1; q0 = q1; p1 = p2; q1 = q2; w = D + I*2*Pi*q1/log(2); for(i = 1, 80, w = w - mf(w)/mg(w)); th = 2*Pi*(q1*log(3)/log(2) - p1); pr = P*Q*log(2)^2*th^2/(2*mu^3); if(q1 > 1, print(q1, \" \", p1, \" \", imag(w), \" \", D - real(w), \" \", pr, \" \", abs(mf(w)))))",201    ])202    rows = [line.replace(" E", "E").split() for line in gp(script).strip().splitlines()]203    print(f"roots near D + 2 pi i q/ln 2 for the convergents p/q of log2(3), k = {K}: D - Re w against P Q (ln 2)^2 theta^2/(2 f'(D)^3), theta = 2 pi (q log2 3 - p)")204    for q, p, im, gap, pred, res in rows:205        print(f"  q {int(q):>7}  p {int(p):>7}  Im w {float(im):>14.6f}  D - Re w {float(gap):.6e}  law {float(pred):.6e}  ratio {float(gap) / float(pred):.6f}  |f(w)| {float(res):.0e}")206207def poles():208    print(f"complex dimensions: the zeros of 1 - 2^-s - k 3^-s in the box Re [D_l - 0.25, D + 0.25], Im [-T, T], T >= {HEIGHT}")209    print(f"seeds from PARI polroots of 1 - x^{DENOM} - k x^{NUMER} ({NUMER}/{DENOM} a convergent of log2 3), then Newton; the count certified by the winding number")210    print("   k        D        D_l      T   roots  winding  margin  from-lattice  Re min      Re max     offset 2pi/ln2 ln3 ln6, floored")211    worst = 0.0212    for k in range(1, 28):213        d, edge, lo, hi, height, found, from_lattice, count, margin = box(k)214        o = offsets(found)215        print(f"  {k:>2}  {d:.6f}  {edge:+.6f}  {height:5.1f}  {found.size:>4}  {count:>5}  {margin:7.1f}  {from_lattice:>5}       {found.real.min():+.6f}  {found.real.max():+.6f}   {o[0]:.3f} {o[1]:.3f} {o[2]:.3f}")216        worst = max(worst, found.real.min() - edge)217        if k == K:218            keep = found219        if k == 1:220            textbook = found[np.argmin(np.abs(found - TEXTBOOK))]221    print(f"largest gap from D_l to the lowest real part over k = 1..27, rounded up: {math.ceil(worst * 10000) / 10000:.4f}")222    z = complex(TEXTBOOK)223    for _ in range(60):224        z -= (1 - 2**-z - 2 ** (-485 * z / 306)) / (LN2 * 2**-z + 485 / 306 * LN2 * 2 ** (-485 * z / 306))225    print(f"k = 1, the 2-3 nonlattice equation: the printed root .7675115443 + 45.55415979 i; the root of its lattice approximant 1 - 2^-s - 2^(-485 s/306) {z.real:.10f} + {z.imag:.8f} i, at {abs(z - TEXTBOOK):.1e}; the true root {textbook.real:.10f} + {textbook.imag:.8f} i, at {abs(textbook - TEXTBOOK):.1e}")226    print(f"the first complex dimensions of the patch, k = {K}, Im >= 0:")227    for w in keep[keep.imag > -1e-9]:228        print(f"  {w.real:+.6f} {w.imag:+.6f} i")229    clock("poles")230    convergents()231    clock("convergents")232233def breakpoints(k, top):234    rows = []235    for b in range(int(top / LN3) + 1):236        for a in range(int((top - b * LN3) / LN2) + 1):237            rows.append((a * LN2 + b * LN3, a, b))238    rows.sort()239    exact = [2**a * 3**b for _, a, b in rows]240    ordered = all(x < y for x, y in zip(exact, exact[1:]))241    weights = [comb(a + b, a) * k**b for _, a, b in rows]242    totals, running = [], 0243    for w in weights:244        running += w245        totals.append(running)246    return np.array([u for u, _, _ in rows]), exact, totals, ordered247248def cells_at_least(exact, totals, x):249    i = bisect_right(exact, x)250    return totals[i - 1] if i else 0251252def renewal_check(k, exact, totals):253    return all(totals[j] == 1 + cells_at_least(exact, totals, Fraction(v, 2)) + k * cells_at_least(exact, totals, Fraction(v, 3)) for j, v in enumerate(exact))254255def normalized(u_breaks, log_totals, d, grid):256    i = np.searchsorted(u_breaks, grid, side="right") - 1257    return np.exp(log_totals[i] - d * grid)258259def fold(u, g, period, nbins):260    phase = np.floor((u % period) / period * nbins).astype(int)261    means = np.bincount(phase, weights=g, minlength=nbins) / np.maximum(np.bincount(phase, minlength=nbins), 1)262    centred = g - g.mean()263    return 1 - float(((g - means[phase]) ** 2).sum() / (centred**2).sum())264265def spectrum(u, g, top):266    y = (g - g.mean()) * np.blackman(g.size)267    pad = 1 << 21268    power = np.abs(np.fft.rfft(y, pad)) ** 2269    omega = 2 * np.pi * np.fft.rfftfreq(pad, u[1] - u[0])270    keep = (omega > 2.0) & (omega < OMEGA_MAX)271    w, pw = omega[keep], power[keep]272    peaks = np.where((pw[1:-1] > pw[:-2]) & (pw[1:-1] >= pw[2:]))[0] + 1273    peaks = peaks[np.argsort(pw[peaks])[::-1][:top]]274    amplitude = np.sqrt(pw[peaks]) / (np.blackman(g.size).sum() / 2)275    return w[peaks], amplitude276277def roots_to(k, height):278    found, _ = roots_in_box(k, left_edge(k) - 0.25, dimension(k) + 0.25, height)279    return found[found.imag > 0]280281def count():282    d = dimension(K)283    p, q = 2**-d, K * 3**-d284    mu = p * LN2 + q * LN3285    limit = 1 / (d * mu)286    u_breaks, exact, totals, ordered = breakpoints(K, UMAX)287    print(f"cells of size 2^-a 3^-b >= r = e^-U, U <= {UMAX:.0f}: {len(exact)} sizes, sorted exactly by 2^a 3^b: {ordered}")288    print(f"N(e^-U) = sum of C(a+b, a) {K}^b; N({UMAX:.0f}) has {len(str(totals[-1]))} digits")289    clock("breakpoints")290    print(f"renewal identity N(U) = 1 + N(U - ln 2) + {K} N(U - ln 3) exact at every breakpoint: {renewal_check(K, exact, totals)}")291    clock("renewal")292    log_totals = np.array([math.log(t) for t in totals])293    print(f"limit N(r) r^D -> C = 1/(D f'(D)), f'(D) = 2^-D ln 2 + {K} 3^-D ln 3 = {mu:.9f}: {limit:.9f}")294    print(f"   window U     min F/C     max F/C     mean F/C - 1    swing      swing sqrt(U)   carpet swing")295    dc = math.log(8) / LN3296    for start in WINDOW_STARTS:297        grid = np.linspace(start, start + WINDOW, 200001)298        f = normalized(u_breaks, log_totals, d, grid) / limit299        m = np.floor(grid / LN3)300        carpet = (8 ** (m + 1) - 1) / 7 * np.exp(-dc * grid)301        cswing = (carpet.max() - carpet.min()) / carpet.mean()302        swing = f.max() - f.min()303        print(f"  [{start:5.0f},{start + WINDOW:5.0f}]  {f.min():.6f}   {f.max():.6f}   {f.mean() - 1:+.6f}      {swing:.6f}   {swing * math.sqrt(start):.6f}       {cswing:.6f}")304    clock("windows")305    u = np.arange(SPEC_LO, UMAX, SPEC_STEP)306    g = np.log(normalized(u_breaks, log_totals, d, u))307    m = np.floor(u / LN3)308    gc = np.log((8 ** (m + 1) - 1) / 7) - dc * u309    print(f"folded variance of g(u) = ln N(e^-u) - D u on [{SPEC_LO:.0f}, {UMAX:.0f}], {NBINS} bins:   ln 2    ln 3    ln 6")310    print(f"  patch                                               {fold(u, g, LN2, NBINS):.3f}   {fold(u, g, LN3, NBINS):.3f}   {fold(u, g, math.log(6), NBINS):.3f}")311    print(f"  carpet control                                      {fold(u, gc, LN2, NBINS):.3f}   {fold(u, gc, LN3, NBINS):.3f}   {fold(u, gc, math.log(6), NBINS):.3f}")312    roots = roots_to(K, OMEGA_MAX + 5)313    print(f"Blackman periodogram of g on [{SPEC_LO:.0f}, {UMAX:.0f}], the {PEAKS} highest peaks in ({2.0}, {OMEGA_MAX:.0f}), each against the nearest complex dimension:")314    w, a = spectrum(u, g, PEAKS)315    taper = np.blackman(u.size)316    for wi, ai in sorted(zip(w, a)):317        near = roots[np.argmin(np.abs(roots.imag - wi))]318        decay = float((taper * np.exp(-(d - near.real) * u)).sum() / taper.sum())319        predicted = 2 * decay / (abs(near * moran_prime(near, K)) * limit)320        print(f"  peak {wi:8.3f}  amplitude {ai:.3e}  root {near.real:.6f} + {near.imag:.3f} i  D - Re {d - near.real:.2e}  offset {wi - near.imag:+.3f}  residue amplitude {predicted:.3e}")321    wc, ac = spectrum(u, gc, 6)322    print("carpet control peaks, as multiples of 2 pi/ln 3:", " ".join(f"{x / (2 * math.pi / LN3):.3f}" for x in sorted(wc)))323    clock("count")324325def ball_mass(cx, cy, radius, d):326    ratios = np.array([float(r) for r, _ in CHILDREN])327    shifts = np.array([[float(x), float(y)] for _, (x, y) in CHILDREN])328    weights = ratios**d329    x, y, size, mass = np.zeros(1), np.zeros(1), np.ones(1), np.ones(1)330    inside, r2 = 0.0, radius * radius331    while size.size:332        far = np.maximum(np.abs(x - cx), np.abs(x + size - cx)) ** 2 + np.maximum(np.abs(y - cy), np.abs(y + size - cy)) ** 2333        nx = np.clip(cx, x, x + size) - cx334        ny = np.clip(cy, y, y + size) - cy335        near = nx * nx + ny * ny336        full = far <= r2 * (1 - 1e-12)337        inside += float(mass[full].sum())338        open_ = ~full & (near < r2 * (1 + 1e-12))339        x, y, size, mass = x[open_], y[open_], size[open_], mass[open_]340        if size.size == 0 or size.max() < TOLERANCE * radius:341            return inside, inside + float(mass.sum())342        x = (x[:, None] + size[:, None] * shifts[None, :, 0]).ravel()343        y = (y[:, None] + size[:, None] * shifts[None, :, 1]).ravel()344        mass = (mass[:, None] * weights[None, :]).ravel()345        size = (size[:, None] * ratios[None, :]).ravel()346    return inside, inside347348def ripple():349    d = dimension(K)350    print(f"the spin mass M(r) = mu(B(c, r)) of the natural measure, weights 2^-D and 3^-D, D = {d:.9f}, enclosed to relative {TOLERANCE:g} by cells")351    print("   centre    map ratio  window   identity M(r) = ratio^D M(r/ratio)   ripple swing   bar     swing/bar  fold ln 2   fold ln 3   fold ln 6")352    for (cx, cy), ratio, window in RIPPLE_CENTRES:353        period = math.log(1 / ratio)354        steps = np.arange(RIPPLE_PERIODS * RIPPLE_BINS + RIPPLE_BINS) * period / RIPPLE_BINS355        radii = window * 0.999 * np.exp(-steps)356        bounds = np.array([ball_mass(cx, cy, r, d) for r in radii])357        mid = bounds.mean(axis=1)358        bar = float(np.max(np.log(bounds[:, 1] / bounds[:, 0])))359        lo, hi = bounds[RIPPLE_BINS:, 0], bounds[RIPPLE_BINS:, 1]360        scaled_lo, scaled_hi = ratio**d * bounds[:-RIPPLE_BINS, 0], ratio**d * bounds[:-RIPPLE_BINS, 1]361        meets = bool(np.all((lo <= scaled_hi * (1 + 1e-9)) & (scaled_lo <= hi * (1 + 1e-9))))362        g = np.log(mid) - d * np.log(radii)363        logr = np.log(radii)364        folds = [fold(logr, g, math.log(b), RIPPLE_BINS) for b in (2, 3, 6)]365        print(f"   ({cx},{cy})   1/{round(1 / ratio)}       {window:.4f}   bands meet at all {lo.size} shifted radii: {meets}          {g.max() - g.min():.5f}   {bar:.1e}   {math.floor((g.max() - g.min()) / bar):>6}     {folds[0]:.3f}       {folds[1]:.3f}       {folds[2]:.3f}")366    clock("ripple")367368VERBS = {"patch": patch, "poles": poles, "count": count, "ripple": ripple}369370if __name__ == "__main__":371    VERBS[sys.argv[1] if len(sys.argv) > 1 else "patch"]()