bench: add optional CP-SAT reference
Provide a separately installed OR-Tools model to compare a generic constraint solver with the native first-solution path without adding a production or default-test dependency. Use no-overlap, exact-fill, edge and equal-copy symmetry constraints, validate placements independently, and record model size, memory, worker count and timings for orders 8 and 9. Keep the tool only as a reference and defer DLX absent new evidence. Tests: Release CTest (9 passed) Tests: Python reference tests and compilation checks Tests: independently validated CP-SAT orders 8 and 9 Refs: #5
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dependency-free independent validator for Partridge placement JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def validate(document: dict[str, Any]) -> list[str]:
|
||||
"""Return diagnostics for a placement document, or an empty list."""
|
||||
diagnostics: list[str] = []
|
||||
try:
|
||||
order = int(document["order"])
|
||||
board_side = int(document["board_side"])
|
||||
placements = document["placements"]
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
return [f"invalid document: {error}"]
|
||||
|
||||
expected_side = order * (order + 1) // 2
|
||||
if order < 1:
|
||||
diagnostics.append("order must be positive")
|
||||
if board_side != expected_side:
|
||||
diagnostics.append(
|
||||
f"board side is {board_side}; expected triangular number {expected_side}"
|
||||
)
|
||||
if not isinstance(placements, list):
|
||||
return diagnostics + ["placements must be a list"]
|
||||
if board_side < 0:
|
||||
return diagnostics + ["board side must not be negative"]
|
||||
|
||||
multiplicities = [0] * (max(order, 0) + 1)
|
||||
occupied = [-1] * (board_side * board_side)
|
||||
for index, raw in enumerate(placements):
|
||||
try:
|
||||
x = int(raw["x"])
|
||||
y = int(raw["y"])
|
||||
side = int(raw["side"])
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
diagnostics.append(f"placement {index} is malformed: {error}")
|
||||
continue
|
||||
|
||||
description = f"placement {index} at ({x}, {y}) with side {side}"
|
||||
if side < 1 or side > order:
|
||||
diagnostics.append(f"{description} has an invalid side length")
|
||||
continue
|
||||
multiplicities[side] += 1
|
||||
if x < 0 or y < 0 or x + side > board_side or y + side > board_side:
|
||||
diagnostics.append(f"{description} is outside the board bounds")
|
||||
continue
|
||||
|
||||
overlap = -1
|
||||
for row in range(y, y + side):
|
||||
for column in range(x, x + side):
|
||||
cell = column + row * board_side
|
||||
if occupied[cell] != -1:
|
||||
overlap = occupied[cell]
|
||||
else:
|
||||
occupied[cell] = index
|
||||
if overlap != -1:
|
||||
diagnostics.append(f"{description} overlaps placement {overlap}")
|
||||
|
||||
for side in range(1, order + 1):
|
||||
if multiplicities[side] != side:
|
||||
diagnostics.append(
|
||||
f"side {side} has multiplicity {multiplicities[side]}; expected {side}"
|
||||
)
|
||||
if -1 in occupied:
|
||||
diagnostics.append("board is not completely covered")
|
||||
return diagnostics
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"input",
|
||||
nargs="?",
|
||||
type=argparse.FileType("r", encoding="utf-8"),
|
||||
default=sys.stdin,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
document = json.load(args.input)
|
||||
diagnostics = validate(document)
|
||||
json.dump(
|
||||
{"valid": not diagnostics, "diagnostics": diagnostics},
|
||||
sys.stdout,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
)
|
||||
sys.stdout.write("\n")
|
||||
return 0 if not diagnostics else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user