Download code/cube_ops.py from briscoooe/tiny-cube-value-iter: direct link, hf CLI and curl.
- Browser
- Download file 3.76 kB
-
https://huggingface.co/briscoooe/tiny-cube-value-iter/resolve/main/code/cube_ops.py
- Command line
-
hf download hf://briscoooe/tiny-cube-value-iter/code/cube_ops.py
-
curl -L -o cube_ops.py https://huggingface.co/briscoooe/tiny-cube-value-iter/resolve/main/code/cube_ops.py
3.76 kB
| """Vectorized cube moves over numpy arrays of facelet indices. | |
| `gen_data.apply_move` rebuilds a 54-character Python string per move, which is | |
| fine when the bottleneck is a Kociemba call. Value iteration has no solver in | |
| the loop: every training step expands all 18 children of every state in the | |
| batch, so at batch 1024 that is ~18k move applications per step and the string | |
| path becomes the whole cost. | |
| A move is a fixed permutation of the 54 facelets -- `new[i] = old[perm[i]]` -- | |
| so a batch of states is one fancy-index away: `states[:, perm]`. Quarter turns | |
| compose by indexing a permutation with itself (`p2 = p[p]`), which is how the | |
| half- and counter-turns are built rather than by applying the quarter turn | |
| repeatedly. | |
| States are (N, 54) uint8 of colour indices into COLORS, not strings. Use | |
| `encode`/`decode` at the boundaries only. | |
| """ | |
| import numpy as np | |
| from gen_data import QT, MOVES, SOLVED | |
| COLORS = "URFDLB" | |
| _C2I = {c: i for i, c in enumerate(COLORS)} | |
| # MOVES order is (face, suffix) with suffix in ("", "'", "2") -- 1, 3 and 2 | |
| # quarter turns respectively. PERMS is indexed by position in MOVES so a move | |
| # index from the model maps straight to a permutation. | |
| _TURNS = {"": 1, "'": 3, "2": 2} | |
| def _compose(perm: np.ndarray, times: int) -> np.ndarray: | |
| out = np.arange(54, dtype=np.int64) | |
| for _ in range(times): | |
| out = out[perm] | |
| return out | |
| PERMS = np.stack([_compose(np.array(QT[m[0]], dtype=np.int64), _TURNS[m[1:]]) | |
| for m in MOVES]) | |
| NUM_MOVES = len(MOVES) | |
| assert PERMS.shape == (NUM_MOVES, 54) | |
| def encode(states) -> np.ndarray: | |
| """Facelet string(s) -> (N, 54) uint8.""" | |
| if isinstance(states, str): | |
| states = [states] | |
| return np.array([[_C2I[c] for c in s] for s in states], dtype=np.uint8) | |
| def decode(arr: np.ndarray): | |
| """(N, 54) uint8 -> list of facelet strings.""" | |
| return ["".join(COLORS[i] for i in row) for row in np.asarray(arr)] | |
| SOLVED_ARR = encode(SOLVED)[0] | |
| def apply_moves(states: np.ndarray, move_ids) -> np.ndarray: | |
| """Applies one move per state. `move_ids` is an int array of length N.""" | |
| rows = np.arange(len(states))[:, None] | |
| return states[rows, PERMS[np.asarray(move_ids)]] | |
| def all_children(states: np.ndarray) -> np.ndarray: | |
| """(N, 54) -> (N, 18, 54): every state's 18 successors, in MOVES order.""" | |
| return states[:, PERMS] | |
| def is_solved(states: np.ndarray) -> np.ndarray: | |
| """(N, 54) -> (N,) bool.""" | |
| return (states == SOLVED_ARR).all(axis=1) | |
| def random_walk(n: int, max_depth: int, rng: np.random.Generator): | |
| """n states from independent walks off solved, depths uniform in 1..max_depth. | |
| Returns (states, depths). The walk never turns the same face twice in a row, | |
| matching gen_data.random_state -- consecutive turns of one face are a single | |
| different move, so allowing them biases the effective depth downward. | |
| Depths are returned for reporting only. They are an upper bound on the true | |
| distance, never the label: value iteration derives its own targets, and a | |
| walk of length 12 routinely lands somewhere far nearer to solved. | |
| """ | |
| depths = rng.integers(1, max_depth + 1, size=n) | |
| states = np.repeat(SOLVED_ARR[None, :], n, axis=0) | |
| prev_face = np.full(n, -1) | |
| for step in range(max_depth): | |
| live = np.where(depths > step)[0] | |
| if not len(live): | |
| break | |
| mv = rng.integers(0, NUM_MOVES, size=len(live)) | |
| clash = (mv // 3) == prev_face[live] | |
| while clash.any(): | |
| mv[clash] = rng.integers(0, NUM_MOVES, size=int(clash.sum())) | |
| clash = (mv // 3) == prev_face[live] | |
| states[live] = states[live][np.arange(len(live))[:, None], PERMS[mv]] | |
| prev_face[live] = mv // 3 | |
| return states, depths | |