Download src/sudoku.py from GOVINDFROM/parallel-consistent-masked-diffusion: direct link, hf CLI and curl.
- Browser
- Download file 7.54 kB
-
https://huggingface.co/GOVINDFROM/parallel-consistent-masked-diffusion/resolve/main/src/sudoku.py
- Command line
-
hf download hf://GOVINDFROM/parallel-consistent-masked-diffusion/src/sudoku.py
-
curl -L -o sudoku.py https://huggingface.co/GOVINDFROM/parallel-consistent-masked-diffusion/resolve/main/src/sudoku.py
7.54 kB
| """Sudoku as a constraint-satisfaction benchmark for masked diffusion. | |
| Uses the SATNet dataset (Wang et al., ICML 2019), which is the standard 9x9 | |
| Sudoku benchmark for neural solvers: 10,000 puzzles with the conventional | |
| 9,000 / 1,000 train/test split, scored by BOARD-level accuracy - every one of the | |
| 81 cells correct, nothing partial. That metric is unambiguous and it is what the | |
| published numbers report, so a comparison means something. | |
| Why this task. Sudoku is the cleanest possible case of the regime this project | |
| is about: the answer is UNIQUE, so there is no information-theoretic obstacle at | |
| all, and the dependence between cells is enormous, since each cell is | |
| constrained by twenty others through its row, column and box. Our framework says | |
| the entire difficulty is therefore mode (i) - and, more sharply, that a decoder | |
| which commits cells in order of certainty is performing classical constraint | |
| propagation. Filling the forced cells first, recomputing, and repeating IS the | |
| textbook algorithm, and it is exactly what an entropy-budgeted commit rule does | |
| without being told to. | |
| The prediction, registered before any run: fixed-K parallel decoding should fail | |
| badly on this task while entropy-budgeted decoding should succeed, and the gap | |
| should be far larger than anything seen on text, where dependence is diffuse. | |
| """ | |
| import os | |
| import numpy as np | |
| GRID = 9 | |
| CELLS = GRID * GRID # 81 | |
| N_TRAIN, N_TEST = 9000, 1000 # the conventional SATNet split | |
| class SudokuTokenizer: | |
| """Ten symbols: the digits 1-9 and the absorbing state. | |
| There is no separate 'blank' symbol. A blank cell is simply one that starts | |
| masked, which is what makes this a conditional masked-diffusion problem | |
| rather than a classification one: clues are observed context, blanks are the | |
| positions to fill. | |
| """ | |
| def __init__(self): | |
| self.itos = [str(d) for d in range(1, 10)] + ["[MASK]"] | |
| self.stoi = {s: i for i, s in enumerate(self.itos)} | |
| self.mask = self.stoi["[MASK]"] | |
| self.pad = self.mask | |
| def __len__(self): | |
| return len(self.itos) | |
| def _onehot_to_int(t): | |
| """(N, 9, 9, 9) one-hot -> (N, 81) with 0 for an empty cell, 1-9 otherwise.""" | |
| filled = t.sum(-1) > 0 | |
| digits = t.argmax(-1) + 1 | |
| return np.where(filled, digits, 0).reshape(len(t), CELLS).astype(np.int64) | |
| def is_valid_solution(grid): | |
| """Does a completed 81-cell grid satisfy every Sudoku constraint? | |
| Checked rather than assumed: if the dataset's labels were not valid Sudoku | |
| the entire benchmark would be meaningless, and the failure would be silent. | |
| """ | |
| g = np.asarray(grid).reshape(GRID, GRID) | |
| target = set(range(1, 10)) | |
| for i in range(GRID): | |
| if set(g[i]) != target or set(g[:, i]) != target: | |
| return False | |
| for bi in range(0, GRID, 3): | |
| for bj in range(0, GRID, 3): | |
| if set(g[bi:bi + 3, bj:bj + 3].ravel()) != target: | |
| return False | |
| return True | |
| def consistent_with_clues(pred, puzzle): | |
| """Does a prediction keep every given clue? (It should: clues are observed.)""" | |
| p, q = np.asarray(pred), np.asarray(puzzle) | |
| given = q > 0 | |
| return bool((p[given] == q[given]).all()) | |
| def load_satnet(root="data/sudoku"): | |
| """Return (puzzles, solutions, tokenizer) as (N, 81) integer arrays. | |
| puzzles: 0 where the cell is blank, 1-9 where a clue is given | |
| solutions: 1-9 everywhere | |
| """ | |
| import torch | |
| base = os.path.join(root, "sudoku") | |
| if not os.path.isdir(base): | |
| base = root | |
| fx = os.path.join(base, "features.pt") | |
| fy = os.path.join(base, "labels.pt") | |
| if not (os.path.exists(fx) and os.path.exists(fy)): | |
| raise FileNotFoundError( | |
| f"SATNet Sudoku not found under {root}. Fetch once with:\n" | |
| f" mkdir -p {root} && cd {root} && curl -sLO https://powei.tw/sudoku.zip " | |
| f"&& unzip -q sudoku.zip") | |
| X = _onehot_to_int(torch.load(fx, map_location="cpu").numpy()) | |
| Y = _onehot_to_int(torch.load(fy, map_location="cpu").numpy()) | |
| return X, Y, SudokuTokenizer() | |
| def split(X, Y): | |
| """The conventional 9,000 / 1,000 split, in dataset order.""" | |
| return (X[:N_TRAIN], Y[:N_TRAIN]), (X[N_TRAIN:N_TRAIN + N_TEST], | |
| Y[N_TRAIN:N_TRAIN + N_TEST]) | |
| def board_accuracy(pred, gold): | |
| """Fraction of boards with ALL 81 cells correct - the published metric.""" | |
| return float((np.asarray(pred) == np.asarray(gold)).all(axis=1).mean()) | |
| def cell_accuracy(pred, gold, blanks): | |
| """Per-cell accuracy over blanks only, reported alongside as context.""" | |
| p, g, b = np.asarray(pred), np.asarray(gold), np.asarray(blanks) | |
| return float((p[b] == g[b]).mean()) | |
| if __name__ == "__main__": | |
| root = os.environ.get("SUDOKUROOT", "data/sudoku") | |
| X, Y, tok = load_satnet(root) | |
| (Xtr, Ytr), (Xte, Yte) = split(X, Y) | |
| print(f"puzzles {X.shape} solutions {Y.shape} vocab {len(tok)}") | |
| print(f"train {Xtr.shape[0]} test {Xte.shape[0]} (conventional split)") | |
| clues = (X > 0).sum(1) | |
| print(f"clues per puzzle: min={clues.min()} max={clues.max()} " | |
| f"mean={clues.mean():.1f}") | |
| # every label must be a valid Sudoku solution, or the benchmark is meaningless | |
| bad = sum(not is_valid_solution(Y[i]) for i in range(0, len(Y), 7)) | |
| print(f"labels that are NOT valid Sudoku (sampled): {bad} (must be 0)") | |
| # every clue must agree with its solution | |
| dis = sum(not consistent_with_clues(Y[i], X[i]) for i in range(0, len(X), 7)) | |
| print(f"puzzles whose clues disagree with the label (sampled): {dis} (must be 0)") | |
| # train and test must not overlap | |
| tr = {bytes(r) for r in Xtr[::3]} | |
| ov = sum(bytes(r) in tr for r in Xte) | |
| print(f"test puzzles also seen in the training sample: {ov} (must be 0)") | |
| def _unit_list(): | |
| us = [] | |
| for r in range(GRID): | |
| us.append([r * GRID + c for c in range(GRID)]) | |
| for c in range(GRID): | |
| us.append([r * GRID + c for r in range(GRID)]) | |
| for br in range(0, GRID, 3): | |
| for bc in range(0, GRID, 3): | |
| us.append([(br + dr) * GRID + bc + dc for dr in range(3) for dc in range(3)]) | |
| return us | |
| UNIT_LIST = _unit_list() | |
| def violation_mask(x): | |
| """Positions involved in a row/column/box duplicate. Exact, not heuristic. | |
| x is (B, 81) of token ids 0-8. A cell is flagged when its value appears more | |
| than once in any unit it belongs to. A solved grid flags nothing, so this | |
| doubles as the stopping condition for verifier-guided remasking. | |
| """ | |
| import torch | |
| B = x.shape[0] | |
| bad = torch.zeros_like(x, dtype=torch.bool) | |
| for unit in UNIT_LIST: | |
| idx = torch.tensor(unit, device=x.device) | |
| vals = x[:, idx] # (B, 9) | |
| same = vals[:, :, None] == vals[:, None, :] # (B, 9, 9) | |
| dup = same.sum(-1) > 1 # appears more than once | |
| bad[:, idx] |= dup | |
| return bad | |
| def expand_to_units(bad): | |
| """Widen a violation mask to every cell sharing a unit with a flagged cell. | |
| Re-deciding a single conflicting cell in an otherwise unchanged context tends | |
| to reproduce the same value. Reopening its row, column and box gives the | |
| model a subproblem it can actually re-solve. | |
| """ | |
| import torch | |
| out = torch.zeros_like(bad) | |
| for unit in UNIT_LIST: | |
| idx = torch.tensor(unit, device=bad.device) | |
| hit = bad[:, idx].any(1, keepdim=True) # (B, 1) | |
| out[:, idx] |= hit | |
| return out | |