AS2 / src /softasp /sudoku_inference.py
wabdalm's picture
Release AS2 framework with Hydra-selected instantiations
69928d6 verified
Raw History Blame Contribute Delete
3.82 kB
"""Label-free raw prediction for one or more Visual Sudoku boards."""
from pathlib import Path
import numpy as np
import torch
from omegaconf import DictConfig
from softasp_solver.sudoku.softasp_sudoku import SoftASPSudoku
from softasp.instantiations.checkpoint import read_checkpoint, validate_checkpoint
from softasp.sudoku_reporting import file_sha256
def load_model(
checkpoint: Path, device: str = "auto", *, cfg: DictConfig | None = None
) -> tuple[SoftASPSudoku, torch.device]:
"""Load public inference weights; optimizer state and labels are unnecessary."""
target = torch.device(
("cuda" if torch.cuda.is_available() else "cpu") if device == "auto" else device
)
saved = read_checkpoint(checkpoint)
if cfg is not None:
validate_checkpoint(
cfg,
saved,
legacy_model="softasp_sudoku",
legacy_constraints={"_target_": "softasp_solver.sudoku.constraints.TPOperatorLoss"},
)
model_config = saved["config"]["model"]
if model_config["name"] != "softasp_sudoku":
raise ValueError("Expected a Sudoku AS2 checkpoint")
params = dict(model_config["params"])
if params.get("oracle_perception", False):
raise ValueError("Oracle checkpoints are not supported")
model = SoftASPSudoku(**params)
if cfg is not None:
from hydra.utils import instantiate
model.constraint_loss = instantiate(cfg.instantiation.constraints)
model.load_state_dict(saved["model_state_dict"], strict=True)
return model.to(target).eval(), target
@torch.no_grad()
def predict_file(
checkpoint: Path, input_path: Path, device: str = "auto", *, cfg: DictConfig | None = None
) -> dict:
"""Read images and clue positions only; output raw digits 1 through 9."""
with np.load(input_path, allow_pickle=False) as data:
images = data["images"].copy()
mask = data["puzzle_mask"].copy()
if images.ndim != 4 or images.shape[1:] != (252, 252, 1) or len(images) == 0:
raise ValueError("images must have shape (N, 252, 252, 1), N > 0")
if images.dtype != np.uint8 and not (
images.dtype == np.float32
and np.isfinite(images).all()
and np.all((images >= 0) & (images <= 1))
):
raise ValueError("images must be uint8 [0,255] or float32 [0,1]")
if mask.shape != (len(images), 81) or not np.isin(mask, [0, 1]).all():
raise ValueError("puzzle_mask must have shape (N,81) and contain only 0/1")
model, target = load_model(checkpoint, device, cfg=cfg)
predictions = []
# Bound inference batches without requiring all images to fit on the device.
for start in range(0, len(images), 32):
out = model.predict(
torch.from_numpy(images[start : start + 32]).to(target),
torch.from_numpy(mask[start : start + 32].astype(np.int64)).to(target),
)
logits = out["logits"]
if logits.shape[1:] != (81, 9) or not torch.isfinite(logits).all():
raise ValueError("Expected finite raw Sudoku logits")
predictions.append(logits.argmax(-1).cpu())
digits = torch.cat(predictions).numpy().reshape(-1, 9, 9) + 1
expected = np.arange(1, 10)
valid = []
for board in digits:
boxes = board.reshape(3, 3, 3, 3).transpose(0, 2, 1, 3).reshape(9, 9)
valid.append(
bool(
np.all(np.sort(board, axis=1) == expected)
and np.all(np.sort(board.T, axis=1) == expected)
and np.all(np.sort(boxes, axis=1) == expected)
)
)
return {
"decoder": "raw_argmax",
"digits": digits.tolist(),
"structurally_valid": valid,
"checkpoint_sha256": file_sha256(checkpoint),
"input_sha256": file_sha256(input_path),
}