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:])