sudoku-smt-solvers 0.2.0__tar.gz → 0.4.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. {sudoku_smt_solvers-0.2.0/sudoku_smt_solvers.egg-info → sudoku_smt_solvers-0.4.0}/PKG-INFO +4 -4
  2. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/README.md +3 -3
  3. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/__init__.py +35 -0
  4. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/__init__.py +3 -0
  5. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/sudoku_generator/__init__.py +6 -0
  6. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/sudoku_generator/dfs_solver.py +99 -0
  7. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/sudoku_generator/hole_digger.py +144 -0
  8. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/sudoku_generator/las_vegas.py +150 -0
  9. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/benchmarks/sudoku_generator/sudoku_generator.py +113 -0
  10. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/solvers/__init__.py +2 -0
  11. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/solvers/utils/__init__.py +3 -0
  12. sudoku_smt_solvers-0.4.0/sudoku_smt_solvers/solvers/utils/sudoku_error.py +8 -0
  13. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0/sudoku_smt_solvers.egg-info}/PKG-INFO +4 -4
  14. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers.egg-info/SOURCES.txt +7 -0
  15. sudoku_smt_solvers-0.2.0/sudoku_smt_solvers/__init__.py +0 -19
  16. sudoku_smt_solvers-0.2.0/sudoku_smt_solvers/benchmarks/__init__.py +0 -5
  17. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/LICENSE +0 -0
  18. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/pyproject.toml +0 -0
  19. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/setup.cfg +0 -0
  20. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/setup.py +0 -0
  21. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/benchmarks/benchmark_runner.py +0 -0
  22. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/solvers/cvc5_solver.py +0 -0
  23. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/solvers/dpll_solver.py +0 -0
  24. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/solvers/dpllt_solver.py +0 -0
  25. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers/solvers/z3_solver.py +0 -0
  26. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers.egg-info/dependency_links.txt +0 -0
  27. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers.egg-info/requires.txt +0 -0
  28. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/sudoku_smt_solvers.egg-info/top_level.txt +0 -0
  29. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_benchmark_runner.py +0 -0
  30. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_cvc5_solver.py +0 -0
  31. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_dfs_solver.py +0 -0
  32. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_dpll_solver.py +0 -0
  33. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_dpllt_solver.py +0 -0
  34. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_hole_digger.py +0 -0
  35. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_las_vegas.py +0 -0
  36. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_parser.py +0 -0
  37. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_profiler.py +0 -0
  38. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.0}/tests/test_sudoku_generator.py +0 -0
  39. {sudoku_smt_solvers-0.2.0 → sudoku_smt_solvers-0.4.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.2.0
3
+ Version: 0.4.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
@@ -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.solvers.z3_solver import Z3Solver
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.benchmarks.sudoku_generator.sudoku_generator import SudokuGenerator
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,7 +91,7 @@ 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.benchmarks.benchmark_runner import BenchmarkRunner
94
+ from sudoku_smt_solvers import BenchmarkRunner
95
95
 
96
96
  runner = BenchmarkRunner(
97
97
  puzzles_dir='resources/benchmarks/puzzles/',
@@ -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.solvers.z3_solver import Z3Solver
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.benchmarks.sudoku_generator.sudoku_generator import SudokuGenerator
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,7 +63,7 @@ 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.benchmarks.benchmark_runner import BenchmarkRunner
66
+ from sudoku_smt_solvers import BenchmarkRunner
67
67
 
68
68
  runner = BenchmarkRunner(
69
69
  puzzles_dir='resources/benchmarks/puzzles/',
@@ -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__ = "0.4.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,3 @@
1
+ from .benchmark_runner import BenchmarkRunner
2
+
3
+ __all__ = ["BenchmarkRunner"]
@@ -0,0 +1,6 @@
1
+ from .sudoku_generator import SudokuGenerator
2
+ from .las_vegas import LasVegasGenerator
3
+ from .hole_digger import HoleDigger
4
+ from .dfs_solver import DFSSolver
5
+
6
+ __all__ = ["SudokuGenerator", "LasVegasGenerator", "HoleDigger", "DFSSolver"]
@@ -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)
@@ -2,3 +2,5 @@ from .cvc5_solver import CVC5Solver
2
2
  from .dpll_solver import DPLLSolver
3
3
  from .dpllt_solver import DPLLTSolver
4
4
  from .z3_solver import Z3Solver
5
+
6
+ __all__ = ["CVC5Solver", "DPLLSolver", "DPLLTSolver", "Z3Solver"]
@@ -0,0 +1,3 @@
1
+ from .sudoku_error import SudokuError
2
+
3
+ __all__ = ["SudokuError"]
@@ -0,0 +1,8 @@
1
+ class SudokuError(Exception):
2
+ """An exception class for errors in the sudoku solver"""
3
+
4
+ def __init__(self, message):
5
+ self.message = message
6
+
7
+ def __str__(self):
8
+ return self.message
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: sudoku_smt_solvers
3
- Version: 0.2.0
3
+ Version: 0.4.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
@@ -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.solvers.z3_solver import Z3Solver
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.benchmarks.sudoku_generator.sudoku_generator import SudokuGenerator
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,7 +91,7 @@ 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.benchmarks.benchmark_runner import BenchmarkRunner
94
+ from sudoku_smt_solvers import BenchmarkRunner
95
95
 
96
96
  runner = BenchmarkRunner(
97
97
  puzzles_dir='resources/benchmarks/puzzles/',
@@ -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.2.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