| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import random |
| from pathlib import Path |
| from typing import Iterable, List, Sequence, Tuple |
|
|
| import numpy as np |
|
|
|
|
| GRID_SIZE = 9 |
| BOX_SIZE = 3 |
| ALL_VALUES = tuple(range(1, 10)) |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| root = Path(__file__).resolve().parent.parent |
| default_output = root / "data" / "sudoku_t3_30empty_value_qwen_text.jsonl" |
| p = argparse.ArgumentParser() |
| p.add_argument("--output", type=str, default=str(default_output)) |
| p.add_argument("--num_puzzles", type=int, default=20000) |
| p.add_argument("--empties", type=int, default=30) |
| p.add_argument("--seed", type=int, default=0) |
| return p.parse_args() |
|
|
|
|
| def permute_groups(rng: random.Random, values: Sequence[int], group_size: int) -> List[int]: |
| groups = [list(values[idx : idx + group_size]) for idx in range(0, len(values), group_size)] |
| rng.shuffle(groups) |
| out: List[int] = [] |
| for group in groups: |
| rng.shuffle(group) |
| out.extend(group) |
| return out |
|
|
|
|
| def base_grid() -> np.ndarray: |
| return np.asarray( |
| [[((rr * BOX_SIZE + rr // BOX_SIZE + cc) % GRID_SIZE) + 1 for cc in range(GRID_SIZE)] for rr in range(GRID_SIZE)], |
| dtype=int, |
| ) |
|
|
|
|
| def random_solved_grid(rng: random.Random) -> np.ndarray: |
| grid = base_grid().copy() |
|
|
| digits = list(ALL_VALUES) |
| rng.shuffle(digits) |
| digit_map = {src: dst for src, dst in zip(ALL_VALUES, digits, strict=True)} |
| grid = np.vectorize(lambda value: digit_map[int(value)], otypes=[int])(grid) |
|
|
| row_order = permute_groups(rng, list(range(GRID_SIZE)), BOX_SIZE) |
| col_order = permute_groups(rng, list(range(GRID_SIZE)), BOX_SIZE) |
| grid = grid[row_order, :] |
| grid = grid[:, col_order] |
|
|
| if rng.random() < 0.5: |
| grid = grid.T |
| return np.asarray(grid, dtype=int) |
|
|
|
|
| def row_major_empty_locs(grid: np.ndarray) -> List[Tuple[int, int]]: |
| return [(int(r), int(c)) for r, c in np.argwhere(np.asarray(grid, dtype=int) == 0).tolist()] |
|
|
|
|
| def make_prompt(grid: np.ndarray) -> str: |
| tuples = [f"({r + 1},{c + 1},{int(grid[r, c])})" for r in range(GRID_SIZE) for c in range(GRID_SIZE)] |
| return ( |
| "9x9 Sudoku board encoded as (row,col,value) tuples in row-major order.\n" |
| "Value 0 means the cell is empty.\n" |
| + " ".join(tuples) |
| ) |
|
|
|
|
| def make_example(solved: np.ndarray, *, empties: int, rng: random.Random) -> dict: |
| if empties <= 0 or empties >= GRID_SIZE * GRID_SIZE: |
| raise ValueError(f"empties must be between 1 and {GRID_SIZE * GRID_SIZE - 1}") |
|
|
| cells = list(range(GRID_SIZE * GRID_SIZE)) |
| rng.shuffle(cells) |
| masked_cells = sorted(cells[:empties]) |
|
|
| puzzle = np.asarray(solved, dtype=int).copy() |
| for cell in masked_cells: |
| rr, cc = divmod(int(cell), GRID_SIZE) |
| puzzle[rr, cc] = 0 |
|
|
| empty_locs_1based = [(rr + 1, cc + 1) for rr, cc in row_major_empty_locs(puzzle)] |
| target_triples_1based = [(rr + 1, cc + 1, int(solved[rr, cc])) for rr, cc in row_major_empty_locs(puzzle)] |
| completion_values = [int(value) for _, _, value in target_triples_1based] |
|
|
| return { |
| "prompt": make_prompt(puzzle), |
| "completion": json.dumps(completion_values, separators=(",", ":")), |
| "metadata": { |
| "grid_size": GRID_SIZE, |
| "box_size": BOX_SIZE, |
| "empties": int(empties), |
| "empty_locs_1based": empty_locs_1based, |
| "target_triples_1based": target_triples_1based, |
| }, |
| } |
|
|
|
|
| def generate_examples(num_puzzles: int, *, empties: int, seed: int) -> Iterable[dict]: |
| rng = random.Random(int(seed)) |
| for _ in range(int(num_puzzles)): |
| solved = random_solved_grid(rng) |
| yield make_example(solved, empties=int(empties), rng=rng) |
|
|
|
|
| def main() -> None: |
| args = parse_args() |
| output_path = Path(args.output).resolve() |
| output_path.parent.mkdir(parents=True, exist_ok=True) |
| with output_path.open("w", encoding="utf-8") as f: |
| for row in generate_examples(args.num_puzzles, empties=args.empties, seed=args.seed): |
| f.write(json.dumps(row, separators=(",", ":")) + "\n") |
| print(f"Wrote {int(args.num_puzzles)} puzzles to {output_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|