toolpath.py

10.7 kB · python · 331 lines

1import sys2import time34U = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)]5F, R, M, MR = 0, 1, 2, 36REV = [R, F, MR, M]7MIR = [M, MR, F, R]8NAMES = "F R M MR".split()910def norm(c):11    a, b = c12    return a * a + a * b + b * b1314def mul(p, q):15    x, y = p16    s, t = q17    return (x * s - y * t, x * t + y * s + y * t)1819def mirror_index(c):20    c2 = mul(c, c)21    n = norm(c)22    for m in range(6):23        if (U[m][0] * n, U[m][1] * n) == c2:24            return m25    return None2627def turn(d1, d2):28    t = (d2 - d1) % 629    return t - 6 if t > 3 else t3031def walks(n, c, gentle):32    out = []33    ta, tb = c34    def dfs(path, seen, a, b, d):35        k = len(path)36        if k == n:37            if (a, b) == (ta, tb):38                out.append(tuple(path))39            return40        rem = n - k - 141        for nd in range(6):42            if k:43                t = abs(turn(d, nd))44                if t == 3 or (gentle and t > 1):45                    continue46            x, y = a + U[nd][0], b + U[nd][1]47            dx, dy = ta - x, tb - y48            if max(abs(dx), abs(dy), abs(dx + dy)) > rem:49                continue50            if (x, y) in seen:51                continue52            seen.add((x, y))53            path.append(nd)54            dfs(path, seen, x, y, nd)55            path.pop()56            seen.discard((x, y))57    dfs([], {(0, 0)}, 0, 0, None)58    return out5960def copy(d, f, g, fl, m):61    n = len(g)62    if f == F:63        return [((d + g[j]) % 6, fl[j]) for j in range(n)]64    if f == R:65        return [((d + g[n - 1 - j]) % 6, REV[fl[n - 1 - j]]) for j in range(n)]66    if f == M:67        return [((d + m - g[j]) % 6, MIR[fl[j]]) for j in range(n)]68    return [((d + m - g[n - 1 - j]) % 6, REV[MIR[fl[n - 1 - j]]]) for j in range(n)]6970def expand(seq, g, fl, m):71    out = []72    for d, f in seq:73        out.extend(copy(d, f, g, fl, m))74    return out7576def points(seq):77    a = b = 078    P = [(0, 0)]79    for d, _ in seq:80        a, b = a + U[d][0], b + U[d][1]81        P.append((a, b))82    return P8384def avoids(seq):85    a = b = 086    seen = {(0, 0)}87    for d, _ in seq:88        a, b = a + U[d][0], b + U[d][1]89        if (a, b) in seen:90            return False91        seen.add((a, b))92    return True9394def counts(seq):95    c = [0, 0, 0, 0]96    for (d1, _), (d2, _) in zip(seq, seq[1:]):97        c[abs(turn(d1, d2))] += 198    return c99100def ends(g, fl, m):101    out = {}102    for f in (F, R, M, MR):103        if m is None and f >= M:104            continue105        s = copy(0, f, g, fl, m)106        out[f] = (s[0][0], s[0][1], s[-1][0], s[-1][1])107    return out108109def orbit(t, f, h, E):110    seen = set()111    x = (t % 6, f, h)112    worst = 0113    while x not in seen:114        seen.add(x)115        worst = max(worst, abs(turn(0, x[0])))116        t, f, h = x117        x = ((t + E[h][0] - E[f][2]) % 6, E[f][3], E[h][1])118    return worst, seen119120def closure(g, fl, m):121    E = ends(g, fl, m)122    types = set()123    flags = set(fl)124    todo = [(g[j + 1] - g[j], fl[j], fl[j + 1]) for j in range(len(g) - 1)]125    fdone = set()126    while todo or flags - fdone:127        while todo:128            x = todo.pop()129            x = (x[0] % 6, x[1], x[2])130            if x in types:131                continue132            types.add(x)133            t, f, h = x134            todo.append(((t + E[h][0] - E[f][2]) % 6, E[f][3], E[h][1]))135        for f in list(flags - fdone):136            fdone.add(f)137            s = copy(0, f, g, fl, m)138            flags.update(x[1] for x in s)139            todo.extend((b[0] - a[0], a[1], b[1]) for a, b in zip(s, s[1:]))140    return max(abs(turn(0, t)) for t, _, _ in types)141142def flag_search(g, flags, m, gentle=True):143    n = len(g)144    shapes = {}145    for d in range(6):146        for f in flags:147            steps = copy(d, f, g, [F] * n, m)148            shapes[(d, f)] = (points(steps)[1:], steps[0][0], steps[-1][0])149    tj = [(g[j + 1] - g[j]) % 6 for j in range(n - 1)]150    res = []151    for f0 in flags:152        for fz in flags:153            proto = [f0] + [F] * (n - 2) + [fz]154            E = ends(g, proto, m)155            good = {(t, f, h): not gentle or orbit(t, f, h, E)[0] <= 1 for t in range(6) for f in flags for h in flags}156            def dfs(j, fl, used, o, last):157                if j == n:158                    res.append(tuple(fl))159                    return160                cand = [f0] if j == 0 else ([fz] if j == n - 1 else flags)161                for f in cand:162                    if j and not good[(tj[j - 1], fl[-1], f)]:163                        continue164                    K, first, end = shapes[(g[j], f)]165                    if last is not None and abs(turn(last, first)) > (1 if gentle else 2):166                        continue167                    P = [(o[0] + p[0], o[1] + p[1]) for p in K]168                    if any(p in used for p in P):169                        continue170                    used.update(P)171                    fl.append(f)172                    dfs(j + 1, fl, used, P[-1], end)173                    fl.pop()174                    used.difference_update(P)175            dfs(0, [], {(0, 0)}, (0, 0), None)176    return res177178def canon(g, fl, mm):179    orb = [(g, fl), (g[::-1], fl[::-1])]180    if mm is not None:181        h = tuple((mm - d) % 6 for d in g)182        orb += [(h, fl), (h[::-1], fl[::-1])]183    return min(orb)184185A229214 = (1, 2, -1, 3, 1, 1, -3, 1, 2, 2, -1, -2, 3, 2, 3, -1, -1, -3, 1, -2, -1, 3, -1, -3, -2, 3, 3, 2, 1, 2, -1, 3, 1, 1, -3, 1, 2, -1, 3, 1, 1, -3, -2, -3, -3, 2, 3, 1, -3)186187CHORDS = [(3, (1, 1)), (4, (2, 0)), (7, (2, 1)), (9, (3, 0)), (12, (2, 2)), (13, (3, 1)), (16, (4, 0)), (19, (3, 2))]188189def census(orders, top, stop, mirrors=True):190    for n, c in CHORDS:191        if n not in orders:192            continue193        t0 = time.time()194        mm = mirror_index(c)195        m = mm if mirrors else None196        flags = [F, R] if m is None else [F, R, M, MR]197        G = walks(n, c, True)198        W = G[:stop] if stop else G199        lv2 = sharp = 0200        cross = {}201        alive = []202        kept = set()203        for g in W:204            for fl in flag_search(g, flags, m):205                lv2 += 1206                if closure(g, fl, m) > 1:207                    sharp += 1208                    continue209                kept.add(canon(g, fl, mm))210                seq = expand([(d, f) for d, f in zip(g, fl)], g, fl, m)211                L = 2212                while L < top:213                    seq = expand(seq, g, fl, m)214                    L += 1215                    if not avoids(seq):216                        cross[L] = cross.get(L, 0) + 1217                        break218                else:219                    alive.append((g, fl))220        mir = "F R" if m is None else "F R M MR"221        sym = "reversal" if mm is None else "reversal and mirror"222        if len(W) == len(G):223            tail = f"classes up to {sym} {len(kept)}, exhausted"224        else:225            tail = f"CUT after walk {len(W)} of {len(G)}"226        print(f"order {n} chord {c} flags {mir}: gentle walks {len(G)}, (generator, flags) pairs gentle and point-avoiding at level 2 by the typed search {lv2}, sharp at a later level by the junction closure {sharp}, gentle at every level {lv2 - sharp}, first crossing level {sorted(cross.items())}, alive at level {top} {len(alive)}, {tail}, {time.time() - t0:.1f}s")227        for g, fl in alive[:4]:228            print("  alive", g, [NAMES[f] for f in fl])229        sys.stdout.flush()230231def curve(g, fl, m, k):232    seq = [(0, F)]233    for _ in range(k):234        seq = expand(seq, g, fl, m)235    return seq236237def harmonic(seq):238    c = [0, 0, 0]239    for d, _ in seq:240        c[d % 3] += 1241    return c, c[0] ** 2 + c[1] ** 2 + c[2] ** 2 - c[0] * c[1] - c[1] * c[2] - c[0] * c[2]242243def gosper():244    g, fl = (0, 5, 3, 4, 0, 0, 1), (F, R, R, F, F, F, R)245    for k in range(1, 7):246        seq = curve(g, fl, None, k)247        tc = counts(seq)248        want = [7 ** (k - 1), 4 * 7 ** (k - 1) - 1, 2 * 7 ** (k - 1), 0]249        assert tc == want, (k, tc, want)250        assert k > 5 or avoids(seq)251        ax, h2 = harmonic(seq)252        assert h2 == 7 ** k253        if k == 2:254            assert [(-d) % 6 for d, _ in seq] == [x - 1 if x > 0 else 2 - x for x in A229214]255        print(f"gosper level {k}: steps {len(seq)}, turns 0/60/120 {tc[:3]}, axis counts {ax}, |h2|^2 {h2}")256257def lsys(rules, axiom, k):258    s = axiom259    for _ in range(k):260        s = "".join(rules.get(ch, ch) for ch in s)261    return s262263def square(rules, axiom, k):264    d = 0265    h = v = 0266    tc = [0, 0, 0]267    last = None268    for ch in lsys(rules, axiom, k):269        if ch == "+":270            d = (d + 1) % 4271        elif ch == "-":272            d = (d - 1) % 4273        elif ch == "F":274            if last is not None:275                tc[min((d - last) % 4, (last - d) % 4)] += 1276            last = d277            if d % 2:278                v += 1279            else:280                h += 1281    return h, v, tc282283def rate(orders):284    for n, c in CHORDS:285        if n not in orders:286            continue287        t0 = time.time()288        m = mirror_index(c)289        flags = [F, R] if m is None else [F, R, M, MR]290        pairs = 0291        rows = {}292        for g in walks(n, c, False):293            for fl in flag_search(g, flags, m, False):294                seq = curve(g, fl, m, 4)295                if not avoids(seq) or counts(seq)[3]:296                    continue297                pairs += 1298                s = [counts(curve(g, fl, m, k))[2] for k in range(1, 6)]299                rows[canon(g, fl, m)] = s300        sym = "reversal" if m is None else "reversal and mirror"301        print(f"order {n} chord {c}: (generator, flags) pairs avoiding points through level 4 {pairs}, classes up to {sym} {len(rows)}, {time.time() - t0:.1f}s")302        for (g, fl), s in sorted(rows.items(), key=lambda x: (x[1], x[0])):303            print(f"  {g} {[NAMES[f] for f in fl]}: 120-degree turns at levels 1..5 {s}, per unit step at level 5 {s[-1]}/{n ** 5}")304        sys.stdout.flush()305306def bead():307    hil = {"A": "+BF-AFA-FB+", "B": "-AF+BFB+FA-"}308    pea = {"X": "XFYFX+F+YFXFY-F-XFYFX", "Y": "YFXFY-F-XFYFX+F+YFXFY"}309    for k in range(1, 7):310        h, v, tc = square(hil, "A", k)311        assert h + v == 4 ** k - 1312        print(f"hilbert level {k}: steps {h + v}, horizontal {h}, vertical {v}, h2 {h - v}, turns 0/90 {tc[:2]}")313    for k in range(1, 5):314        h, v, tc = square(pea, "X", k)315        assert h + v == 9 ** k - 1316        print(f"peano level {k}: steps {h + v}, horizontal {h}, vertical {v}, h2 {h - v}, turns 0/90 {tc[:2]}")317318if __name__ == "__main__":319    verb = sys.argv[1] if len(sys.argv) > 1 else "census"320    orders = [int(x) for x in sys.argv[2].split(",")] if len(sys.argv) > 2 else None321    stop = int(sys.argv[3]) if len(sys.argv) > 3 else None322    if verb == "census":323        census(orders or [3, 4, 7, 9, 13], 4, stop)324    if verb == "plain":325        census(orders or [12], 4, stop, False)326    if verb == "gosper":327        gosper()328    if verb == "bead":329        bead()330    if verb == "rate":331        rate(orders or [7])