sudoku-smt-solvers 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sudoku_smt_solvers/__init__.py +4 -0
- sudoku_smt_solvers/benchmarks/__init__.py +5 -0
- sudoku_smt_solvers/benchmarks/benchmark_runner.py +211 -0
- sudoku_smt_solvers/solvers/__init__.py +4 -0
- sudoku_smt_solvers/solvers/cvc5_solver.py +207 -0
- sudoku_smt_solvers/solvers/dpll_solver.py +175 -0
- sudoku_smt_solvers/solvers/dpllt_solver.py +211 -0
- sudoku_smt_solvers/solvers/sudoku_error.py +8 -0
- sudoku_smt_solvers/solvers/z3_solver.py +160 -0
- sudoku_smt_solvers-0.1.0.dist-info/LICENSE +21 -0
- sudoku_smt_solvers-0.1.0.dist-info/METADATA +161 -0
- sudoku_smt_solvers-0.1.0.dist-info/RECORD +14 -0
- sudoku_smt_solvers-0.1.0.dist-info/WHEEL +5 -0
- sudoku_smt_solvers-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
|
|
1
|
+
from .benchmark_runner import BenchmarkRunner
|
2
|
+
from .sudoku_generator.dfs_solver import DFSSolver
|
3
|
+
from .sudoku_generator.sudoku_generator import SudokuGenerator
|
4
|
+
from .sudoku_generator.las_vegas import LasVegasGenerator
|
5
|
+
from .sudoku_generator.hole_digger import HoleDigger
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
import multiprocessing
|
5
|
+
from typing import Dict, List, Optional
|
6
|
+
|
7
|
+
from ..solvers import CVC5Solver, DPLLSolver, DPLLTSolver, Z3Solver
|
8
|
+
|
9
|
+
|
10
|
+
class BenchmarkRunner:
|
11
|
+
"""A benchmark runner for comparing different Sudoku solver implementations.
|
12
|
+
|
13
|
+
This class manages running performance benchmarks across multiple Sudoku solvers,
|
14
|
+
collecting metrics like solve time and propagation counts, and saving results
|
15
|
+
to CSV files.
|
16
|
+
|
17
|
+
Attributes:
|
18
|
+
puzzles_dir (str): Directory containing puzzle JSON files
|
19
|
+
results_dir (str): Directory where benchmark results are saved
|
20
|
+
timeout (int): Maximum time in seconds allowed for each solver attempt
|
21
|
+
solvers (dict): Dictionary mapping solver names to solver classes
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
puzzles_dir: str = "benchmarks/puzzles",
|
27
|
+
results_dir: str = "benchmarks/results",
|
28
|
+
timeout: int = 120,
|
29
|
+
):
|
30
|
+
"""Initialize the benchmark runner.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
puzzles_dir: Directory containing puzzle JSON files
|
34
|
+
results_dir: Directory where benchmark results will be saved
|
35
|
+
timeout: Maximum time in seconds allowed for each solver attempt
|
36
|
+
"""
|
37
|
+
self.puzzles_dir = puzzles_dir
|
38
|
+
self.results_dir = results_dir
|
39
|
+
self.timeout = timeout
|
40
|
+
self.solvers = {
|
41
|
+
"CVC5": CVC5Solver,
|
42
|
+
"DPLL": DPLLSolver,
|
43
|
+
"DPLL(T)": DPLLTSolver,
|
44
|
+
"Z3": Z3Solver,
|
45
|
+
}
|
46
|
+
os.makedirs(results_dir, exist_ok=True)
|
47
|
+
|
48
|
+
def load_puzzle(self, puzzle_id: str) -> Optional[List[List[int]]]:
|
49
|
+
puzzle_path = os.path.join(self.puzzles_dir, f"{puzzle_id}.json")
|
50
|
+
try:
|
51
|
+
with open(puzzle_path, "r") as f:
|
52
|
+
data = json.load(f)
|
53
|
+
for key in ["grid", "puzzle", "gridc"]:
|
54
|
+
if key in data:
|
55
|
+
return data[key]
|
56
|
+
print(
|
57
|
+
f"No valid grid found in {puzzle_id}. Available keys: {list(data.keys())}"
|
58
|
+
)
|
59
|
+
return None
|
60
|
+
except Exception as e:
|
61
|
+
print(f"Error loading puzzle {puzzle_id}: {e}")
|
62
|
+
return None
|
63
|
+
|
64
|
+
def _solve_with_timeout(self, solver_class, puzzle, queue):
|
65
|
+
solver = solver_class(puzzle)
|
66
|
+
result = solver.solve()
|
67
|
+
# Pack both the result and propagation count
|
68
|
+
queue.put((result, getattr(solver, "propagated_clauses", 0)))
|
69
|
+
|
70
|
+
def run_solver(self, solver_name: str, puzzle: List[List[int]]) -> Dict:
|
71
|
+
"""Run a single solver on a puzzle and collect results with timeout.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
solver_name: Name of the solver to use
|
75
|
+
puzzle: 2D list representing the Sudoku puzzle
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Dict containing:
|
79
|
+
status: 'sat', 'unsat', 'timeout', or 'error'
|
80
|
+
solve_time: Time taken in seconds
|
81
|
+
propagations: Number of clause propagations (if available)
|
82
|
+
"""
|
83
|
+
solver_class = self.solvers[solver_name]
|
84
|
+
|
85
|
+
# Create queue for getting results
|
86
|
+
ctx = multiprocessing.get_context("spawn")
|
87
|
+
queue = ctx.Queue()
|
88
|
+
|
89
|
+
# Create process for solving
|
90
|
+
process = ctx.Process(
|
91
|
+
target=self._solve_with_timeout, args=(solver_class, puzzle, queue)
|
92
|
+
)
|
93
|
+
|
94
|
+
start_time = time.time()
|
95
|
+
process.start()
|
96
|
+
process.join(timeout=self.timeout)
|
97
|
+
|
98
|
+
solve_time = time.time() - start_time
|
99
|
+
|
100
|
+
if process.is_alive():
|
101
|
+
process.terminate()
|
102
|
+
process.join()
|
103
|
+
return {"status": "timeout", "solve_time": self.timeout, "propagations": 0}
|
104
|
+
|
105
|
+
# Get result and propagation count from queue
|
106
|
+
try:
|
107
|
+
result, propagations = queue.get_nowait()
|
108
|
+
return {
|
109
|
+
"status": "sat" if result else "unsat",
|
110
|
+
"solve_time": solve_time,
|
111
|
+
"propagations": propagations,
|
112
|
+
}
|
113
|
+
except:
|
114
|
+
return {"status": "error", "solve_time": solve_time, "propagations": 0}
|
115
|
+
|
116
|
+
def run_benchmarks(self) -> None:
|
117
|
+
"""Run all solvers on all puzzles and save results.
|
118
|
+
|
119
|
+
Executes benchmarks for each solver on each puzzle, collecting performance
|
120
|
+
metrics and saving results to a timestamped CSV file.
|
121
|
+
|
122
|
+
The CSV output includes:
|
123
|
+
- Solver name
|
124
|
+
- Puzzle unique ID
|
125
|
+
- Solution status
|
126
|
+
- Solve time
|
127
|
+
- Propagation count
|
128
|
+
|
129
|
+
Also calculates and stores aggregate statistics per solver:
|
130
|
+
- Total puzzles attempted
|
131
|
+
- Number of puzzles solved
|
132
|
+
- Total and average solving times
|
133
|
+
- Total and average propagation counts
|
134
|
+
"""
|
135
|
+
results = {
|
136
|
+
solver_name: {
|
137
|
+
"puzzles": {},
|
138
|
+
"stats": {
|
139
|
+
"total_puzzles": 0,
|
140
|
+
"solved_count": 0,
|
141
|
+
"total_time": 0,
|
142
|
+
"total_propagations": 0,
|
143
|
+
"avg_time": 0,
|
144
|
+
"avg_propagations": 0,
|
145
|
+
},
|
146
|
+
}
|
147
|
+
for solver_name in self.solvers
|
148
|
+
}
|
149
|
+
|
150
|
+
puzzle_files = [f for f in os.listdir(self.puzzles_dir) if f.endswith(".json")]
|
151
|
+
print(f"Found {len(puzzle_files)} puzzle files") # Debug
|
152
|
+
|
153
|
+
for puzzle_file in puzzle_files:
|
154
|
+
puzzle_id = puzzle_file[:-5]
|
155
|
+
puzzle = self.load_puzzle(puzzle_id)
|
156
|
+
|
157
|
+
if not puzzle:
|
158
|
+
print(f"Failed to load puzzle: {puzzle_id}") # Debug
|
159
|
+
continue
|
160
|
+
|
161
|
+
for solver_name in self.solvers:
|
162
|
+
print(f"Running {solver_name} on puzzle {puzzle_id}")
|
163
|
+
result = self.run_solver(solver_name, puzzle)
|
164
|
+
print(f"Result: {result}") # Debug
|
165
|
+
|
166
|
+
results[solver_name]["puzzles"][puzzle_id] = result
|
167
|
+
|
168
|
+
stats = results[solver_name]["stats"]
|
169
|
+
stats["total_puzzles"] += 1
|
170
|
+
if result["status"] == "sat":
|
171
|
+
stats["solved_count"] += 1
|
172
|
+
stats["total_time"] += result["solve_time"]
|
173
|
+
stats["total_propagations"] += result["propagations"]
|
174
|
+
|
175
|
+
# Calculate averages
|
176
|
+
for solver_name, solver_stats in results.items():
|
177
|
+
stats = solver_stats["stats"]
|
178
|
+
total_puzzles = stats["total_puzzles"]
|
179
|
+
if total_puzzles > 0:
|
180
|
+
stats["avg_time"] = stats["total_time"] / total_puzzles
|
181
|
+
stats["avg_propagations"] = stats["total_propagations"] / total_puzzles
|
182
|
+
print(f"Stats for {solver_name}: {stats}") # Debug
|
183
|
+
|
184
|
+
# Save results
|
185
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
186
|
+
|
187
|
+
# Debug CSV data
|
188
|
+
csv_data = []
|
189
|
+
for solver_name, solver_results in results.items():
|
190
|
+
for puzzle_id, puzzle_result in solver_results["puzzles"].items():
|
191
|
+
row = {
|
192
|
+
"solver": solver_name,
|
193
|
+
"puzzle_id": puzzle_id,
|
194
|
+
"status": puzzle_result["status"],
|
195
|
+
"solve_time": puzzle_result["solve_time"],
|
196
|
+
"propagations": puzzle_result["propagations"],
|
197
|
+
}
|
198
|
+
csv_data.append(row)
|
199
|
+
print(f"Adding CSV row: {row}") # Debug
|
200
|
+
|
201
|
+
csv_path = os.path.join(self.results_dir, f"benchmark_{timestamp}.csv")
|
202
|
+
print(f"Writing {len(csv_data)} rows to CSV") # Debug
|
203
|
+
|
204
|
+
with open(csv_path, "w") as f:
|
205
|
+
if csv_data:
|
206
|
+
headers = csv_data[0].keys()
|
207
|
+
f.write(",".join(headers) + "\n")
|
208
|
+
for row in csv_data:
|
209
|
+
f.write(",".join(str(row[h]) for h in headers) + "\n")
|
210
|
+
|
211
|
+
print(f"Benchmark results saved to {csv_path}")
|
@@ -0,0 +1,207 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
import atexit
|
3
|
+
from cvc5 import Kind, Solver
|
4
|
+
from .sudoku_error import SudokuError
|
5
|
+
|
6
|
+
|
7
|
+
class CVC5Solver:
|
8
|
+
"""CVC5-based Sudoku solver using SMT encoding.
|
9
|
+
|
10
|
+
Solves 25x25 Sudoku puzzles by encoding the problem as an SMT formula and using
|
11
|
+
CVC5 to find a satisfying assignment.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
sudoku (List[List[int]]): Input puzzle as 25x25 grid
|
15
|
+
size (int): Grid size (always 25)
|
16
|
+
solver (Solver): CVC5 solver instance
|
17
|
+
variables (List[List[Term]]): SMT variables for each cell
|
18
|
+
propagated_clauses (int): Counter for clause assertions
|
19
|
+
|
20
|
+
Example:
|
21
|
+
>>> puzzle = [[0 for _ in range(25)] for _ in range(25)] # Empty puzzle
|
22
|
+
>>> solver = CVC5Solver(puzzle)
|
23
|
+
>>> solution = solver.solve()
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, sudoku):
|
27
|
+
"""Initialize CVC5 Sudoku solver.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
sudoku: 25x25 grid with values 0-25 (0 for empty cells)
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
SudokuError: If puzzle format is invalid
|
34
|
+
"""
|
35
|
+
if not sudoku or not isinstance(sudoku, list) or len(sudoku) != 25:
|
36
|
+
raise SudokuError("Invalid Sudoku puzzle: must be a 25x25 grid")
|
37
|
+
|
38
|
+
self._validate_input(sudoku)
|
39
|
+
self.sudoku = sudoku
|
40
|
+
self.size = len(sudoku)
|
41
|
+
self.solver = None
|
42
|
+
self.variables = None
|
43
|
+
self.propagated_clauses = 0
|
44
|
+
|
45
|
+
def _validate_input(self, sudoku):
|
46
|
+
for i, row in enumerate(sudoku):
|
47
|
+
if not isinstance(row, list) or len(row) != 25:
|
48
|
+
raise SudokuError(f"Invalid Sudoku puzzle: row {i} must have 25 cells")
|
49
|
+
for j, val in enumerate(row):
|
50
|
+
if not isinstance(val, int) or not (0 <= val <= 25):
|
51
|
+
raise SudokuError(
|
52
|
+
f"Invalid value at position ({i},{j}): must be between 0 and 25"
|
53
|
+
)
|
54
|
+
|
55
|
+
def _count_assertion(self):
|
56
|
+
self.propagated_clauses += 1
|
57
|
+
|
58
|
+
def create_variables(self):
|
59
|
+
self.solver = Solver()
|
60
|
+
|
61
|
+
# Configure CVC5 solver options
|
62
|
+
self.solver.setOption("produce-models", "true")
|
63
|
+
self.solver.setOption("incremental", "true")
|
64
|
+
self.solver.setLogic("QF_LIA") # Quantifier-Free Linear Integer Arithmetic
|
65
|
+
|
66
|
+
integer_sort = self.solver.getIntegerSort()
|
67
|
+
self.variables = [
|
68
|
+
[self.solver.mkConst(integer_sort, f"x_{i}_{j}") for j in range(25)]
|
69
|
+
for i in range(25)
|
70
|
+
]
|
71
|
+
atexit.register(self.cleanup)
|
72
|
+
|
73
|
+
def encode_rules(self):
|
74
|
+
# Domain constraints
|
75
|
+
for i in range(25):
|
76
|
+
for j in range(25):
|
77
|
+
self.solver.assertFormula(
|
78
|
+
self.solver.mkTerm(
|
79
|
+
Kind.AND,
|
80
|
+
self.solver.mkTerm(
|
81
|
+
Kind.LEQ, self.solver.mkInteger(1), self.variables[i][j]
|
82
|
+
),
|
83
|
+
self.solver.mkTerm(
|
84
|
+
Kind.LEQ, self.variables[i][j], self.solver.mkInteger(25)
|
85
|
+
),
|
86
|
+
)
|
87
|
+
)
|
88
|
+
self._count_assertion()
|
89
|
+
|
90
|
+
# Row constraints
|
91
|
+
for i in range(25):
|
92
|
+
self.solver.assertFormula(
|
93
|
+
self.solver.mkTerm(
|
94
|
+
Kind.DISTINCT, *[self.variables[i][j] for j in range(25)]
|
95
|
+
)
|
96
|
+
)
|
97
|
+
self._count_assertion()
|
98
|
+
|
99
|
+
# Column constraints
|
100
|
+
for j in range(25):
|
101
|
+
self.solver.assertFormula(
|
102
|
+
self.solver.mkTerm(
|
103
|
+
Kind.DISTINCT, *[self.variables[i][j] for i in range(25)]
|
104
|
+
)
|
105
|
+
)
|
106
|
+
self._count_assertion()
|
107
|
+
|
108
|
+
# 5x5 subgrid constraints
|
109
|
+
for block_row in range(0, 25, 5):
|
110
|
+
for block_col in range(0, 25, 5):
|
111
|
+
block_vars = [
|
112
|
+
self.variables[i][j]
|
113
|
+
for i in range(block_row, block_row + 5)
|
114
|
+
for j in range(block_col, block_col + 5)
|
115
|
+
]
|
116
|
+
self.solver.assertFormula(
|
117
|
+
self.solver.mkTerm(Kind.DISTINCT, *block_vars)
|
118
|
+
)
|
119
|
+
self._count_assertion()
|
120
|
+
|
121
|
+
def encode_puzzle(self):
|
122
|
+
for i in range(25):
|
123
|
+
for j in range(25):
|
124
|
+
if self.sudoku[i][j] != 0: # Pre-filled cell
|
125
|
+
self.solver.assertFormula(
|
126
|
+
self.solver.mkTerm(
|
127
|
+
Kind.EQUAL,
|
128
|
+
self.variables[i][j],
|
129
|
+
self.solver.mkInteger(self.sudoku[i][j]),
|
130
|
+
)
|
131
|
+
)
|
132
|
+
self._count_assertion()
|
133
|
+
|
134
|
+
def extract_solution(self):
|
135
|
+
solution = [[0 for _ in range(25)] for _ in range(25)]
|
136
|
+
for i in range(25):
|
137
|
+
for j in range(25):
|
138
|
+
solution[i][j] = self.solver.getValue(
|
139
|
+
self.variables[i][j]
|
140
|
+
).getIntegerValue()
|
141
|
+
return solution
|
142
|
+
|
143
|
+
def cleanup(self):
|
144
|
+
if self.solver:
|
145
|
+
self.solver = None
|
146
|
+
|
147
|
+
def validate_solution(self, solution):
|
148
|
+
if not solution:
|
149
|
+
return False
|
150
|
+
|
151
|
+
# Check dimensions
|
152
|
+
if len(solution) != self.size or any(len(row) != self.size for row in solution):
|
153
|
+
return False
|
154
|
+
|
155
|
+
valid_nums = set(range(1, self.size + 1))
|
156
|
+
|
157
|
+
# Check rows
|
158
|
+
if any(set(row) != valid_nums for row in solution):
|
159
|
+
return False
|
160
|
+
|
161
|
+
# Check columns
|
162
|
+
for col in range(self.size):
|
163
|
+
if set(solution[row][col] for row in range(self.size)) != valid_nums:
|
164
|
+
return False
|
165
|
+
|
166
|
+
# Check 5x5 subgrids
|
167
|
+
subgrid_size = 5
|
168
|
+
for box_row in range(0, self.size, subgrid_size):
|
169
|
+
for box_col in range(0, self.size, subgrid_size):
|
170
|
+
numbers = set()
|
171
|
+
for i in range(subgrid_size):
|
172
|
+
for j in range(subgrid_size):
|
173
|
+
numbers.add(solution[box_row + i][box_col + j])
|
174
|
+
if numbers != valid_nums:
|
175
|
+
return False
|
176
|
+
|
177
|
+
return True
|
178
|
+
|
179
|
+
def solve(self):
|
180
|
+
"""Solve the Sudoku puzzle using CVC5.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Solved 25x25 grid if satisfiable, None if unsatisfiable
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
Exception: If solver encounters an error
|
187
|
+
|
188
|
+
Note:
|
189
|
+
Always cleans up solver resources, even on failure
|
190
|
+
"""
|
191
|
+
try:
|
192
|
+
self.create_variables()
|
193
|
+
self.encode_rules()
|
194
|
+
self.encode_puzzle()
|
195
|
+
|
196
|
+
result = self.solver.checkSat()
|
197
|
+
|
198
|
+
if result.isSat():
|
199
|
+
solution = self.extract_solution()
|
200
|
+
if self.validate_solution(solution):
|
201
|
+
return solution
|
202
|
+
return None
|
203
|
+
|
204
|
+
except Exception as e:
|
205
|
+
raise
|
206
|
+
finally:
|
207
|
+
self.cleanup()
|
@@ -0,0 +1,175 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
from pysat.solvers import Solver
|
3
|
+
from pysat.formula import CNF
|
4
|
+
from .sudoku_error import SudokuError
|
5
|
+
|
6
|
+
|
7
|
+
class DPLLSolver:
|
8
|
+
"""DPLL-based Sudoku solver using SAT encoding.
|
9
|
+
|
10
|
+
Solves 25x25 Sudoku puzzles by converting them to CNF (Conjunctive Normal Form)
|
11
|
+
and using DPLL to find a satisfying assignment.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
sudoku (List[List[int]]): Input puzzle as 25x25 grid
|
15
|
+
size (int): Grid size (25)
|
16
|
+
cnf (CNF): PySAT CNF formula object
|
17
|
+
solver (Solver): PySAT Glucose3 solver instance
|
18
|
+
propagated_clauses (int): Counter for clause additions
|
19
|
+
|
20
|
+
Example:
|
21
|
+
>>> puzzle = [[0 for _ in range(25)] for _ in range(25)]
|
22
|
+
>>> solver = DPLLSolver(puzzle)
|
23
|
+
>>> solution = solver.solve()
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, sudoku: List[List[int]]) -> None:
|
27
|
+
"""Initialize DPLL Sudoku solver.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
sudoku: 25x25 grid with values 0-25 (0 for empty cells)
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
SudokuError: If puzzle format is invalid
|
34
|
+
"""
|
35
|
+
if not sudoku or not isinstance(sudoku, list) or len(sudoku) != 25:
|
36
|
+
raise SudokuError("Invalid Sudoku puzzle: must be a 25x25 grid")
|
37
|
+
|
38
|
+
self.sudoku = sudoku
|
39
|
+
self.size = 25
|
40
|
+
self.cnf = CNF() # CNF object to store Boolean clauses
|
41
|
+
self.solver = Solver(name="glucose3") # Low-level SAT solver
|
42
|
+
self.propagated_clauses = 0 # Add clause counter
|
43
|
+
|
44
|
+
def _count_clause(self) -> None:
|
45
|
+
self.propagated_clauses += 1
|
46
|
+
|
47
|
+
def add_sudoku_clauses(self) -> None:
|
48
|
+
size = self.size
|
49
|
+
block_size = int(size**0.5)
|
50
|
+
|
51
|
+
def get_var(row, col, num):
|
52
|
+
return row * size * size + col * size + num
|
53
|
+
|
54
|
+
# At least one number in each cell
|
55
|
+
for row in range(size):
|
56
|
+
for col in range(size):
|
57
|
+
self.cnf.append([get_var(row, col, num) for num in range(1, size + 1)])
|
58
|
+
self._count_clause()
|
59
|
+
|
60
|
+
# At most one number in each cell
|
61
|
+
for num1 in range(1, size + 1):
|
62
|
+
for num2 in range(num1 + 1, size + 1):
|
63
|
+
self.cnf.append(
|
64
|
+
[-get_var(row, col, num1), -get_var(row, col, num2)]
|
65
|
+
)
|
66
|
+
self._count_clause()
|
67
|
+
|
68
|
+
# Add row constraints
|
69
|
+
for row in range(size):
|
70
|
+
for num in range(1, size + 1):
|
71
|
+
self.cnf.append([get_var(row, col, num) for col in range(size)])
|
72
|
+
self._count_clause()
|
73
|
+
|
74
|
+
# Add column constraints
|
75
|
+
for col in range(size):
|
76
|
+
for num in range(1, size + 1):
|
77
|
+
self.cnf.append([get_var(row, col, num) for row in range(size)])
|
78
|
+
self._count_clause()
|
79
|
+
|
80
|
+
# Add block constraints
|
81
|
+
for block_row in range(block_size):
|
82
|
+
for block_col in range(block_size):
|
83
|
+
for num in range(1, size + 1):
|
84
|
+
self.cnf.append(
|
85
|
+
[
|
86
|
+
get_var(
|
87
|
+
block_row * block_size + i,
|
88
|
+
block_col * block_size + j,
|
89
|
+
num,
|
90
|
+
)
|
91
|
+
for i in range(block_size)
|
92
|
+
for j in range(block_size)
|
93
|
+
]
|
94
|
+
)
|
95
|
+
self._count_clause()
|
96
|
+
|
97
|
+
# Add initial assignments from the puzzle
|
98
|
+
for row in range(size):
|
99
|
+
for col in range(size):
|
100
|
+
if self.sudoku[row][col] != 0:
|
101
|
+
num = self.sudoku[row][col]
|
102
|
+
self.cnf.append([get_var(row, col, num)])
|
103
|
+
self._count_clause()
|
104
|
+
|
105
|
+
def extract_solution(self, model: List[int]) -> List[List[int]]:
|
106
|
+
solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
107
|
+
for var in model:
|
108
|
+
if var > 0: # Only consider positive assignments
|
109
|
+
var -= 1
|
110
|
+
num = var % self.size + 1
|
111
|
+
col = (var // self.size) % self.size
|
112
|
+
row = var // (self.size * self.size)
|
113
|
+
solution[row][col] = num
|
114
|
+
return solution
|
115
|
+
|
116
|
+
def validate_solution(self, solution: List[List[int]]) -> bool:
|
117
|
+
size = self.size
|
118
|
+
block_size = int(size**0.5)
|
119
|
+
|
120
|
+
# Validate rows
|
121
|
+
for row in solution:
|
122
|
+
if len(set(row)) != size or not all(1 <= num <= size for num in row):
|
123
|
+
return False
|
124
|
+
|
125
|
+
# Validate columns
|
126
|
+
for col in range(size):
|
127
|
+
column = [solution[row][col] for row in range(size)]
|
128
|
+
if len(set(column)) != size:
|
129
|
+
return False
|
130
|
+
|
131
|
+
# Validate blocks
|
132
|
+
for block_row in range(block_size):
|
133
|
+
for block_col in range(block_size):
|
134
|
+
block = [
|
135
|
+
solution[block_row * block_size + i][block_col * block_size + j]
|
136
|
+
for i in range(block_size)
|
137
|
+
for j in range(block_size)
|
138
|
+
]
|
139
|
+
if len(set(block)) != size:
|
140
|
+
return False
|
141
|
+
|
142
|
+
return True
|
143
|
+
|
144
|
+
def solve(self) -> Optional[List[List[int]]]:
|
145
|
+
"""Solve Sudoku puzzle using DPLL SAT solver.
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
Solved 25x25 grid if satisfiable, None if unsatisfiable
|
149
|
+
|
150
|
+
Raises:
|
151
|
+
SudokuError: If solver produces invalid solution
|
152
|
+
Exception: For other solver errors
|
153
|
+
|
154
|
+
Note:
|
155
|
+
Uses Glucose3 SAT solver from PySAT
|
156
|
+
"""
|
157
|
+
self.add_sudoku_clauses()
|
158
|
+
self.solver.append_formula(self.cnf.clauses)
|
159
|
+
|
160
|
+
try:
|
161
|
+
if self.solver.solve():
|
162
|
+
# Extract and validate the solution
|
163
|
+
model = self.solver.get_model()
|
164
|
+
solution = self.extract_solution(model)
|
165
|
+
|
166
|
+
if self.validate_solution(solution):
|
167
|
+
return solution
|
168
|
+
else:
|
169
|
+
raise SudokuError("Invalid solution generated.")
|
170
|
+
else:
|
171
|
+
# If unsat, return None
|
172
|
+
return None
|
173
|
+
|
174
|
+
except Exception as e:
|
175
|
+
raise
|
@@ -0,0 +1,211 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
from pysat.solvers import Solver
|
3
|
+
from pysat.formula import CNF
|
4
|
+
from .sudoku_error import SudokuError
|
5
|
+
|
6
|
+
|
7
|
+
class DPLLTSolver:
|
8
|
+
"""DPLL(T) solver combining SAT solving with theory propagation.
|
9
|
+
|
10
|
+
Extends basic DPLL SAT solving with theory propagation for Sudoku rules,
|
11
|
+
enabling more efficient pruning of the search space.
|
12
|
+
|
13
|
+
Attributes:
|
14
|
+
sudoku (List[List[int]]): Input puzzle as 25x25 grid
|
15
|
+
size (int): Grid size (25)
|
16
|
+
cnf (CNF): PySAT CNF formula object
|
17
|
+
solver (Solver): PySAT Glucose3 solver
|
18
|
+
theory_state (dict): Dynamic tracking of theory constraints
|
19
|
+
decision_level (int): Current depth in decision tree
|
20
|
+
propagated_clauses (int): Counter for clause additions
|
21
|
+
|
22
|
+
Example:
|
23
|
+
>>> puzzle = [[0 for _ in range(25)] for _ in range(25)]
|
24
|
+
>>> solver = DPLLTSolver(puzzle)
|
25
|
+
>>> solution = solver.solve()
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, sudoku: List[List[int]]) -> None:
|
29
|
+
"""Initialize DPLL(T) solver with theory support.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
sudoku: 25x25 grid with values 0-25 (0 for empty cells)
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
SudokuError: If puzzle format is invalid
|
36
|
+
"""
|
37
|
+
if not sudoku or not isinstance(sudoku, list) or len(sudoku) != 25:
|
38
|
+
raise SudokuError("Invalid Sudoku puzzle: must be a 25x25 grid")
|
39
|
+
|
40
|
+
self.sudoku = sudoku
|
41
|
+
self.size = 25
|
42
|
+
self.cnf = CNF() # CNF object to store Boolean clauses
|
43
|
+
self.solver = Solver(name="glucose3") # Low-level SAT solver
|
44
|
+
self.theory_state = {} # Store theory constraints dynamically
|
45
|
+
self.decision_level = 0
|
46
|
+
self.propagated_clauses = 0
|
47
|
+
|
48
|
+
def _count_clause(self) -> None:
|
49
|
+
self.propagated_clauses += 1
|
50
|
+
|
51
|
+
def add_sudoku_clauses(self) -> None:
|
52
|
+
size = self.size
|
53
|
+
block_size = int(size**0.5)
|
54
|
+
|
55
|
+
def get_var(row, col, num):
|
56
|
+
return row * size * size + col * size + num
|
57
|
+
|
58
|
+
# At least one number in each cell
|
59
|
+
for row in range(size):
|
60
|
+
for col in range(size):
|
61
|
+
self.cnf.append([get_var(row, col, num) for num in range(1, size + 1)])
|
62
|
+
self._count_clause()
|
63
|
+
|
64
|
+
# At most one number in each cell
|
65
|
+
for num1 in range(1, size + 1):
|
66
|
+
for num2 in range(num1 + 1, size + 1):
|
67
|
+
self.cnf.append(
|
68
|
+
[-get_var(row, col, num1), -get_var(row, col, num2)]
|
69
|
+
)
|
70
|
+
self._count_clause()
|
71
|
+
|
72
|
+
# Add row constraints
|
73
|
+
for row in range(size):
|
74
|
+
for num in range(1, size + 1):
|
75
|
+
self.cnf.append([get_var(row, col, num) for col in range(size)])
|
76
|
+
self._count_clause()
|
77
|
+
|
78
|
+
# Add column constraints
|
79
|
+
for col in range(size):
|
80
|
+
for num in range(1, size + 1):
|
81
|
+
self.cnf.append([get_var(row, col, num) for row in range(size)])
|
82
|
+
self._count_clause()
|
83
|
+
|
84
|
+
# Add block constraints
|
85
|
+
for block_row in range(block_size):
|
86
|
+
for block_col in range(block_size):
|
87
|
+
for num in range(1, size + 1):
|
88
|
+
self.cnf.append(
|
89
|
+
[
|
90
|
+
get_var(
|
91
|
+
block_row * block_size + i,
|
92
|
+
block_col * block_size + j,
|
93
|
+
num,
|
94
|
+
)
|
95
|
+
for i in range(block_size)
|
96
|
+
for j in range(block_size)
|
97
|
+
]
|
98
|
+
)
|
99
|
+
self._count_clause()
|
100
|
+
|
101
|
+
# Add initial assignments from the puzzle
|
102
|
+
for row in range(size):
|
103
|
+
for col in range(size):
|
104
|
+
if self.sudoku[row][col] != 0:
|
105
|
+
num = self.sudoku[row][col]
|
106
|
+
self.cnf.append([get_var(row, col, num)])
|
107
|
+
self._count_clause()
|
108
|
+
|
109
|
+
def theory_propagation(self) -> Optional[List[int]]:
|
110
|
+
block_size = int(self.size**0.5)
|
111
|
+
|
112
|
+
def block_index(row, col):
|
113
|
+
return (row // block_size) * block_size + (col // block_size)
|
114
|
+
|
115
|
+
# Track constraints dynamically
|
116
|
+
for row in range(self.size):
|
117
|
+
for col in range(self.size):
|
118
|
+
if self.sudoku[row][col] != 0:
|
119
|
+
num = self.sudoku[row][col]
|
120
|
+
# Check row, column, and block constraints
|
121
|
+
if num in self.theory_state.get((row, "row"), set()):
|
122
|
+
return [-self.get_var(row, col, num)]
|
123
|
+
if num in self.theory_state.get((col, "col"), set()):
|
124
|
+
return [-self.get_var(row, col, num)]
|
125
|
+
if num in self.theory_state.get(
|
126
|
+
(block_index(row, col), "block"), set()
|
127
|
+
):
|
128
|
+
return [-self.get_var(row, col, num)]
|
129
|
+
|
130
|
+
# Add constraints to theory state
|
131
|
+
self.theory_state.setdefault((row, "row"), set()).add(num)
|
132
|
+
self.theory_state.setdefault((col, "col"), set()).add(num)
|
133
|
+
self.theory_state.setdefault(
|
134
|
+
(block_index(row, col), "block"), set()
|
135
|
+
).add(num)
|
136
|
+
return None
|
137
|
+
|
138
|
+
def extract_solution(self, model: List[int]) -> List[List[int]]:
|
139
|
+
"""Convert SAT model to Sudoku grid."""
|
140
|
+
solution = [[0 for _ in range(self.size)] for _ in range(self.size)]
|
141
|
+
for var in model:
|
142
|
+
if var > 0: # Only consider positive assignments
|
143
|
+
var -= 1
|
144
|
+
num = var % self.size + 1
|
145
|
+
col = (var // self.size) % self.size
|
146
|
+
row = var // (self.size * self.size)
|
147
|
+
solution[row][col] = num
|
148
|
+
return solution
|
149
|
+
|
150
|
+
def validate_solution(self, solution: List[List[int]]) -> bool:
|
151
|
+
size = self.size
|
152
|
+
block_size = int(size**0.5)
|
153
|
+
|
154
|
+
# Validate rows
|
155
|
+
for row in solution:
|
156
|
+
if len(set(row)) != size or not all(1 <= num <= size for num in row):
|
157
|
+
return False
|
158
|
+
|
159
|
+
# Validate columns
|
160
|
+
for col in range(size):
|
161
|
+
column = [solution[row][col] for row in range(size)]
|
162
|
+
if len(set(column)) != size:
|
163
|
+
return False
|
164
|
+
|
165
|
+
# Validate blocks
|
166
|
+
for block_row in range(block_size):
|
167
|
+
for block_col in range(block_size):
|
168
|
+
block = [
|
169
|
+
solution[block_row * block_size + i][block_col * block_size + j]
|
170
|
+
for i in range(block_size)
|
171
|
+
for j in range(block_size)
|
172
|
+
]
|
173
|
+
if len(set(block)) != size:
|
174
|
+
return False
|
175
|
+
|
176
|
+
return True
|
177
|
+
|
178
|
+
def solve(self) -> Optional[List[List[int]]]:
|
179
|
+
"""Solve Sudoku using DPLL(T) algorithm.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
Solved 25x25 grid if satisfiable, None if unsatisfiable
|
183
|
+
|
184
|
+
Raises:
|
185
|
+
SudokuError: If solver produces invalid solution
|
186
|
+
|
187
|
+
Note:
|
188
|
+
Combines SAT solving with theory propagation in DPLL(T) style
|
189
|
+
"""
|
190
|
+
"""Solve the Sudoku puzzle using DPLL(T)."""
|
191
|
+
self.add_sudoku_clauses()
|
192
|
+
self.solver.append_formula(self.cnf.clauses)
|
193
|
+
|
194
|
+
while self.solver.solve():
|
195
|
+
# Perform theory propagation
|
196
|
+
conflict_clause = self.theory_propagation()
|
197
|
+
if conflict_clause:
|
198
|
+
# Add conflict clause and continue solving
|
199
|
+
self.solver.add_clause(conflict_clause)
|
200
|
+
self._count_clause()
|
201
|
+
else:
|
202
|
+
# Extract and validate the solution
|
203
|
+
model = self.solver.get_model()
|
204
|
+
solution = self.extract_solution(model)
|
205
|
+
if self.validate_solution(solution):
|
206
|
+
return solution
|
207
|
+
else:
|
208
|
+
raise SudokuError("Invalid solution generated.")
|
209
|
+
|
210
|
+
# If UNSAT, return None
|
211
|
+
return None
|
@@ -0,0 +1,160 @@
|
|
1
|
+
from z3 import Solver, Int, Distinct, sat
|
2
|
+
from sudoku_smt_solvers.solvers.sudoku_error import SudokuError
|
3
|
+
|
4
|
+
|
5
|
+
class Z3Solver:
|
6
|
+
"""Z3-based SMT solver for Sudoku puzzles.
|
7
|
+
|
8
|
+
Uses integer variables and distinct constraints to encode Sudoku rules.
|
9
|
+
Tracks constraint propagation for performance analysis.
|
10
|
+
|
11
|
+
Attributes:
|
12
|
+
sudoku (List[List[int]]): Input puzzle as 25x25 grid
|
13
|
+
size (int): Grid size (25)
|
14
|
+
solver (z3.Solver): Z3 solver instance
|
15
|
+
variables (List[List[z3.Int]]): SMT variables for grid
|
16
|
+
propagated_clauses (int): Counter for constraint additions
|
17
|
+
|
18
|
+
Example:
|
19
|
+
>>> puzzle = [[0 for _ in range(25)] for _ in range(25)]
|
20
|
+
>>> solver = Z3Solver(puzzle)
|
21
|
+
>>> solution = solver.solve()
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, sudoku):
|
25
|
+
"""Initialize Z3 Sudoku solver.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
sudoku: 25x25 grid with values 0-25 (0 for empty cells)
|
29
|
+
|
30
|
+
Raises:
|
31
|
+
SudokuError: If puzzle format is invalid
|
32
|
+
"""
|
33
|
+
if not sudoku or not isinstance(sudoku, list) or len(sudoku) != 25:
|
34
|
+
raise SudokuError("Invalid Sudoku puzzle: must be a 25x25 grid")
|
35
|
+
|
36
|
+
self.sudoku = sudoku
|
37
|
+
self.size = len(sudoku)
|
38
|
+
self.solver = None
|
39
|
+
self.variables = None
|
40
|
+
self.propagated_clauses = 0
|
41
|
+
|
42
|
+
def create_variables(self):
|
43
|
+
self.variables = [
|
44
|
+
[Int(f"x_{i}_{j}") for j in range(self.size)] for i in range(self.size)
|
45
|
+
]
|
46
|
+
|
47
|
+
def _count_clause(self):
|
48
|
+
self.propagated_clauses += 1
|
49
|
+
|
50
|
+
def encode_rules(self):
|
51
|
+
# Cell range constraints
|
52
|
+
cell_constraints = []
|
53
|
+
for i in range(self.size):
|
54
|
+
for j in range(self.size):
|
55
|
+
cell_constraints.append(1 <= self.variables[i][j])
|
56
|
+
cell_constraints.append(self.variables[i][j] <= 25)
|
57
|
+
self._count_clause()
|
58
|
+
self._count_clause()
|
59
|
+
self.solver.add(cell_constraints)
|
60
|
+
|
61
|
+
# Row constraints
|
62
|
+
row_constraints = [Distinct(self.variables[i]) for i in range(self.size)]
|
63
|
+
self.solver.add(row_constraints)
|
64
|
+
for _ in range(self.size):
|
65
|
+
self._count_clause()
|
66
|
+
|
67
|
+
# Column constraints
|
68
|
+
col_constraints = [
|
69
|
+
Distinct([self.variables[i][j] for i in range(self.size)])
|
70
|
+
for j in range(self.size)
|
71
|
+
]
|
72
|
+
self.solver.add(col_constraints)
|
73
|
+
for _ in range(self.size):
|
74
|
+
self._count_clause()
|
75
|
+
|
76
|
+
# Box constraints
|
77
|
+
box_constraints = [
|
78
|
+
Distinct(
|
79
|
+
[
|
80
|
+
self.variables[5 * box_i + i][5 * box_j + j]
|
81
|
+
for i in range(5)
|
82
|
+
for j in range(5)
|
83
|
+
]
|
84
|
+
)
|
85
|
+
for box_i in range(5)
|
86
|
+
for box_j in range(5)
|
87
|
+
]
|
88
|
+
self.solver.add(box_constraints)
|
89
|
+
for _ in range(25):
|
90
|
+
self._count_clause()
|
91
|
+
|
92
|
+
def encode_puzzle(self):
|
93
|
+
initial_values = []
|
94
|
+
for i in range(self.size):
|
95
|
+
for j in range(self.size):
|
96
|
+
if self.sudoku[i][j] != 0:
|
97
|
+
initial_values.append(self.variables[i][j] == self.sudoku[i][j])
|
98
|
+
self._count_clause()
|
99
|
+
self.solver.add(initial_values)
|
100
|
+
|
101
|
+
def extract_solution(self, model):
|
102
|
+
return [
|
103
|
+
[model.evaluate(self.variables[i][j]).as_long() for j in range(self.size)]
|
104
|
+
for i in range(self.size)
|
105
|
+
]
|
106
|
+
|
107
|
+
def validate_solution(self, solution):
|
108
|
+
# Check range
|
109
|
+
for row in solution:
|
110
|
+
if not all(1 <= num <= 25 for num in row):
|
111
|
+
return False
|
112
|
+
|
113
|
+
# Check rows
|
114
|
+
for row in solution:
|
115
|
+
if len(set(row)) != self.size:
|
116
|
+
return False
|
117
|
+
|
118
|
+
# Check columns
|
119
|
+
for j in range(self.size):
|
120
|
+
col = [solution[i][j] for i in range(self.size)]
|
121
|
+
if len(set(col)) != self.size:
|
122
|
+
return False
|
123
|
+
|
124
|
+
# Check boxes
|
125
|
+
for box_i in range(5):
|
126
|
+
for box_j in range(5):
|
127
|
+
box = [
|
128
|
+
solution[5 * box_i + i][5 * box_j + j]
|
129
|
+
for i in range(5)
|
130
|
+
for j in range(5)
|
131
|
+
]
|
132
|
+
if len(set(box)) != self.size:
|
133
|
+
return False
|
134
|
+
|
135
|
+
return True
|
136
|
+
|
137
|
+
def solve(self):
|
138
|
+
"""Solve Sudoku using Z3 SMT solver.
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Solved 25x25 grid if satisfiable, None if unsatisfiable
|
142
|
+
|
143
|
+
Note:
|
144
|
+
Validates solution before returning to ensure correctness
|
145
|
+
"""
|
146
|
+
self.solver = Solver()
|
147
|
+
self.create_variables()
|
148
|
+
self.encode_rules()
|
149
|
+
self.encode_puzzle()
|
150
|
+
|
151
|
+
result = self.solver.check()
|
152
|
+
|
153
|
+
if result == sat:
|
154
|
+
model = self.solver.model()
|
155
|
+
solution = self.extract_solution(model)
|
156
|
+
|
157
|
+
if self.validate_solution(solution):
|
158
|
+
return solution
|
159
|
+
|
160
|
+
return None
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 liamjdavis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,161 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: sudoku_smt_solvers
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A collection of SAT and SMT solvers for solving Sudoku puzzles
|
5
|
+
Home-page: https://liamjdavis.github.io/sudoku-smt-solvers
|
6
|
+
Author: Liam Davis
|
7
|
+
Author-email: ljdavis27@amherst.edu
|
8
|
+
License: MIT
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Requires-Python: >=3.9
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
License-File: LICENSE
|
15
|
+
Requires-Dist: cvc5
|
16
|
+
Requires-Dist: pysat
|
17
|
+
Requires-Dist: z3-solver
|
18
|
+
|
19
|
+
# Sudoku-SMT-Solvers
|
20
|
+
|
21
|
+
[](ttps://github.com/liamjdavis/Sudoku-SMT-Solvers/actions/workflows/test.yml)
|
22
|
+
[](https://coveralls.io/github/liamjdavis/Sudoku-SMT-Solvers)
|
23
|
+
[](https://github.com/liamjdavis/Sudoku-SMT-Solvers/actions/workflows/docs.yml)
|
24
|
+
[](https://liamjdavis.github.io/sudoku-smt-solvers)
|
25
|
+
|
26
|
+
|
27
|
+
## About
|
28
|
+
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Ryan Ji (@TairanJ) as their for COSC-241 Artificial Intelligence at Amherst College, it evaluates the efficacy of SMT-Based Solvers by benchmarking three modern SMT solvers (DPLL(T), Z3, and CVC5) against the DPLL algorithm on a collection of 100 25x25 Sudoku puzzles of varying difficulty.
|
29
|
+
|
30
|
+
Along with the study, we also published `sudoku-smt-solvers`, a Python package that provides the various SMT-based Sudoku solvers and benchmarking tools we built for this study. The package features DPLL(T), Z3, and CVC5 solvers optimized for 25x25 Sudoku puzzles, a puzzle generator for creating test cases, and a comprehensive benchmarking suite. Available through pip, it offers a simple API for solving Sudoku puzzles using state-of-the-art SMT solvers while facilitating performance comparisons between different solving approaches.
|
31
|
+
|
32
|
+
The study aims to answer three research questions:
|
33
|
+
1. How have logical solvers evolved over time in terms of performance and capability?
|
34
|
+
2. How do different encodings of Sudoku affect the efficiency and scalability of these solvers?
|
35
|
+
3. Are there specific features or optimizations in SMT solvers that provide a significant advantage over traditional SAT solvers for this class of problem?
|
36
|
+
|
37
|
+
## Getting started
|
38
|
+
### Installation
|
39
|
+
To run the code locally, you can install with `pip`
|
40
|
+
|
41
|
+
```bash
|
42
|
+
pip install sudoku-smt-solvers
|
43
|
+
```
|
44
|
+
|
45
|
+
### Solvers
|
46
|
+
This package includes the DPLL solver and three modern SMT solvers:
|
47
|
+
* DPLL(T)
|
48
|
+
* CVC5
|
49
|
+
* Z3
|
50
|
+
|
51
|
+
To run any of the solvers on a 25x25 Sudoku puzzle, you can create an instance of the solver class and call the solve method in a file at the root (Sudoku-smt-solvers). Here is an example using Z3:
|
52
|
+
|
53
|
+
```python
|
54
|
+
from sudoku_smt_solvers.solvers.z3_solver import Z3Solver
|
55
|
+
|
56
|
+
# Example grid (25x25)
|
57
|
+
grid = [[0] * 25 for _ in range(25)]
|
58
|
+
solver = Z3Solver(grid)
|
59
|
+
solution = solver.solve()
|
60
|
+
|
61
|
+
if solution:
|
62
|
+
print(f"Solution:\n\n{solution}")
|
63
|
+
else:
|
64
|
+
print("No solution exists.")
|
65
|
+
```
|
66
|
+
|
67
|
+
### Sudoku Generator
|
68
|
+
This package also includes a generator for creating Sudoku puzzles to be used as benchmarks. To generate a puzzle, create an instance of the `SudokuGenerator` class and call the `generate` method. Here is an example:
|
69
|
+
|
70
|
+
```python
|
71
|
+
from sudoku_smt_solvers.benchmarks.sudoku_generator.sudoku_generator import SudokuGenerator
|
72
|
+
|
73
|
+
generator = SudokuGenerator(size = 25, givens = 80, timeout = 5, difficulty = "Medium", puzzles_dir = "benchmarks/puzzles", solutions_dir = "benchmarks/solutions")
|
74
|
+
|
75
|
+
generator.generate()
|
76
|
+
```
|
77
|
+
|
78
|
+
Due to the computational complexity of generating large sudoku puzzles, it is recommended that you run multiple generator instances in parallel to create benchmarks.
|
79
|
+
|
80
|
+
### Benchmark Runner
|
81
|
+
To run the benchmarks you created on all four solvers, create an instance of the `BenchmarkRunner` class and call the `run_benchmarks` method. Here is an example:
|
82
|
+
|
83
|
+
```python
|
84
|
+
from sudoku_smt_solvers.benchmarks.benchmark_runner import BenchmarkRunner
|
85
|
+
|
86
|
+
runner = BenchmarkRunner(
|
87
|
+
puzzles_dir='resources/benchmarks/puzzles/',
|
88
|
+
solutions_dir='resources/benchmarks/solutions/',
|
89
|
+
results_dir='results/'
|
90
|
+
)
|
91
|
+
runner.run_benchmarks()
|
92
|
+
```
|
93
|
+
|
94
|
+
## Contributing
|
95
|
+
|
96
|
+
We welcome contributions in the form of new solvers, additions to our benchmark suite, or anything that improves the tool! Here's how to get started:
|
97
|
+
|
98
|
+
### Development Setup
|
99
|
+
|
100
|
+
1. **Fork and Clone**:
|
101
|
+
Begin by forking the repository and cloning your fork locally:
|
102
|
+
```bash
|
103
|
+
git clone https://github.com/yourusername/Sudoku-SMT-Solvers.git
|
104
|
+
cd Sudoku-SMT-Solvers
|
105
|
+
```
|
106
|
+
|
107
|
+
2. **Create and Activate a Virtual Environment**:
|
108
|
+
Set up a Python virtual environment to isolate your dependencies:
|
109
|
+
```bash
|
110
|
+
python3 -m venv venv
|
111
|
+
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
|
112
|
+
```
|
113
|
+
|
114
|
+
3. **Install Dependencies**:
|
115
|
+
Install the required dependencies from the `requirements.txt` file:
|
116
|
+
```bash
|
117
|
+
pip install -r requirements.txt
|
118
|
+
```
|
119
|
+
|
120
|
+
4. **Set Up Pre-Commit Hooks**:
|
121
|
+
Install and configure pre-commit hooks to maintain code quality:
|
122
|
+
```bash
|
123
|
+
pip install pre-commit
|
124
|
+
pre-commit install
|
125
|
+
```
|
126
|
+
|
127
|
+
To manually run the hooks and verify code compliance, use:
|
128
|
+
```bash
|
129
|
+
pre-commit run
|
130
|
+
```
|
131
|
+
|
132
|
+
5. **Testing and Coverage Requirements**:
|
133
|
+
- Write tests for any new code or modifications.
|
134
|
+
- Use `pytest` for running tests:
|
135
|
+
```bash
|
136
|
+
pytest
|
137
|
+
```
|
138
|
+
- Ensure the test coverage is at least 90%:
|
139
|
+
|
140
|
+
6. **Add and Commit Your Changes**:
|
141
|
+
- Follow the existing code style and structure.
|
142
|
+
- Verify that all pre-commit hooks pass and the test coverage meets the minimum requirement.
|
143
|
+
```bash
|
144
|
+
git add .
|
145
|
+
git commit -m "Description of your changes"
|
146
|
+
```
|
147
|
+
|
148
|
+
7. **Push Your Branch**:
|
149
|
+
Push your changes to your forked repository:
|
150
|
+
```bash
|
151
|
+
git push origin your-branch-name
|
152
|
+
```
|
153
|
+
|
154
|
+
8. **Open a PR for us to review**
|
155
|
+
---
|
156
|
+
|
157
|
+
Thank you for your interest in contributing to Sudoku-SMT-Solvers! Your efforts help make this project better for everyone.
|
158
|
+
|
159
|
+
|
160
|
+
## Contact Us
|
161
|
+
For any questions or support, please reach out to Liam at ljdavis27 at amherst.edu and Ryan at tji26 at amherst.edu
|
@@ -0,0 +1,14 @@
|
|
1
|
+
sudoku_smt_solvers/__init__.py,sha256=vTwJLa8Yp7q-OdzA44zhFIK-LhK2o8aDt-4qOJJ1M7M,134
|
2
|
+
sudoku_smt_solvers/benchmarks/__init__.py,sha256=lMrK_yj_otywN4dMvvVFtTzyTdamS_4nUgjm-k07obU,271
|
3
|
+
sudoku_smt_solvers/benchmarks/benchmark_runner.py,sha256=Mc87ul-6VkWMomtlmOMc9GXmC4AwfQUwIWKDjeFvSVA,7712
|
4
|
+
sudoku_smt_solvers/solvers/__init__.py,sha256=5qFfWzKN2WPDoFLN5ye6Ly5BqEaUdk2ks_jPP3c53l8,142
|
5
|
+
sudoku_smt_solvers/solvers/cvc5_solver.py,sha256=DfLlgzhhgSaI3tV_4mzy3vdpxY5tISSFhs0xhRWcMYk,6849
|
6
|
+
sudoku_smt_solvers/solvers/dpll_solver.py,sha256=ThLT1v87oNnzDMpYmoPcmTeeXijVIgk6A6qULWVID28,6138
|
7
|
+
sudoku_smt_solvers/solvers/dpllt_solver.py,sha256=4UXcod7EnJGlD7OlakmPHOuIyWga5yN9rMtKMvuEAc8,7986
|
8
|
+
sudoku_smt_solvers/solvers/sudoku_error.py,sha256=iUcv1QCgQ7anov0b-AtIB1fbfZ3yWfci4eTp_8RuUJg,208
|
9
|
+
sudoku_smt_solvers/solvers/z3_solver.py,sha256=awQ3tzEMIy2woFmATMiwQsC2YtktxfJlx55MudB1SN0,4922
|
10
|
+
sudoku_smt_solvers-0.1.0.dist-info/LICENSE,sha256=PbuZlvluV1l4HMMfPAVe5yjVvFGBK9DFp20JNhoJ8bI,1067
|
11
|
+
sudoku_smt_solvers-0.1.0.dist-info/METADATA,sha256=lbOf7Pgg4nsMlchZ54WHWcJoGa98BvgCMHCMieULSrw,6414
|
12
|
+
sudoku_smt_solvers-0.1.0.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
13
|
+
sudoku_smt_solvers-0.1.0.dist-info/top_level.txt,sha256=Ww9vs8KC4aujzfGfddMl_X8Qzh-Cywn9aBTLQgemi5A,19
|
14
|
+
sudoku_smt_solvers-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
sudoku_smt_solvers
|