memory.py

17.8 kB · python · 556 lines

1import csv2import math3import tempfile4import subprocess5import sys6import time7from itertools import permutations8from pathlib import Path910HERE = Path(__file__).resolve().parent11ROOT = HERE.parents[3]1213CASES = [(1, 1), (1, 2), (1, 3), (1, 4), (2, 1), (2, 2)]14A000616 = {1: 3, 2: 6, 3: 22, 4: 402}15NAMED = {16    (1, -1, -1): "golden",17    (1, -1, 0, -1): "supergolden",18    (1, 0, -1, -1): "plastic",19    (1, -1, -1, -1): "tribonacci",20}2122# WINDOWS2324def digits_of(w, d, k):25    a = 1 << d26    return [(w >> (d * (k - 1 - j))) & (a - 1) for j in range(k)]272829def word_of(ds, d):30    w = 031    for c in ds:32        w = (w << d) | c33    return w3435# GROUP3637def bd_apply(c, perm, flip, d):38    out = 039    for i in range(d):40        b = ((c >> i) & 1) ^ ((flip >> i) & 1)41        out |= b << perm[i]42    return out434445def window_tables(d, k, reversal):46    nw = 1 << (d * k)47    revs = (False, True) if reversal else (False,)48    tables = set()49    for perm in permutations(range(d)):50        for flip in range(1 << d):51            for rev in revs:52                t = []53                for w in range(nw):54                    ds = [bd_apply(c, perm, flip, d) for c in digits_of(w, d, k)]55                    if rev:56                        ds = ds[::-1]57                    t.append(word_of(ds, d))58                tables.add(tuple(t))59    return sorted(tables)606162def code_array(table, nbits):63    n = 1 << nbits64    arr = [0] * n65    for code in range(1, n):66        low = code & -code67        arr[code] = arr[code ^ low] | (1 << table[low.bit_length() - 1])68    return arr697071def orbit_walk(maps, ncodes):72    seen = bytearray(ncodes)73    reps, sizes = [], []74    for c in range(ncodes):75        if seen[c]:76            continue77        orb = {m[c] for m in maps}78        for x in orb:79            seen[x] = 180        reps.append(c)81        sizes.append(len(orb))82    return reps, sizes8384# TRANSFER MATRIX8586def transfer(code, d, k):87    a = 1 << d88    if k == 1:89        return [[bin(code).count("1")]]90    s_n = a ** (k - 1)91    m = [[0] * s_n for _ in range(s_n)]92    for s in range(s_n):93        for c in range(a):94            w = s * a + c95            if (code >> w) & 1:96                m[s][w % s_n] = 197    return m9899100def matmul(x, y):101    n = len(x)102    return [[sum(x[i][t] * y[t][j] for t in range(n)) for j in range(n)] for i in range(n)]103104105def charpoly(m):106    n = len(m)107    cs = [1]108    mj = [row[:] for row in m]109    for j in range(1, n + 1):110        tr = sum(mj[i][i] for i in range(n))111        cj = -tr // j112        cs.append(cj)113        if j < n:114            t = [row[:] for row in mj]115            for i in range(n):116                t[i][i] += cj117            mj = matmul(m, t)118    return tuple(cs)119120121def poly_str(cs):122    n = len(cs) - 1123    parts = []124    for i, c in enumerate(cs):125        if c == 0:126            continue127        e = n - i128        mono = "1" if e == 0 else ("x" if e == 1 else f"x^{e}")129        if abs(c) != 1 or e == 0:130            mono = f"{abs(c)}*{mono}" if e else f"{abs(c)}"131        parts.append(("- " if c < 0 else "+ ") + mono)132    s = " ".join(parts)133    return s[2:] if s.startswith("+ ") else "-" + s[2:]134135136def divides(p, q):137    p = list(p)138    q = list(q)139    while len(q) >= len(p):140        if q[0] % p[0]:141            return False142        f = q[0] // p[0]143        for i, c in enumerate(p):144            q[i] -= f * c145        if q[0] != 0:146            return False147        q.pop(0)148    return all(c == 0 for c in q)149150# PARI151152def pari_roots(polys, scratch):153    body = (154        "for(i = 1, #V, p = V[i]; f = factor(p); best = -1; bg = 0; "155        "for(j = 1, #f~, g = f[j, 1]; if(poldegree(g) > 0, r = polrootsreal(g); "156        "if(#r > 0, m = vecmax(r); if(m > best, best = m; bg = g)))); "157        "s = 0; r = polroots(bg); for(t = 1, #r, if(abs(r[t] - best) > 1e-25, s = max(s, abs(r[t])))); "158        'print(i, "|", Vec(bg), "|", best, "|", s))'159    )160    lines = [f"V = [{', '.join(poly_str(p) for p in polys)}];", body, "quit()"]161    src = scratch / "perron.gp"162    src.write_text("\n".join(lines) + "\n")163    out = subprocess.run(164        ["gp", "-q", str(src)],165        capture_output=True,166        text=True,167        stdin=subprocess.DEVNULL,168    )169    if out.returncode != 0:170        sys.exit(out.stderr)171    got = {}172    for line in out.stdout.strip().splitlines():173        idx, vec, val, sec = line.split("|")174        cs = tuple(int(t) for t in vec.strip().strip("[]").split(","))175        got[int(idx) - 1] = (cs, val.strip(), float(sec))176    return [got[i] for i in range(len(polys))]177178# CENSUS179180def burnside(d, k, reversal):181    total = 0182    tables = window_tables(d, k, reversal)183    for t in tables:184        seen = [False] * len(t)185        cycles = 0186        for w in range(len(t)):187            if seen[w]:188                continue189            cycles += 1190            x = w191            while not seen[x]:192                seen[x] = True193                x = t[x]194        total += 1 << cycles195    return total // len(tables)196197198def product_codes(d, k):199    a = 1 << d200    out = set()201    for f in range(1 << a):202        full = 0203        for w in range(1 << (d * k)):204            if all((f >> c) & 1 for c in digits_of(w, d, k)):205                full |= 1 << w206        out.add(full)207    return out208209210def main():211    scratch = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(tempfile.gettempdir())212    t_all = time.time()213    census, table = [], []214    for d, k in CASES:215        t0 = time.time()216        nbits = 1 << (d * k)217        ncodes = 1 << nbits218        maps_g = [code_array(t, nbits) for t in window_tables(d, k, True)]219        maps_b = [code_array(t, nbits) for t in window_tables(d, k, False)]220        reps, sizes = orbit_walk(maps_g, ncodes)221        reps_b, _ = orbit_walk(maps_b, ncodes)222        orbit_of = dict(zip(reps, sizes))223        products = product_codes(d, k)224        t_orbit = time.time() - t0225226        t0 = time.time()227        polys = {}228        for c in reps:229            polys.setdefault(charpoly(transfer(c, d, k)), []).append(c)230        t_poly = time.time() - t0231232        t0 = time.time()233        keys = sorted(polys)234        roots = pari_roots(keys, scratch)235        t_pari = time.time() - t0236237        rows = []238        strict = {}239        for key, (minp, val, sec) in zip(keys, roots):240            strict[minp] = sec241            for c in polys[key]:242                w = bin(c).count("1")243                rho = float(val)244                zero = w > 0 and divides(minp, [1] + [0] * (k - 1) + [-w])245                rows.append((c, w, minp, val, rho, zero))246        n_zero = sum(1 for r in rows if r[5])247        bad = [r for r in rows if r[5] and r[0] not in products]248        live = [r for r in rows if r[4] > 0]249        kappas = [math.log2(r[1]) / k - math.log2(r[4]) for r in live]250        k_max = max(kappas)251        tied = [r for kap, r in zip(kappas, live) if kap > k_max - 1e-12]252        rho1 = [r for r in live if abs(r[4] - 1.0) < 1e-12]253        by_poly = {}254        for c, w, minp, val, rho, zero in rows:255            e = by_poly.setdefault(minp, {"rho": val, "classes": 0, "rules": 0, "code": c, "w": w, "ks": []})256            e["classes"] += 1257            e["rules"] += orbit_of[c]258            if rho > 0 and w > 0:259                e["ks"].append(math.log2(w) / k - math.log2(rho))260            if c < e["code"]:261                e["code"], e["w"] = c, w262        weak = {263            m: e for m, e in by_poly.items() if float(e["rho"]) > 0 and strict[m] >= float(e["rho"]) - 1e-20264        }265        n_weak = len(weak)266        roots_seen = {m for m in by_poly if float(by_poly[m]["rho"]) > 0 and strict[m] < float(by_poly[m]["rho"]) - 1e-20}267        n_radical = 0268        for m in weak:269            step = 0270            for i, c in enumerate(m):271                if c:272                    step = math.gcd(step, len(m) - 1 - i)273            if step > 1 and tuple(m[i] for i in range(0, len(m), step)) in roots_seen:274                n_radical += 1275        entry = {276                "D": d,277                "k": k,278                "rules": ncodes,279                "classes_G": len(reps),280                "classes_B": len(reps_b),281                "group_G": len(maps_g),282                "group_B": len(maps_b),283                "charpolys": len(keys),284                "perron_polys": len(by_poly),285                "classes_kappa0": n_zero,286                "kappa0_nonproduct": len(bad),287                "kappa_min": round(min(kappas), 6),288                "kappa_max": round(k_max, 6),289                "kappa_max_code": min(r[0] for r in tied),290                "kappa_max_ties": len(tied),291                "kappa_max_windows": max(r[1] for r in tied),292                "rho1_windows": max((r[1] for r in rho1), default=0),293                "weak_perron_polys": n_weak,294                "weak_are_radicals": n_radical,295                "burnside_G": burnside(d, k, True),296                "burnside_B": burnside(d, k, False),297                "dead_classes": len(rows) - len(live),298                "a000616": A000616.get(d * k, ""),299                "t_orbit": round(t_orbit, 2),300                "t_poly": round(t_poly, 2),301                "t_pari": round(t_pari, 2),302        }303        census.append(entry)304        for minp, e in sorted(by_poly.items(), key=lambda kv: -float(kv[1]["rho"])):305            table.append(306                {307                    "D": d,308                    "k": k,309                    "minpoly": poly_str(minp),310                    "degree": len(minp) - 1,311                    "rho": e["rho"][:14],312                    "classes": e["classes"],313                    "rules": e["rules"],314                    "code": e["code"],315                    "windows": e["w"],316                    "kappa_min": round(min(e["ks"]), 6) if e["ks"] else "",317                    "kappa_max": round(max(e["ks"]), 6) if e["ks"] else "",318                    "strict": int(strict[minp] < float(e["rho"]) - 1e-20) if float(e["rho"]) > 0 else "",319                    "name": NAMED.get(minp, ""),320                }321            )322        print(323            f"D={d} k={k} rules={ncodes} classes(G)={len(reps)} classes(B)={len(reps_b)} "324            f"charpoly={len(keys)} perron={len(by_poly)} weak={n_weak} kappa0={n_zero} nonproduct={len(bad)} "325            f"kappa_max={round(k_max, 6)} ties={len(tied)} "326            f"orbit={t_orbit:.2f}s poly={t_poly:.2f}s pari={t_pari:.2f}s"327        )328        for r in bad:329            print(f"  NONPRODUCT kappa=0 code={r[0]} windows={r[1]} minpoly={poly_str(r[2])}")330331    with (HERE / "census.csv").open("w", newline="") as fh:332        w = csv.DictWriter(fh, fieldnames=list(census[0]))333        w.writeheader()334        w.writerows(census)335    with (HERE / "classes.csv").open("w", newline="") as fh:336        w = csv.DictWriter(fh, fieldnames=list(table[0]))337        w.writeheader()338        w.writerows(table)339    print(f"live classes {sum(r['classes_G'] - r['dead_classes'] for r in census)}")340    seen_polys = {}341    for r in table:342        seen_polys.setdefault((r["D"], r["k"]), set()).add(r["minpoly"])343    for a, b in (((1, 3), (1, 4)), ((1, 2), (1, 3)), ((1, 1), (1, 2)), ((2, 1), (2, 2))):344        miss = seen_polys[a] - seen_polys[b]345        print(f"nest {a} -> {b}: {len(seen_polys[a])} of {len(seen_polys[b])}, missing {len(miss)}")346    t0 = time.time()347    print("burnside D=1 G  " + ", ".join(str(burnside(1, k, True)) for k in range(1, 9)))348    print("burnside D=1 B  " + ", ".join(str(burnside(1, k, False)) for k in range(1, 9)))349    print("burnside D=2 G  " + ", ".join(str(burnside(2, k, True)) for k in range(1, 5)))350    print("burnside D=2 B  " + ", ".join(str(burnside(2, k, False)) for k in range(1, 5)))351    print(f"burnside extension {time.time() - t0:.2f}s")352    print(f"total {time.time() - t_all:.2f}s")353354# CLOSED FORM355356def classes_closed(k):357    if k % 2 == 0:358        m = k // 2359        return (360            (1 << (2 ** (2 * m) - 2))361            + (1 << (2 ** (2 * m - 1) - 2))362            + (1 << (2 ** (2 * m - 1) + 2 ** (m - 1) - 1))363        )364    m = (k - 1) // 2365    return (366        (1 << (2 ** (2 * m + 1) - 2))367        + (1 << (2 ** (2 * m) - 1))368        + (1 << (2 ** (2 * m) + 2 ** m - 2))369    )370371372def word_orbits(d, k):373    tables = window_tables(d, k, True)374    seen = [False] * (1 << (d * k))375    n = 0376    for w in range(len(seen)):377        if seen[w]:378            continue379        n += 1380        for t in tables:381            seen[t[w]] = True382    return n383384385def verb_burnside(top=11):386    t0 = time.time()387    cycle = [burnside(1, k, True) for k in range(1, top + 1)]388    closed = [classes_closed(k) for k in range(1, top + 1)]389    words = [word_orbits(1, k) for k in range(1, top + 1)]390    print("classes G_(1,k) k=1..8  " + ", ".join(str(v) for v in cycle[:8]))391    for k in range(9, top + 1):392        print(f"classes G_(1,{k})  {cycle[k - 1]}")393    agree = sum(1 for a, b in zip(cycle, closed) if a == b)394    print(f"closed form against the cycle index k=1..{top}: {agree} of {top} agree")395    for k in range(1, top + 1):396        if cycle[k - 1] != closed[k - 1]:397            print(f"  MISMATCH k={k}")398    print("digits k=9,10,11  " + ", ".join(str(len(str(cycle[k - 1]))) for k in (9, 10, 11)))399    print("words under the same group k=1..8  " + ", ".join(str(v) for v in words[:8]))400    print(f"burnside {time.time() - t0:.2f}s")401402# DETERMINANT403404def det_bareiss(m):405    n = len(m)406    a = [row[:] for row in m]407    sign = 1408    prev = 1409    for i in range(n - 1):410        if a[i][i] == 0:411            p = -1412            for r in range(i + 1, n):413                if a[r][i]:414                    p = r415                    break416            if p < 0:417                return 0418            a[i], a[p] = a[p], a[i]419            sign = -sign420        for r in range(i + 1, n):421            for c in range(i + 1, n):422                a[r][c] = (a[r][c] * a[i][i] - a[r][i] * a[i][c]) // prev423            a[r][i] = 0424        prev = a[i][i]425    return sign * a[n - 1][n - 1]426427428def perm_sign(p):429    seen = [False] * len(p)430    s = 1431    for i in range(len(p)):432        if seen[i]:433            continue434        j, c = i, 0435        while not seen[j]:436            seen[j] = True437            j = p[j]438            c += 1439        if c % 2 == 0:440            s = -s441    return s442443444def block_frame(k):445    s_n = 1 << (k - 1)446    h = 1 << (k - 2)447    rows, cols = [], []448    for s in range(h):449        rows += [s, s + h]450        cols += [(2 * s) % s_n, (2 * s + 1) % s_n]451    return rows, cols, perm_sign(rows) * perm_sign(cols)452453454def block_det(m, rows, cols, sign):455    d = 1456    for i in range(0, len(rows), 2):457        d *= m[rows[i]][cols[i]] * m[rows[i + 1]][cols[i + 1]] - m[rows[i]][cols[i + 1]] * m[rows[i + 1]][cols[i]]458        if d == 0:459            return 0460    return sign * d461462463def block_support(k):464    s_n = 1 << (k - 1)465    h = 1 << (k - 2)466    m = transfer((1 << (1 << k)) - 1, 1, k)467    cover = []468    for s in range(h):469        pair = {(2 * s) % s_n, (2 * s + 1) % s_n}470        for r in (s, s + h):471            if {c for c in range(s_n) if m[r][c]} - pair:472                return False473        cover += sorted(pair)474    return sorted(cover) == list(range(s_n))475476# PERRON477478def pari_minpolys(polys, scratch):479    body = (480        "for(i = 1, #V, p = V[i]; rho = vecmax(abs(polroots(p))); "481        "f = factor(p); g = 0; n = 0; w = 0; "482        "for(j = 1, #f~, h = f[j, 1]; if(poldegree(h) > 0, s = polrootsreal(h); "483        "if(#s > 0 && abs(vecmax(s) - rho) < 1e-20, g = h; n = n + 1; "484        "w = vecmax(abs(polroots(h)))))); "485        'print(i, "|", Vec(g), "|", n, "|", rho, "|", w))'486    )487    lines = [f"V = [{', '.join(poly_str(p) for p in polys)}];", body, "quit()"]488    src = scratch / "lemmas.gp"489    src.write_text("\n".join(lines) + "\n")490    out = subprocess.run(491        ["gp", "-q", str(src)],492        capture_output=True,493        text=True,494        stdin=subprocess.DEVNULL,495    )496    if out.returncode != 0:497        sys.exit(out.stderr)498    got = {}499    for line in out.stdout.strip().splitlines():500        idx, vec, hits, rho, wide = line.split("|")501        cs = tuple(int(t) for t in vec.strip().strip("[]").split(","))502        got[int(idx) - 1] = (cs, int(hits), rho.strip(), float(wide) - float(rho))503    return [got[i] for i in range(len(polys))]504505506def verb_lemmas(scratch):507    t0 = time.time()508    keys, dets = {}, {}509    for k in range(1, 5):510        ncodes = 1 << (1 << k)511        seen = set()512        if k < 2:513            for code in range(ncodes):514                seen.add(charpoly(transfer(code, 1, k)))515        else:516            rows, cols, sign = block_frame(k)517            small, agree = 0, 0518            for code in range(ncodes):519                m = transfer(code, 1, k)520                seen.add(charpoly(m))521                d = det_bareiss(m)522                small += d in (-1, 0, 1)523                agree += block_det(m, rows, cols, sign) == d524            dets[k] = (ncodes, small, agree, 1 << (k - 2), block_support(k))525        keys[k] = sorted(seen)526    for k in sorted(dets):527        ncodes, small, agree, blocks, support = dets[k]528        print(529            f"det k={k} rules={ncodes} blocks={blocks} det in [-1,0,1] {small} of {ncodes} "530            f"block product agrees {agree} of {ncodes} support={int(support)}"531        )532    minpolys, radii = [], []533    for k in range(1, 5):534        got = pari_minpolys(keys[k], scratch)535        ones = sum(1 for _, n, _, _ in got if n == 1)536        tight = sum(1 for _, _, _, gap in got if abs(gap) < 1e-20)537        mp = {cs for cs, _, _, _ in got}538        rr = {rho for _, _, rho, _ in got}539        minpolys.append(len(mp))540        radii.append(len(rr))541        print(542            f"perron k={k} charpolys={len(keys[k])} minpolys={len(mp)} distinct_rho={len(rr)} "543            f"unique factor {ones} of {len(got)} no conjugate above rho {tight} of {len(got)}"544        )545    print("minpolys " + ", ".join(str(v) for v in minpolys) + " and distinct rho " + ", ".join(str(v) for v in radii))546    print(f"lemmas {time.time() - t0:.2f}s")547548549if __name__ == "__main__":550    verb = sys.argv[1] if len(sys.argv) > 1 else ""551    if verb == "burnside":552        verb_burnside()553    elif verb == "lemmas":554        verb_lemmas(Path(sys.argv[2]) if len(sys.argv) > 2 else Path(tempfile.gettempdir()))555    else:556        main()