sudoku-smt-solvers 0.3.0__tar.gz → 1.0.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {sudoku_smt_solvers-0.3.0/sudoku_smt_solvers.egg-info → sudoku_smt_solvers-1.0.0}/PKG-INFO +5 -6
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/README.md +4 -5
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/__init__.py +35 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/__init__.py +3 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/sudoku_generator/__init__.py +6 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/sudoku_generator/dfs_solver.py +99 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/sudoku_generator/hole_digger.py +144 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/sudoku_generator/las_vegas.py +150 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/benchmarks/sudoku_generator/sudoku_generator.py +113 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/__init__.py +2 -1
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/solvers/utils/__init__.py +3 -0
- sudoku_smt_solvers-1.0.0/sudoku_smt_solvers/solvers/utils/sudoku_error.py +8 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0/sudoku_smt_solvers.egg-info}/PKG-INFO +5 -6
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/SOURCES.txt +7 -0
- sudoku_smt_solvers-0.3.0/sudoku_smt_solvers/__init__.py +0 -19
- sudoku_smt_solvers-0.3.0/sudoku_smt_solvers/benchmarks/__init__.py +0 -5
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/LICENSE +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/pyproject.toml +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/setup.cfg +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/setup.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/benchmarks/benchmark_runner.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/cvc5_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/dpll_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/dpllt_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/z3_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/dependency_links.txt +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/requires.txt +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/top_level.txt +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_benchmark_runner.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_cvc5_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_dfs_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_dpll_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_dpllt_solver.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_hole_digger.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_las_vegas.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_parser.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_profiler.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_sudoku_generator.py +0 -0
- {sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/tests/test_z3_solver.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: sudoku_smt_solvers
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: A collection of SAT and SMT solvers for solving Sudoku puzzles
|
5
5
|
Home-page: https://liamjdavis.github.io/sudoku-smt-solvers
|
6
6
|
Author: Liam Davis, Tairan 'Ryan' Ji
|
@@ -35,7 +35,7 @@ Dynamic: summary
|
|
35
35
|
|
36
36
|
|
37
37
|
## About
|
38
|
-
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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.
|
38
|
+
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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. The corresponding paper can be found [here](https://arxiv.org/abs/2501.08569).
|
39
39
|
|
40
40
|
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.
|
41
41
|
|
@@ -61,7 +61,7 @@ This package includes the DPLL solver and three modern SMT solvers:
|
|
61
61
|
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:
|
62
62
|
|
63
63
|
```python
|
64
|
-
from sudoku_smt_solvers
|
64
|
+
from sudoku_smt_solvers import Z3Solver
|
65
65
|
|
66
66
|
# Example grid (25x25)
|
67
67
|
grid = [[0] * 25 for _ in range(25)]
|
@@ -78,7 +78,7 @@ else:
|
|
78
78
|
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:
|
79
79
|
|
80
80
|
```python
|
81
|
-
from sudoku_smt_solvers
|
81
|
+
from sudoku_smt_solvers import SudokuGenerator
|
82
82
|
|
83
83
|
generator = SudokuGenerator(size = 25, givens = 80, timeout = 5, difficulty = "Medium", puzzles_dir = "benchmarks/puzzles", solutions_dir = "benchmarks/solutions")
|
84
84
|
|
@@ -91,11 +91,10 @@ Due to the computational complexity of generating large sudoku puzzles, it is re
|
|
91
91
|
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:
|
92
92
|
|
93
93
|
```python
|
94
|
-
from sudoku_smt_solvers
|
94
|
+
from sudoku_smt_solvers import BenchmarkRunner
|
95
95
|
|
96
96
|
runner = BenchmarkRunner(
|
97
97
|
puzzles_dir='resources/benchmarks/puzzles/',
|
98
|
-
solutions_dir='resources/benchmarks/solutions/',
|
99
98
|
results_dir='results/'
|
100
99
|
)
|
101
100
|
runner.run_benchmarks()
|
@@ -7,7 +7,7 @@
|
|
7
7
|
|
8
8
|
|
9
9
|
## About
|
10
|
-
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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.
|
10
|
+
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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. The corresponding paper can be found [here](https://arxiv.org/abs/2501.08569).
|
11
11
|
|
12
12
|
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.
|
13
13
|
|
@@ -33,7 +33,7 @@ This package includes the DPLL solver and three modern SMT solvers:
|
|
33
33
|
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:
|
34
34
|
|
35
35
|
```python
|
36
|
-
from sudoku_smt_solvers
|
36
|
+
from sudoku_smt_solvers import Z3Solver
|
37
37
|
|
38
38
|
# Example grid (25x25)
|
39
39
|
grid = [[0] * 25 for _ in range(25)]
|
@@ -50,7 +50,7 @@ else:
|
|
50
50
|
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:
|
51
51
|
|
52
52
|
```python
|
53
|
-
from sudoku_smt_solvers
|
53
|
+
from sudoku_smt_solvers import SudokuGenerator
|
54
54
|
|
55
55
|
generator = SudokuGenerator(size = 25, givens = 80, timeout = 5, difficulty = "Medium", puzzles_dir = "benchmarks/puzzles", solutions_dir = "benchmarks/solutions")
|
56
56
|
|
@@ -63,11 +63,10 @@ Due to the computational complexity of generating large sudoku puzzles, it is re
|
|
63
63
|
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:
|
64
64
|
|
65
65
|
```python
|
66
|
-
from sudoku_smt_solvers
|
66
|
+
from sudoku_smt_solvers import BenchmarkRunner
|
67
67
|
|
68
68
|
runner = BenchmarkRunner(
|
69
69
|
puzzles_dir='resources/benchmarks/puzzles/',
|
70
|
-
solutions_dir='resources/benchmarks/solutions/',
|
71
70
|
results_dir='results/'
|
72
71
|
)
|
73
72
|
runner.run_benchmarks()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""Sudoku SMT Solvers - A package for solving and benchmarking large-scale Sudoku puzzles.
|
2
|
+
|
3
|
+
This package provides various SAT and SMT-based solvers optimized for 25x25 Sudoku puzzles,
|
4
|
+
along with tools for puzzle generation and solver benchmarking.
|
5
|
+
|
6
|
+
Key Components:
|
7
|
+
- Multiple solver implementations (DPLL, DPLL(T), Z3, CVC5)
|
8
|
+
- Sudoku puzzle generator with difficulty settings
|
9
|
+
- Benchmarking suite for comparing solver performance
|
10
|
+
"""
|
11
|
+
|
12
|
+
from .solvers import CVC5Solver, DPLLSolver, DPLLTSolver, Z3Solver
|
13
|
+
from .solvers.utils import SudokuError
|
14
|
+
from .benchmarks import BenchmarkRunner
|
15
|
+
from .benchmarks.sudoku_generator import (
|
16
|
+
SudokuGenerator,
|
17
|
+
LasVegasGenerator,
|
18
|
+
HoleDigger,
|
19
|
+
DFSSolver,
|
20
|
+
)
|
21
|
+
|
22
|
+
__version__ = "1.0.0"
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
"CVC5Solver",
|
26
|
+
"DPLLSolver",
|
27
|
+
"DPLLTSolver",
|
28
|
+
"Z3Solver",
|
29
|
+
"BenchmarkRunner",
|
30
|
+
"SudokuGenerator",
|
31
|
+
"LasVegasGenerator",
|
32
|
+
"HoleDigger",
|
33
|
+
"DFSSolver",
|
34
|
+
"SudokuError",
|
35
|
+
]
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from math import sqrt
|
2
|
+
|
3
|
+
|
4
|
+
class DFSSolver:
|
5
|
+
def __init__(self, size=25, solutions_limit=-1):
|
6
|
+
"""Initialize solver with configurable grid size"""
|
7
|
+
self.size = size # Total grid size (default 25x25)
|
8
|
+
self.box_size = int(sqrt(size)) # Size of each sub-box (default 5x5)
|
9
|
+
self.solutions_limit = solutions_limit # Number of solutions to find (-1 for all solutions, n>0 for n solutions)
|
10
|
+
self.solutions_found = 0
|
11
|
+
self.rows = [set() for _ in range(self.size)]
|
12
|
+
self.cols = [set() for _ in range(self.size)]
|
13
|
+
self.boxes = [set() for _ in range(self.size)]
|
14
|
+
self.unfilled_positions = [] # Track positions of empty cells
|
15
|
+
|
16
|
+
self.box_lookup = [[0] * size for _ in range(size)]
|
17
|
+
for i in range(size):
|
18
|
+
for j in range(size):
|
19
|
+
self.box_lookup[i][j] = (i // self.box_size) * self.box_size + (
|
20
|
+
j // self.box_size
|
21
|
+
)
|
22
|
+
|
23
|
+
def setup_board(self, grid):
|
24
|
+
"""Set up initial board state"""
|
25
|
+
self.unfilled_positions.clear()
|
26
|
+
for i in range(self.size):
|
27
|
+
for j in range(self.size):
|
28
|
+
num = grid[i][j]
|
29
|
+
if num == 0:
|
30
|
+
self.unfilled_positions.append((i, j))
|
31
|
+
else:
|
32
|
+
self.rows[i].add(num)
|
33
|
+
self.cols[j].add(num)
|
34
|
+
self.boxes[self.box_lookup[i][j]].add(num)
|
35
|
+
|
36
|
+
def get_valid_numbers(self, row, col):
|
37
|
+
"""Get valid numbers for a cell using set operations"""
|
38
|
+
used = self.rows[row] | self.cols[col] | self.boxes[self.box_lookup[row][col]]
|
39
|
+
return set(range(1, self.size + 1)) - used
|
40
|
+
|
41
|
+
def solve(self, grid):
|
42
|
+
"""Main solving function using backtracking"""
|
43
|
+
self.setup_board(grid)
|
44
|
+
self.solutions_found = 0
|
45
|
+
solutions = []
|
46
|
+
|
47
|
+
def search():
|
48
|
+
"""Depth-first search implementation"""
|
49
|
+
if not self.unfilled_positions: # Found a solution
|
50
|
+
solutions.append([row[:] for row in grid])
|
51
|
+
self.solutions_found += 1
|
52
|
+
# Stop if we've found desired number of solutions
|
53
|
+
return (
|
54
|
+
self.solutions_limit > 0
|
55
|
+
and self.solutions_found >= self.solutions_limit
|
56
|
+
)
|
57
|
+
|
58
|
+
# Select cell with minimum valid numbers to reduce branching
|
59
|
+
min_candidates = float("inf")
|
60
|
+
min_pos = None
|
61
|
+
min_idx = None
|
62
|
+
|
63
|
+
for idx, (row, col) in enumerate(self.unfilled_positions):
|
64
|
+
valid_numbers = self.get_valid_numbers(row, col)
|
65
|
+
if len(valid_numbers) < min_candidates:
|
66
|
+
min_candidates = len(valid_numbers)
|
67
|
+
min_pos = (row, col)
|
68
|
+
min_idx = idx
|
69
|
+
if min_candidates == 0: # No valid numbers available
|
70
|
+
return False
|
71
|
+
|
72
|
+
row, col = min_pos
|
73
|
+
self.unfilled_positions.pop(min_idx)
|
74
|
+
|
75
|
+
for num in self.get_valid_numbers(row, col):
|
76
|
+
# Place number and update board state
|
77
|
+
grid[row][col] = num
|
78
|
+
self.rows[row].add(num)
|
79
|
+
self.cols[col].add(num)
|
80
|
+
self.boxes[self.box_lookup[row][col]].add(num)
|
81
|
+
|
82
|
+
if search(): # If search returns True, we should stop
|
83
|
+
return True
|
84
|
+
|
85
|
+
# Backtrack: remove number and restore board state
|
86
|
+
grid[row][col] = 0
|
87
|
+
self.rows[row].remove(num)
|
88
|
+
self.cols[col].remove(num)
|
89
|
+
self.boxes[self.box_lookup[row][col]].remove(num)
|
90
|
+
|
91
|
+
self.unfilled_positions.insert(min_idx, (row, col))
|
92
|
+
return False
|
93
|
+
|
94
|
+
search()
|
95
|
+
return (
|
96
|
+
solutions
|
97
|
+
if self.solutions_limit != 1
|
98
|
+
else (solutions[0] if solutions else [])
|
99
|
+
)
|
@@ -0,0 +1,144 @@
|
|
1
|
+
import random
|
2
|
+
from typing import List, Tuple
|
3
|
+
from .dfs_solver import DFSSolver
|
4
|
+
|
5
|
+
from .las_vegas import LasVegasGenerator
|
6
|
+
|
7
|
+
# A mapping from difficulty to the range of givens
|
8
|
+
difficulty_givensRange_mapping = {
|
9
|
+
"Extremely Easy": [382, float("inf")], # More than 386 given cells
|
10
|
+
"Easy": [274, 381],
|
11
|
+
"Medium": [243, 273],
|
12
|
+
"Difficult": [212, 242],
|
13
|
+
"Evil": [166, 211],
|
14
|
+
}
|
15
|
+
|
16
|
+
# A mapping from difficulty to the lower bound of givens in each row and column
|
17
|
+
difficulty_lower_bound_mapping = {
|
18
|
+
"Extremely Easy": 14,
|
19
|
+
"Easy": 11,
|
20
|
+
"Medium": 7,
|
21
|
+
"Difficult": 4,
|
22
|
+
"Evil": 0,
|
23
|
+
}
|
24
|
+
|
25
|
+
digging_sequence_mapping = {
|
26
|
+
"Extremely Easy": "random",
|
27
|
+
"Easy": "random",
|
28
|
+
"Medium": "Jumping one cell",
|
29
|
+
"Difficult": "Wandering along 'S'",
|
30
|
+
"Evil": "Left to right, top to bottom",
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
class HoleDigger:
|
35
|
+
def __init__(self, puzzle: List[List[int]], difficulty: str):
|
36
|
+
self.puzzle = puzzle
|
37
|
+
self.size = len(puzzle)
|
38
|
+
self.difficulty = difficulty
|
39
|
+
|
40
|
+
self.target_range = difficulty_givensRange_mapping[difficulty]
|
41
|
+
self.num_givens = (
|
42
|
+
random.randint(*self.target_range)
|
43
|
+
if self.target_range[1] != float("inf")
|
44
|
+
else random.randint(self.target_range[0], self.size**2)
|
45
|
+
)
|
46
|
+
|
47
|
+
self.lower_bound = difficulty_lower_bound_mapping[difficulty]
|
48
|
+
self.pattern = digging_sequence_mapping[difficulty]
|
49
|
+
|
50
|
+
self.can_be_dug = {(i, j) for i in range(self.size) for j in range(self.size)}
|
51
|
+
self.remaining_cells = self.size * self.size
|
52
|
+
|
53
|
+
def get_digging_sequence(self) -> List[Tuple[int, int]]:
|
54
|
+
if self.pattern == "random":
|
55
|
+
return self._random_pattern()
|
56
|
+
elif self.pattern == "Jumping one cell":
|
57
|
+
return self._jumping_pattern()
|
58
|
+
elif self.pattern == "Wandering along 'S'":
|
59
|
+
return self._s_pattern()
|
60
|
+
else: # "Left to right, top to bottom"
|
61
|
+
return self._linear_pattern()
|
62
|
+
|
63
|
+
def _random_pattern(self) -> List[Tuple[int, int]]:
|
64
|
+
cells = list(self.can_be_dug)
|
65
|
+
random.shuffle(cells)
|
66
|
+
return cells
|
67
|
+
|
68
|
+
def _jumping_pattern(self) -> List[Tuple[int, int]]:
|
69
|
+
sequence = []
|
70
|
+
# Forward pass - only on even rows (0, 2, 4...)
|
71
|
+
for i in range(0, self.size, 2):
|
72
|
+
for j in range(0, self.size, 2):
|
73
|
+
sequence.append((i, j))
|
74
|
+
|
75
|
+
# Backward pass - only on odd rows (1, 3, 5...)
|
76
|
+
for i in range(1, self.size, 2):
|
77
|
+
for j in range(self.size - 2, -1, -2):
|
78
|
+
sequence.append((i, j))
|
79
|
+
|
80
|
+
return sequence
|
81
|
+
|
82
|
+
def _s_pattern(self) -> List[Tuple[int, int]]:
|
83
|
+
sequence = []
|
84
|
+
for i in range(self.size):
|
85
|
+
row = range(self.size) if i % 2 == 0 else range(self.size - 1, -1, -1)
|
86
|
+
for j in row:
|
87
|
+
sequence.append((i, j))
|
88
|
+
return sequence
|
89
|
+
|
90
|
+
def _linear_pattern(self) -> List[Tuple[int, int]]:
|
91
|
+
return [(i, j) for i in range(self.size) for j in range(self.size)]
|
92
|
+
|
93
|
+
def pass_restrictions(self, row: int, col: int) -> bool:
|
94
|
+
# Skip if already dug
|
95
|
+
if self.puzzle[row][col] == 0:
|
96
|
+
return False
|
97
|
+
|
98
|
+
# Check if digging would violate minimum remaining cells
|
99
|
+
if self.remaining_cells - 1 < self.num_givens:
|
100
|
+
return False
|
101
|
+
|
102
|
+
# Check row constraint
|
103
|
+
row_remaining = sum(cell != 0 for cell in self.puzzle[row])
|
104
|
+
if row_remaining - 1 < self.lower_bound:
|
105
|
+
return False
|
106
|
+
|
107
|
+
# Check column constraint
|
108
|
+
col_remaining = sum(self.puzzle[i][col] != 0 for i in range(self.size))
|
109
|
+
if col_remaining - 1 < self.lower_bound:
|
110
|
+
return False
|
111
|
+
|
112
|
+
return True
|
113
|
+
|
114
|
+
def has_unique_solution(self) -> bool:
|
115
|
+
solver = DFSSolver(size=self.size, solutions_limit=2)
|
116
|
+
solutions = solver.solve(self.puzzle)
|
117
|
+
return len(solutions) == 1
|
118
|
+
|
119
|
+
def dig_holes(self) -> List[List[int]]:
|
120
|
+
digging_sequence = self.get_digging_sequence()
|
121
|
+
cells_to_check = set(digging_sequence)
|
122
|
+
|
123
|
+
while cells_to_check:
|
124
|
+
row, col = next(iter(cells_to_check))
|
125
|
+
|
126
|
+
if not self.pass_restrictions(row, col):
|
127
|
+
cells_to_check.remove((row, col))
|
128
|
+
continue
|
129
|
+
|
130
|
+
# Save current value in case we need to restore it
|
131
|
+
current_value = self.puzzle[row][col]
|
132
|
+
self.puzzle[row][col] = 0
|
133
|
+
|
134
|
+
if not self.has_unique_solution():
|
135
|
+
# Restore the value if digging creates multiple solutions
|
136
|
+
self.puzzle[row][col] = current_value
|
137
|
+
self.can_be_dug.remove((row, col))
|
138
|
+
else:
|
139
|
+
# Update remaining cells if digging is successful
|
140
|
+
self.remaining_cells -= 1
|
141
|
+
|
142
|
+
cells_to_check.remove((row, col))
|
143
|
+
|
144
|
+
return self.puzzle
|
@@ -0,0 +1,150 @@
|
|
1
|
+
import random
|
2
|
+
import time
|
3
|
+
from .dfs_solver import DFSSolver
|
4
|
+
from typing import List, Set, Tuple
|
5
|
+
from multiprocessing import Process, Queue
|
6
|
+
|
7
|
+
|
8
|
+
def solve_with_timeout(grid, solver, queue):
|
9
|
+
start_time = time.time()
|
10
|
+
solutions = solver.solve(grid)
|
11
|
+
solve_time = time.time() - start_time
|
12
|
+
queue.put((solutions, solve_time))
|
13
|
+
|
14
|
+
|
15
|
+
class LasVegasGenerator:
|
16
|
+
def __init__(self, size: int = 25, givens: int = 80, timeout: int | None = 5):
|
17
|
+
self.size = size
|
18
|
+
self.givens = givens
|
19
|
+
self.timeout = timeout
|
20
|
+
self.all_positions = [
|
21
|
+
(i, j) for i in range(self.size) for j in range(self.size)
|
22
|
+
] # Create list of all possible positions
|
23
|
+
self.rows = [set() for _ in range(self.size)]
|
24
|
+
self.cols = [set() for _ in range(self.size)]
|
25
|
+
self.boxes = [set() for _ in range(self.size)]
|
26
|
+
self.solver = DFSSolver(size, solutions_limit=1)
|
27
|
+
|
28
|
+
box_size = int(self.size**0.5)
|
29
|
+
self.box_lookup = [[0] * size for _ in range(size)]
|
30
|
+
for i in range(size):
|
31
|
+
for j in range(size):
|
32
|
+
self.box_lookup[i][j] = (i // box_size) * box_size + (j // box_size)
|
33
|
+
|
34
|
+
def create_empty_grid(self) -> List[List[int]]:
|
35
|
+
return [[0 for _ in range(self.size)] for _ in range(self.size)]
|
36
|
+
|
37
|
+
def get_random_positions(self) -> Set[Tuple[int, int]]:
|
38
|
+
# Use random.sample to select givens number of positions without replacement
|
39
|
+
selected_positions = random.sample(self.all_positions, self.givens)
|
40
|
+
|
41
|
+
return set(selected_positions)
|
42
|
+
|
43
|
+
def is_valid_number(
|
44
|
+
self, grid: List[List[int]], row: int, col: int, num: int
|
45
|
+
) -> bool:
|
46
|
+
# Check if number exists in row, column or box using sets
|
47
|
+
if (
|
48
|
+
num in self.rows[row]
|
49
|
+
or num in self.cols[col]
|
50
|
+
or num in self.boxes[self.box_lookup[row][col]]
|
51
|
+
):
|
52
|
+
return False
|
53
|
+
return True
|
54
|
+
|
55
|
+
def fill_random_positions(
|
56
|
+
self, grid: List[List[int]], positions: Set[Tuple[int, int]]
|
57
|
+
) -> bool:
|
58
|
+
# Clear existing sets
|
59
|
+
self.rows = [set() for _ in range(self.size)]
|
60
|
+
self.cols = [set() for _ in range(self.size)]
|
61
|
+
self.boxes = [set() for _ in range(self.size)]
|
62
|
+
|
63
|
+
positions_list = list(positions)
|
64
|
+
random.shuffle(positions_list)
|
65
|
+
|
66
|
+
def backtrack(pos_index: int) -> bool:
|
67
|
+
if pos_index == len(positions_list):
|
68
|
+
return True
|
69
|
+
|
70
|
+
row, col = positions_list[pos_index]
|
71
|
+
valid_numbers = [
|
72
|
+
num
|
73
|
+
for num in range(1, self.size + 1)
|
74
|
+
if self.is_valid_number(grid, row, col, num)
|
75
|
+
]
|
76
|
+
|
77
|
+
random.shuffle(valid_numbers)
|
78
|
+
|
79
|
+
for num in valid_numbers:
|
80
|
+
grid[row][col] = num
|
81
|
+
self.rows[row].add(num)
|
82
|
+
self.cols[col].add(num)
|
83
|
+
self.boxes[self.box_lookup[row][col]].add(num)
|
84
|
+
|
85
|
+
if backtrack(pos_index + 1):
|
86
|
+
return True
|
87
|
+
|
88
|
+
grid[row][col] = 0
|
89
|
+
self.rows[row].remove(num)
|
90
|
+
self.cols[col].remove(num)
|
91
|
+
self.boxes[self.box_lookup[row][col]].remove(num)
|
92
|
+
|
93
|
+
return False
|
94
|
+
|
95
|
+
return backtrack(0)
|
96
|
+
|
97
|
+
def generate(self) -> List[List[int]]:
|
98
|
+
attempts = 0
|
99
|
+
|
100
|
+
while True:
|
101
|
+
attempts += 1
|
102
|
+
print(f"Attempt {attempts}...")
|
103
|
+
attempt_start = time.time()
|
104
|
+
|
105
|
+
grid = self.create_empty_grid()
|
106
|
+
positions = self.get_random_positions()
|
107
|
+
|
108
|
+
if not self.fill_random_positions(grid, positions):
|
109
|
+
continue
|
110
|
+
|
111
|
+
queue = Queue()
|
112
|
+
process = Process(
|
113
|
+
target=solve_with_timeout, args=(grid, self.solver, queue)
|
114
|
+
)
|
115
|
+
process.start()
|
116
|
+
|
117
|
+
if self.timeout is not None:
|
118
|
+
process.join(timeout=self.timeout)
|
119
|
+
if process.is_alive():
|
120
|
+
process.terminate()
|
121
|
+
process.join()
|
122
|
+
continue
|
123
|
+
else:
|
124
|
+
process.join()
|
125
|
+
attempt_duration = time.time() - attempt_start
|
126
|
+
print(f"Attempt duration: {attempt_duration:.2f} seconds")
|
127
|
+
|
128
|
+
try:
|
129
|
+
solution, solve_time = queue.get_nowait()
|
130
|
+
if solution:
|
131
|
+
print(f"Found valid puzzle after {attempts} attempts")
|
132
|
+
print(f"Solving time: {solve_time:.2f} seconds")
|
133
|
+
return solution
|
134
|
+
except Exception: # Changed from queue.Empty to catch any queue errors
|
135
|
+
continue
|
136
|
+
|
137
|
+
|
138
|
+
def print_grid(grid: List[List[int]]):
|
139
|
+
size = len(grid)
|
140
|
+
box_size = int(size**0.5)
|
141
|
+
|
142
|
+
for i, row in enumerate(grid):
|
143
|
+
if i > 0 and i % box_size == 0:
|
144
|
+
print("-" * (size * 3 + box_size))
|
145
|
+
|
146
|
+
for j, num in enumerate(row):
|
147
|
+
if j > 0 and j % box_size == 0:
|
148
|
+
print("|", end=" ")
|
149
|
+
print(f"{num:2}", end=" ")
|
150
|
+
print()
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import copy
|
4
|
+
from datetime import datetime
|
5
|
+
from typing import List, Tuple
|
6
|
+
|
7
|
+
from .las_vegas import LasVegasGenerator
|
8
|
+
from .hole_digger import HoleDigger
|
9
|
+
|
10
|
+
|
11
|
+
class SudokuGenerator:
|
12
|
+
"""Generates Sudoku puzzles and their solutions using Las Vegas algorithm.
|
13
|
+
|
14
|
+
This class handles the complete puzzle generation process:
|
15
|
+
1. Generates a complete valid solution using Las Vegas algorithm
|
16
|
+
2. Creates holes in the solution to create the actual puzzle
|
17
|
+
3. Saves both puzzle and solution with metadata
|
18
|
+
|
19
|
+
Attributes:
|
20
|
+
size (int): Size of the Sudoku grid (e.g., 25 for 25x25)
|
21
|
+
givens (int): Number of initial filled positions for Las Vegas
|
22
|
+
timeout (int): Maximum generation attempt time in seconds
|
23
|
+
difficulty (str): Target difficulty level for hole creation
|
24
|
+
puzzles_dir (str): Directory for storing generated puzzles
|
25
|
+
solutions_dir (str): Directory for storing solutions
|
26
|
+
|
27
|
+
Example:
|
28
|
+
>>> generator = SudokuGenerator(size=9, difficulty="Hard")
|
29
|
+
>>> puzzle, solution, puzzle_id = generator.generate()
|
30
|
+
"""
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
size: int = 25,
|
35
|
+
givens: int = 80,
|
36
|
+
timeout: int = 5,
|
37
|
+
difficulty: str = "Medium",
|
38
|
+
puzzles_dir: str = "benchmarks/puzzles",
|
39
|
+
solutions_dir: str = "benchmarks/solutions",
|
40
|
+
):
|
41
|
+
"""Initialize the Sudoku puzzle generator.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
size: Grid size (default 25 for 25x25 grid)
|
45
|
+
givens: Number of initial filled positions for Las Vegas
|
46
|
+
timeout: Maximum time in seconds for generation attempts
|
47
|
+
difficulty: Puzzle difficulty level for hole digger
|
48
|
+
puzzles_dir: Directory to store generated puzzles
|
49
|
+
solutions_dir: Directory to store solutions
|
50
|
+
"""
|
51
|
+
self.size = size
|
52
|
+
self.givens = givens
|
53
|
+
self.timeout = timeout
|
54
|
+
self.difficulty = difficulty
|
55
|
+
self.puzzles_dir = puzzles_dir
|
56
|
+
self.solutions_dir = solutions_dir
|
57
|
+
|
58
|
+
# Create directories if they don't exist
|
59
|
+
os.makedirs(puzzles_dir, exist_ok=True)
|
60
|
+
os.makedirs(solutions_dir, exist_ok=True)
|
61
|
+
|
62
|
+
def generate(self) -> Tuple[List[List[int]], List[List[int]], str]:
|
63
|
+
"""Generate a complete Sudoku puzzle and solution pair.
|
64
|
+
|
65
|
+
Uses a two-step process:
|
66
|
+
1. Las Vegas algorithm generates a complete valid solution
|
67
|
+
2. Hole digger creates the puzzle by removing certain cells
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
tuple: Contains:
|
71
|
+
- List[List[int]]: The puzzle grid with holes
|
72
|
+
- List[List[int]]: The complete solution grid
|
73
|
+
- str: Unique identifier for the puzzle/solution pair
|
74
|
+
|
75
|
+
Note:
|
76
|
+
The generated puzzle is guaranteed to have a unique solution
|
77
|
+
"""
|
78
|
+
# Step 1: Generate complete solution using Las Vegas
|
79
|
+
generator = LasVegasGenerator(self.size, self.givens, self.timeout)
|
80
|
+
solution = generator.generate()
|
81
|
+
|
82
|
+
# Step 2: Create holes using HoleDigger
|
83
|
+
digger = HoleDigger(copy.deepcopy(solution), self.difficulty)
|
84
|
+
puzzle = digger.dig_holes()
|
85
|
+
|
86
|
+
# Generate unique identifier for this puzzle
|
87
|
+
puzzle_id = self._generate_puzzle_id()
|
88
|
+
|
89
|
+
# Save both puzzle and solution
|
90
|
+
self._save_grid(puzzle, puzzle_id, is_puzzle=True)
|
91
|
+
self._save_grid(solution, puzzle_id, is_puzzle=False)
|
92
|
+
|
93
|
+
return puzzle, solution, puzzle_id
|
94
|
+
|
95
|
+
def _generate_puzzle_id(self) -> str:
|
96
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
97
|
+
return f"sudoku_{self.size}x{self.size}_{self.difficulty}_{timestamp}"
|
98
|
+
|
99
|
+
def _save_grid(self, grid: List[List[int]], puzzle_id: str, is_puzzle: bool):
|
100
|
+
directory = self.puzzles_dir if is_puzzle else self.solutions_dir
|
101
|
+
filename = f"{puzzle_id}_{'puzzle' if is_puzzle else 'solution'}.json"
|
102
|
+
filepath = os.path.join(directory, filename)
|
103
|
+
|
104
|
+
metadata = {
|
105
|
+
"size": self.size,
|
106
|
+
"difficulty": self.difficulty,
|
107
|
+
"givens": sum(cell != 0 for row in grid for cell in row),
|
108
|
+
"type": "puzzle" if is_puzzle else "solution",
|
109
|
+
"grid": grid,
|
110
|
+
}
|
111
|
+
|
112
|
+
with open(filepath, "w") as f:
|
113
|
+
json.dump(metadata, f, indent=2)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: sudoku_smt_solvers
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: A collection of SAT and SMT solvers for solving Sudoku puzzles
|
5
5
|
Home-page: https://liamjdavis.github.io/sudoku-smt-solvers
|
6
6
|
Author: Liam Davis, Tairan 'Ryan' Ji
|
@@ -35,7 +35,7 @@ Dynamic: summary
|
|
35
35
|
|
36
36
|
|
37
37
|
## About
|
38
|
-
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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.
|
38
|
+
This repository contains the code for the study "Evaluating SMT-Based Solvers on Sudoku". Created by Liam Davis (@liamjdavis) and Tairan "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. The corresponding paper can be found [here](https://arxiv.org/abs/2501.08569).
|
39
39
|
|
40
40
|
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.
|
41
41
|
|
@@ -61,7 +61,7 @@ This package includes the DPLL solver and three modern SMT solvers:
|
|
61
61
|
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:
|
62
62
|
|
63
63
|
```python
|
64
|
-
from sudoku_smt_solvers
|
64
|
+
from sudoku_smt_solvers import Z3Solver
|
65
65
|
|
66
66
|
# Example grid (25x25)
|
67
67
|
grid = [[0] * 25 for _ in range(25)]
|
@@ -78,7 +78,7 @@ else:
|
|
78
78
|
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:
|
79
79
|
|
80
80
|
```python
|
81
|
-
from sudoku_smt_solvers
|
81
|
+
from sudoku_smt_solvers import SudokuGenerator
|
82
82
|
|
83
83
|
generator = SudokuGenerator(size = 25, givens = 80, timeout = 5, difficulty = "Medium", puzzles_dir = "benchmarks/puzzles", solutions_dir = "benchmarks/solutions")
|
84
84
|
|
@@ -91,11 +91,10 @@ Due to the computational complexity of generating large sudoku puzzles, it is re
|
|
91
91
|
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:
|
92
92
|
|
93
93
|
```python
|
94
|
-
from sudoku_smt_solvers
|
94
|
+
from sudoku_smt_solvers import BenchmarkRunner
|
95
95
|
|
96
96
|
runner = BenchmarkRunner(
|
97
97
|
puzzles_dir='resources/benchmarks/puzzles/',
|
98
|
-
solutions_dir='resources/benchmarks/solutions/',
|
99
98
|
results_dir='results/'
|
100
99
|
)
|
101
100
|
runner.run_benchmarks()
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/SOURCES.txt
RENAMED
@@ -10,11 +10,18 @@ sudoku_smt_solvers.egg-info/requires.txt
|
|
10
10
|
sudoku_smt_solvers.egg-info/top_level.txt
|
11
11
|
sudoku_smt_solvers/benchmarks/__init__.py
|
12
12
|
sudoku_smt_solvers/benchmarks/benchmark_runner.py
|
13
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/__init__.py
|
14
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/dfs_solver.py
|
15
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/hole_digger.py
|
16
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/las_vegas.py
|
17
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/sudoku_generator.py
|
13
18
|
sudoku_smt_solvers/solvers/__init__.py
|
14
19
|
sudoku_smt_solvers/solvers/cvc5_solver.py
|
15
20
|
sudoku_smt_solvers/solvers/dpll_solver.py
|
16
21
|
sudoku_smt_solvers/solvers/dpllt_solver.py
|
17
22
|
sudoku_smt_solvers/solvers/z3_solver.py
|
23
|
+
sudoku_smt_solvers/solvers/utils/__init__.py
|
24
|
+
sudoku_smt_solvers/solvers/utils/sudoku_error.py
|
18
25
|
tests/test_benchmark_runner.py
|
19
26
|
tests/test_cvc5_solver.py
|
20
27
|
tests/test_dfs_solver.py
|
@@ -1,19 +0,0 @@
|
|
1
|
-
"""Sudoku SMT Solvers - A package for solving and benchmarking large-scale Sudoku puzzles.
|
2
|
-
|
3
|
-
This package provides various SAT and SMT-based solvers optimized for 25x25 Sudoku puzzles,
|
4
|
-
along with tools for puzzle generation and solver benchmarking.
|
5
|
-
|
6
|
-
Key Components:
|
7
|
-
- Multiple solver implementations (DPLL, DPLL(T), Z3, CVC5)
|
8
|
-
- Sudoku puzzle generator with difficulty settings
|
9
|
-
- Benchmarking suite for comparing solver performance
|
10
|
-
"""
|
11
|
-
|
12
|
-
from .solvers.dpll_solver import DPLLSolver
|
13
|
-
from .solvers.dpllt_solver import DPLLTSolver
|
14
|
-
from .solvers.z3_solver import Z3Solver
|
15
|
-
from .solvers.cvc5_solver import CVC5Solver
|
16
|
-
from .benchmarks.benchmark_runner import BenchmarkRunner
|
17
|
-
from .benchmarks.sudoku_generator.sudoku_generator import SudokuGenerator
|
18
|
-
|
19
|
-
__version__ = "0.3.0"
|
@@ -1,5 +0,0 @@
|
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/cvc5_solver.py
RENAMED
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/dpll_solver.py
RENAMED
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/dpllt_solver.py
RENAMED
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers/solvers/z3_solver.py
RENAMED
File without changes
|
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/requires.txt
RENAMED
File without changes
|
{sudoku_smt_solvers-0.3.0 → sudoku_smt_solvers-1.0.0}/sudoku_smt_solvers.egg-info/top_level.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|