mobius_region.py

20.9 kB · python · 510 lines

1import itertools2import math3import sys4from fractions import Fraction56import numpy as np7from mpmath import mp89# DESIGNS1011def missing(q, a0):12    return q, tuple(d for d in range(q) if d != a0)1314def proper_sets(q):15    out = []16    for m in range(2, q):17        for F in itertools.combinations(range(q), m):18            out.append((q, F))19    return out2021def census():22    rows = []23    for q in (3, 4, 5):24        rows.extend(proper_sets(q))25    for a0 in range(10):26        rows.append(missing(10, a0))27    rows.append(missing(21, 0))28    return rows2930DEPTH = {3: (9, 8), 4: (7, 8), 5: (6, 8), 10: (5, 8), 21: (5, 8)}3132TGRID = [Fraction(1), Fraction(11, 10), Fraction(6, 5), Fraction(13, 10),33         Fraction(7, 5), Fraction(235, 154), Fraction(3, 2), Fraction(8, 5),34         Fraction(17, 10), Fraction(9, 5), Fraction(19, 10), Fraction(39, 20)]3536# SAFE ROUNDING3738def fdown(x, n=7):39    return math.floor(x * 10 ** n) / 10 ** n4041def fup(x, n=7):42    return math.ceil(x * 10 ** n) / 10 ** n4344FAILS = []4546def want(cond, name):47    if not cond:48        FAILS.append(name)49    return cond5051# THE MASS EXPONENT5253def alpha_bracket(q, k, den=10 ** 5):54    mp.dps = 4055    a = int(mp.floor(den * mp.log(k) / mp.log(q)))56    kb = k ** den57    while q ** (a + 1) <= kb:58        a += 159    while q ** a > kb:60        a -= 161    return Fraction(a, den), Fraction(a + 1, den)6263# THE WINDOW FACTORS6465def differences(F):66    k = len(F)67    mult = {}68    for f1 in F:69        for f2 in F:70            d = abs(f1 - f2)71            mult[d] = mult.get(d, 0) + 172    return sorted(mult.items()), k7374def window_factors(q, F, nd, m, chunk=1 << 17):75    dif, k = differences(F)76    lip = 2.0 * math.pi * sum(F) / k77    slack = lip / (2.0 * m * q ** nd)78    tol = 1e-1279    total = q ** nd80    gup = np.empty(total)81    glo = np.empty(total)82    off = (np.arange(m) + 0.5) / (m * q ** nd)83    for lo in range(0, total, chunk):84        hi = min(lo + chunk, total)85        t = np.arange(lo, hi, dtype=np.float64)[:, None] / q ** nd + off[None, :]86        acc = np.zeros_like(t)87        for d, c in dif:88            if d == 0:89                acc += c90            else:91                acc += c * np.cos(2.0 * math.pi * d * t)92        acc /= k * k93        gup[lo:hi] = np.sqrt(np.clip(acc + tol, 0.0, None)).max(axis=1) + slack94        glo[lo:hi] = np.clip(np.sqrt(np.clip(acc - tol, 0.0, None)).min(axis=1) - slack, 0.0, None)95    return gup, glo9697# THE TRANSFER ROOT9899def roots(G, q, nd, power=1.0, iters=400):100    S = q ** (nd - 1)101    W = (G.reshape(S, q) ** power) if power != 1.0 else G.reshape(S, q)102    tgt = ((np.arange(S, dtype=np.int64)[:, None] * q + np.arange(q)) % S).astype(np.int32)103    y = np.ones(S)104    for _ in range(iters):105        z = (W * y[tgt]).sum(axis=1)106        top = z.max()107        if top <= 0.0:108            return 0.0, 0.0109        y = z / top + 1e-30110    z = (W * y[tgt]).sum(axis=1)111    r = z / y112    return float(r.min()) * (1 - 1e-12), float(r.max()) * (1 + 1e-12)113114def exponent(mu, q):115    if mu <= 0.0:116        return None117    return math.log(mu) / math.log(q)118119# THE THRESHOLD FROM BELOW120121def beta_lower(glo, q, nd, ahi, target=0.25, tail=1.99, cap_cells=600):122    tailbound = (1.0 - ahi) / (2.0 - tail)123    work = [(1.0, tail)]124    done = []125    cells = 0126    while work:127        t0, t1 = work.pop()128        mu, _ = roots(glo, q, nd, power=t1)129        e = exponent(mu, q)130        cells += 1131        b = -1.0 if e is None else e / (2.0 - t0)132        if b > target or cells > cap_cells:133            done.append((t0, t1, b))134            if b <= target:135                return None, cells, tailbound, (t0, t1, b)136        else:137            mid = 0.5 * (t0 + t1)138            if mid - t0 < 1e-4:139                return None, cells, tailbound, (t0, t1, b)140            work.append((t0, mid))141            work.append((mid, t1))142    return min(min(r[2] for r in done), tailbound), cells, tailbound, None143144# THE THREE PARAMETERS145146def params(q, F, nd=None, m=None, tgrid=None):147    k = len(F)148    if nd is None:149        nd, m = DEPTH.get(q, (5, 8))150    if tgrid is None:151        tgrid = TGRID if q <= 10 else TGRID[:1]152    alo, ahi = alpha_bracket(q, k)153    gup, glo = window_factors(q, F, nd, m)154    _, muup = roots(gup, q, nd)155    mulo, _ = roots(glo, q, nd)156    a1hi = exponent(muup, q)157    a1lo = exponent(mulo, q)158    floor = 1.0 - float(ahi)159    if a1lo is None or a1lo < floor:160        a1lo = floor161    mts = []162    for t in tgrid:163        _, mu = roots(gup, q, nd, power=float(t))164        mt = exponent(mu, q)165        mts.append((t, mt, mt / float(2 - t)))166    tbest, mtbest, bhi = min(mts, key=lambda r: r[2])167    return {"q": q, "F": F, "k": k, "nd": nd, "m": m,168            "alo": float(alo), "ahi": float(ahi),169            "a1lo": a1lo, "a1hi": a1hi,170            "bhi": bhi, "tbest": tbest, "mtbest": mtbest,171            "bfloor": 1.0 - float(ahi), "mts": mts,172            "gup": gup, "glo": glo}173174def name_of(q, F):175    if len(F) == q - 1:176        return "missing " + str(next(d for d in range(q) if d not in F))177    return "{" + ",".join(str(d) for d in F) + "}"178179# THE CRITERION180181CONDS = ["L1", "C1", "C2", "L2", "L3", "L4", "L5"]182183def cap(name, alpha, a1):184    if name == "C1":185        return Fraction(1, 4)186    if name == "C2":187        return Fraction(2, 5) * (1 - a1)188    if name == "L2":189        return (1 - a1) / (2 * (2 - alpha))190    if name == "L4":191        return (1 + alpha / 2) / 5192    if name == "L5":193        return (1 - a1) * (1 - a1 + alpha / 2) / 2194    if name == "L3":195        if a1 < Fraction(1, 3):196            return None197        u = min(Fraction(1), 2 * a1 / alpha)198        return u * alpha / (4 * (3 * a1 - 1 + u * (1 - a1)))199    raise ValueError(name)200201def beta_cap(alpha, a1):202    best = None203    who = None204    for name in CONDS[1:]:205        c = cap(name, alpha, a1)206        if c is None:207            continue208        if best is None or c < best:209            best, who = c, name210    return best, who211212def verdict(alpha, a1, beta):213    rows = []214    rows.append(("L1", 2 * a1, alpha, a1 < alpha / 2))215    for name in CONDS[1:]:216        c = cap(name, alpha, a1)217        if c is None:218            rows.append((name, beta, None, True))219        else:220            ok = beta <= c if name in ("C1", "C2") else beta < c221            rows.append((name, beta, c, ok))222    ok = all(r[3] for r in rows) and a1 < Fraction(1, 2)223    binder = None224    slack = None225    for name, left, right, good in rows:226        if right is None:227            continue228        s = float(right) - float(left)229        if slack is None or s < slack:230            slack, binder = s, name231    return rows, ok, binder, slack232233# VERB PARAMS234235def verb_params(argv):236    q = int(argv[0])237    F = tuple(int(c, 36) for c in argv[1])238    nd = int(argv[2]) if len(argv) > 2 else None239    m = int(argv[3]) if len(argv) > 3 else None240    p = params(q, F, nd, m)241    print(f"q = {p['q']}  F = {sorted(p['F'])}  k = {p['k']}  window {p['nd']} digits, sub-scan {p['m']}")242    print(f"alpha in [{fdown(p['alo'])}, {fup(p['ahi'])}]  (integer comparison, k^b against q^a, b = 10^5)")243    print(f"alpha_1 in [{fdown(p['a1lo'])}, {fup(p['a1hi'])}]  (lower from the inf window and the l^1 floor, upper from the sup window)")244    print(f"beta <= {fup(p['bhi'])} at t = {p['tbest']}, m_t <= {fup(p['mtbest'])}; beta >= {fdown(p['bfloor'])} by Parseval")245246# VERB CRITERION247248def frac_up(x, den=10 ** 7):249    return Fraction(math.ceil(x * den), den)250251def frac_down(x, den=10 ** 7):252    return Fraction(math.floor(x * den), den)253254def decide(p, betalo):255    pes = verdict(frac_down(p["alo"]), frac_up(p["a1hi"]), frac_up(p["bhi"]))256    opt = verdict(frac_up(p["ahi"]), frac_down(p["a1lo"]), frac_down(betalo))257    holds = pes[1]258    dead = [r[0] for r in opt[0] if not r[3]]259    if not (frac_down(p["a1lo"]) < Fraction(1, 2)):260        dead.append("LAT")261    return holds, dead, pes262263def verb_criterion(_argv):264    print("q  design         k  alpha        alpha_1 in                beta in                   verdict   binds                     GRH theta <=")265    out = []266    for q, F in census():267        p = params(q, F)268        betalo = 1.0 - float(p["ahi"])269        cert = ""270        if betalo <= 0.25 and 2.0 * p["a1lo"] < float(p["ahi"]):271            lo, cells, _, _ = beta_lower(p["glo"], q, p["nd"], float(p["ahi"]))272            if lo is not None:273                betalo, cert = lo, f" ({cells} cells)"274        holds, dead, pes = decide(p, betalo)275        tag = "HOLDS" if holds else ("REFUTED" if dead else "open")276        grh = f"{fup(max(1.0 - (0.25 - p['a1hi']) / p['alo'], 1.0 - (0.25 - p['a1hi']) / p['ahi']))}"277        binds = ",".join(dead) if dead else (pes[2] or "-")278        print(f"{q:<3}{name_of(q, F):<15}{p['k']:<3}{fdown(p['alo'], 6):<13}"279              f"[{fdown(p['a1lo'], 6)}, {fup(p['a1hi'], 6)}]{'':<6}"280              f"[{fdown(betalo, 6)}, {fup(p['bhi'], 6)}]{'':<5}{tag:<10}{binds:<26}{grh}{cert}")281        out.append((q, F, p, tag, dead))282    print()283    print("Every alpha and every lower bound truncates down, every upper bound rounds up.")284    print("HOLDS uses the pessimistic corner (alpha low, alpha_1 high, beta high); REFUTED uses the optimistic one; open is neither.")285    print("A beta lower bound is 1 - alpha by Parseval, or the certified cell chain where that does not decide.")286    for tag in ("HOLDS", "REFUTED", "open"):287        rows = [r for r in out if r[3] == tag]288        print(f"{tag}: {len(rows)} of {len(out)} designs" + ("" if tag != "HOLDS" else ": " + ", ".join(f"q={r[0]} {name_of(r[0], r[1])}" for r in rows)))289    counts = {}290    for _, _, _, tag, dead in out:291        if tag == "REFUTED":292            counts[",".join(dead)] = counts.get(",".join(dead), 0) + 1293    for key in sorted(counts):294        print(f"  refuted on {key}: {counts[key]} designs")295    best = min((r for r in out if r[0] == 10), key=lambda r: r[2]["bhi"])296    worst = max((r for r in out if r[0] == 10), key=lambda r: r[2]["bhi"])297    print(f"base 10 cheapest column {name_of(10, best[1])}: beta <= {fup(best[2]['bhi'])}, over the bar by {fdown(best[2]['bhi'] - 0.25)}")298    print(f"base 10 dearest column {name_of(10, worst[1])}: beta <= {fup(worst[2]['bhi'])}, over the bar by {fdown(worst[2]['bhi'] - 0.25)}")299300# VERB THRESHOLD301302def verb_threshold(argv):303    target = 0.25304    if argv and len(argv) == 1:305        target = float(Fraction(argv[0]))306        argv = []307    elif len(argv) >= 3:308        target = float(Fraction(argv[2]))309    fams = [(10, tuple(d for d in range(10) if d != a0)) for a0 in range(10)] if not argv \310        else [(int(argv[0]), tuple(int(c, 36) for c in argv[1]))]311    print("The exceptional-set threshold from below: m_t is non-increasing in t and m_2 = 1 - alpha exactly,")312    print("so on a cell [t0, t1] every t has m_t/(2 - t) >= m_(t1)/(2 - t0), and above the cut the Parseval value alone decides.")313    print(f"target {fdown(target)}")314    print("design            alpha_1 <=   beta <=      beta >=      cells  tail cut  verdict")315    for q, F in fams:316        p = params(q, F)317        lo, cells, tb, bad = beta_lower(p["glo"], q, p["nd"], float(p["ahi"]), target=target)318        if lo is None:319            note = f"undecided at the target, t in [{bad[0]:.4f}, {bad[1]:.4f}]"320            shown = "-"321        else:322            note = f"REFUTED, no admissible beta clears {fdown(target)}" if lo > target else "open"323            shown = str(fdown(lo))324        print(f"{q} {name_of(q, F):<15} {fup(p['a1hi']):<12} {fup(p['bhi']):<12} {shown:<12} {cells:<6} {tb:<9.4f} {note}")325    print()326    print("A lower bound truncates down and an upper bound rounds up; the lower bound comes from the infimum window,")327    print("which is a lower bound on the grid moment and so on the sup-over-shift one.")328329# VERB REGION330331def cross(alpha, lo, hi, name, target=Fraction(1, 4), steps=60):332    for _ in range(steps):333        mid = (lo + hi) / 2334        c = cap(name, alpha, mid)335        if c is not None and c < target:336            hi = mid337        else:338            lo = mid339    return hi340341def verb_region(argv):342    alphas = [Fraction(3, 4), Fraction(4, 5), Fraction(9, 10), Fraction(19, 20), Fraction(98397, 10 ** 5)]343    if argv:344        alphas = [Fraction(argv[0])]345    print("The region at fixed alpha, in the (alpha_1, beta) plane: the wall L1 puts a ceiling on alpha_1 and the six caps put one on beta.")346    print("alpha        L1 wall      floor 1-alpha  C1 cut at    C2 cut at    L2 cut at    L3 cut at    L5 cut at    beta at the wall")347    for alpha in alphas:348        top = min(alpha / 2, Fraction(1, 2))349        cuts = {}350        for name in ("C2", "L2", "L3", "L5"):351            c0 = cap(name, alpha, Fraction(0))352            ct = cap(name, alpha, top)353            cuts[name] = "never" if (ct is None or ct >= Fraction(1, 4)) else fdown(float(cross(alpha, Fraction(0), top, name)), 6)354        wall, who = beta_cap(alpha, top)355        print(f"{fdown(float(alpha), 6):<12} {fdown(float(top), 6):<12} {fdown(1 - float(alpha), 6):<14} "356              f"{'0':<12} {str(cuts['C2']):<12} {str(cuts['L2']):<12} {str(cuts['L3']):<12} {str(cuts['L5']):<12} {fdown(float(wall), 6)} by {who}")357    print()358    print("The boundary itself, printed at nine stations from the origin to the wall:")359    for alpha in alphas:360        top = min(alpha / 2, Fraction(1, 2))361        print(f"alpha = {fdown(float(alpha), 6)}")362        for i in range(9):363            a1 = top * i / 8364            c, who = beta_cap(alpha, a1)365            reach = "design" if a1 >= 1 - alpha and c >= 1 - alpha else "-"366            print(f"  alpha_1 {fdown(float(a1), 6):<11} beta <= {fdown(float(c), 6):<11} {who:<4} {reach}")367    print()368    print("A cap is an upper limit on beta and prints truncated down; a design also obeys alpha_1 >= 1 - alpha and beta >= 1 - alpha.")369370# VERB BOUNDARY371372def verb_boundary(_argv):373    print("Which of the seven inequalities can bind. Each cap is monotone in alpha_1, so its minimum over the region sits at the wall alpha_1 = alpha/2.")374    print("alpha        C1       C2 at wall   L2 at wall   L3 at wall   L4          L5 at wall")375    for na in (68, 70, 75, 80, 85, 90, 95, 98397 / 1000, 99, 999 / 10):376        alpha = Fraction(int(round(na * 1000)), 100000)377        top = min(alpha / 2, Fraction(1, 2))378        line = f"{fdown(float(alpha), 6):<12} {'0.25':<8}"379        for name in ("C2", "L2", "L3", "L4", "L5"):380            c = cap(name, alpha, top)381            line += f" {fdown(float(c), 6):<12}"382        print(line)383    print()384    print("L2 and L3 read exactly 1/4 at the wall at every alpha, L5 reads (2 - alpha)/4 and L4 reads (1 + alpha/2)/5,")385    print("so inside the wall only C1 and C2 ever cut below 1/4, and C2 cuts exactly from alpha_1 = 3/8.")386    bad = []387    exact = []388    for na in range(670, 1000):389        alpha = Fraction(na, 1000)390        top = min(alpha / 2, Fraction(1, 2))391        for i in range(1, 201):392            a1 = top * i / 200393            for name in ("L2", "L3", "L4", "L5"):394                c = cap(name, alpha, a1)395                if c is not None and c < Fraction(1, 4):396                    bad.append((alpha, a1, name, c))397        for name in ("L2", "L3"):398            c = cap(name, alpha, top)399            exact.append(c == Fraction(1, 4))400    print(f"sweep of alpha in [67/100, 999/1000] by 1/1000 and alpha_1 in (0, alpha/2] by alpha/400: "401          f"{len(bad)} cells where L2, L3, L4 or L5 falls below 1/4, out of {330 * 200 * 4}")402    print(f"L2 and L3 equal 1/4 at the wall in {sum(exact)} of {len(exact)} exact rational tests")403    want(len(bad) == 0, "no inactive cap dips")404    want(all(exact), "L2 and L3 meet the corner")405    print()406    print("So the region is exactly alpha_1 < alpha/2 and beta <= min(1/4, (2/5)(1 - alpha_1)), with alpha_1 >= 1 - alpha and beta >= 1 - alpha at a design:")407    print("alpha        design alpha_1 window        beta window            non-empty")408    for na in (60, 66, 67, 70, 75, 80, 90, 95, 98397 / 1000):409        alpha = Fraction(int(round(na * 1000)), 100000)410        top = min(alpha / 2, Fraction(1, 2))411        fl = 1 - alpha412        c, _ = beta_cap(alpha, fl)413        ok = fl < top and fl <= c414        print(f"{fdown(float(alpha), 6):<12} [{fdown(float(fl), 6)}, {fdown(float(top), 6)})       "415              f"[{fdown(float(fl), 6)}, {fdown(float(c), 6)}]      {'yes' if ok else 'no'}")416    print()417    print("The single-window branch, read with the step beta <= m_1 <= alpha_1 that t = 1 in the infimum and one shift of the supremum give:")418    bmax = Fraction(1) / (1 + Fraction(13, 4))419    amin = 1 - bmax420    print(f"  alpha_1 <= 1 - (13/4) beta against beta <= alpha_1 asks beta <= {bmax} = {fup(float(bmax))}, under the {Fraction(1, 4)} it replaces")421    print(f"  with the Parseval floor beta >= 1 - alpha that asks alpha >= {amin} = {fdown(float(amin), 6)}, over the {Fraction(3, 4)} the window cap asks: the branch narrows the route")422    want(bmax < Fraction(1, 4), "branch cap under a quarter")423    want(amin > Fraction(3, 4), "branch gate over three quarters")424    alpha, a1, beta = Fraction(9, 10), Fraction(77, 500), Fraction(13, 50)425    rows = [("floor alpha_1", a1 >= 1 - alpha), ("floor beta", beta >= 1 - alpha), ("L1", 2 * a1 < alpha),426            ("branch", a1 <= 1 - Fraction(13, 4) * beta)]427    for name in ("C2", "L2", "L3", "L4", "L5"):428        c = cap(name, alpha, a1)429        rows.append((name, True if c is None else (beta <= c if name == "C2" else beta < c)))430    print(f"  the step is load-bearing: without it alpha = {fdown(float(alpha), 6)}, alpha_1 = {fdown(float(a1), 6)}, beta = {fdown(float(beta), 6)} > 1/4 passes " + ", ".join(n for n, ok in rows if ok))431    want(all(ok for _, ok in rows) and beta > Fraction(1, 4), "the load-bearing witness")432    print()433    print("FAILS: " + (", ".join(FAILS) if FAILS else "none"))434    if FAILS:435        raise SystemExit(1)436437# VERB CHECK438439def verb_check(_argv):440    print("Parseval anchor, its expectation from unique base-q expansion and not from the sweep:")441    for q, F in ((3, (0, 1)), (5, (0, 1, 2, 3)), (10, tuple(d for d in range(10) if d != 5))):442        k = len(F)443        for L in (2, 3):444            x = q ** L445            a = np.arange(x)446            t = a / x447            v = np.ones(x)448            for j in range(L):449                s = np.zeros(x, dtype=complex)450                for f in F:451                    s += np.exp(2j * math.pi * f * (q ** j) * t)452                v *= np.abs(s) / k453            got = float((v ** 2).sum())454            wantv = (q / k) ** L455            print(f"  q = {q}, k = {k}, L = {L}: sum of F^2 = {got:.9f} against (q/k)^L = {wantv:.9f}")456            want(abs(got - wantv) < 1e-6 * wantv, f"parseval {q} {L}")457    print()458    print("The two floors, each an upper bound on nothing and a lower bound on both parameters:")459    for q, F in ((3, (0, 1)), (5, (0, 1, 2, 3)), (10, tuple(d for d in range(10) if d != 5)), (21, tuple(range(1, 21)))):460        p = params(q, F, tgrid=TGRID[:6] if q <= 10 else TGRID[:1])461        fl = 1.0 - float(p["ahi"])462        print(f"  q = {q}, k = {p['k']}: 1 - alpha <= {fup(fl)}, alpha_1 <= {fup(p['a1hi'])}, beta <= {fup(p['bhi'])}")463        want(p["a1hi"] >= fl - 1e-9, f"l1 floor {q}")464        want(p["bhi"] >= fl - 1e-9, f"beta floor {q}")465    print()466    print("Both window bounds against an exact grid sum, two inequalities the construction forces term by term:")467    for q, F, L in ((3, (0, 1), 6), (5, (0, 1, 2, 3), 5), (10, tuple(d for d in range(10) if d != 5), 4)):468        k = len(F)469        x = q ** L470        t = np.arange(x) / x471        v = np.ones(x)472        for j in range(L):473            s2 = np.zeros(x, dtype=complex)474            for f in F:475                s2 += np.exp(2j * math.pi * f * (q ** j) * t)476            v *= np.abs(s2) / k477        exact = float(v.sum())478        nd, m = DEPTH[q]479        gup, glo = window_factors(q, F, nd, m)480        a = np.arange(x, dtype=np.int64)481        up = np.ones(x)482        dn2 = np.ones(x)483        for j in range(L):484            idx = (a * q ** j % x) * q ** nd // x485            up *= gup[idx]486            dn2 *= glo[idx]487        path = float(up.sum())488        pathlo = float(dn2.sum())489        print(f"  q = {q}, k = {k}, L = {L}: infimum path {pathlo:.6f} at most exact grid {exact:.6f} at most supremum path {path:.6f}, ratios {pathlo / exact:.6f} and {exact / path:.6f}")490        want(exact <= path, f"grid under sup window {q}")491        want(pathlo <= exact, f"inf window under grid {q}")492    print()493    print("The q = 21 recompute from scratch, against the quarter bar:")494    p = params(21, tuple(range(1, 21)), tgrid=TGRID[:1])495    print(f"  q = 21 missing 0, window {p['nd']} digits, sub-scan {p['m']}: alpha_1 in [{fdown(p['a1lo'])}, {fup(p['a1hi'])}]")496    print(f"  clears 1/4 by {fdown(0.25 - p['a1hi'])}; beta <= alpha_1 at t = 1, so the exceptional-set threshold clears too")497    want(p["a1hi"] < 0.25, "q21 clears")498    print()499    print("FAILS: " + (", ".join(FAILS) if FAILS else "none"))500    if FAILS:501        raise SystemExit(1)502503VERBS = {"params": verb_params, "criterion": verb_criterion, "region": verb_region,504         "boundary": verb_boundary, "threshold": verb_threshold, "check": verb_check}505506if __name__ == "__main__":507    if len(sys.argv) < 2 or sys.argv[1] not in VERBS:508        print("verbs: " + " ".join(sorted(VERBS)))509        raise SystemExit(2)510    VERBS[sys.argv[1]](sys.argv[2:])