sudoku-smt-solvers 0.3.0__py3-none-any.whl → 0.4.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 +23 -7
- sudoku_smt_solvers/benchmarks/__init__.py +2 -4
- sudoku_smt_solvers/benchmarks/sudoku_generator/__init__.py +6 -0
- sudoku_smt_solvers/benchmarks/sudoku_generator/dfs_solver.py +99 -0
- sudoku_smt_solvers/benchmarks/sudoku_generator/hole_digger.py +144 -0
- sudoku_smt_solvers/benchmarks/sudoku_generator/las_vegas.py +150 -0
- sudoku_smt_solvers/benchmarks/sudoku_generator/sudoku_generator.py +113 -0
- sudoku_smt_solvers/solvers/__init__.py +2 -1
- sudoku_smt_solvers/solvers/utils/__init__.py +3 -0
- sudoku_smt_solvers/solvers/utils/sudoku_error.py +8 -0
- {sudoku_smt_solvers-0.3.0.dist-info → sudoku_smt_solvers-0.4.0.dist-info}/METADATA +4 -4
- sudoku_smt_solvers-0.4.0.dist-info/RECORD +20 -0
- sudoku_smt_solvers-0.3.0.dist-info/RECORD +0 -13
- {sudoku_smt_solvers-0.3.0.dist-info → sudoku_smt_solvers-0.4.0.dist-info}/LICENSE +0 -0
- {sudoku_smt_solvers-0.3.0.dist-info → sudoku_smt_solvers-0.4.0.dist-info}/WHEEL +0 -0
- {sudoku_smt_solvers-0.3.0.dist-info → sudoku_smt_solvers-0.4.0.dist-info}/top_level.txt +0 -0
sudoku_smt_solvers/__init__.py
CHANGED
@@ -9,11 +9,27 @@ Key Components:
|
|
9
9
|
- Benchmarking suite for comparing solver performance
|
10
10
|
"""
|
11
11
|
|
12
|
-
from .solvers
|
13
|
-
from .solvers.
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
|
17
|
-
|
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
|
+
)
|
18
21
|
|
19
|
-
__version__ = "0.
|
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
|
+
]
|
@@ -1,5 +1,3 @@
|
|
1
1
|
from .benchmark_runner import BenchmarkRunner
|
2
|
-
|
3
|
-
|
4
|
-
from .sudoku_generator.las_vegas import LasVegasGenerator
|
5
|
-
from .sudoku_generator.hole_digger import HoleDigger
|
2
|
+
|
3
|
+
__all__ = ["BenchmarkRunner"]
|
@@ -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: 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
|
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,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
|
94
|
+
from sudoku_smt_solvers import BenchmarkRunner
|
95
95
|
|
96
96
|
runner = BenchmarkRunner(
|
97
97
|
puzzles_dir='resources/benchmarks/puzzles/',
|
@@ -0,0 +1,20 @@
|
|
1
|
+
sudoku_smt_solvers/__init__.py,sha256=PJo8qJmamMhps_-HTKegaxq3FqUf_PWWz7f_8DqRafk,935
|
2
|
+
sudoku_smt_solvers/benchmarks/__init__.py,sha256=qRc3eFgeV0yzO2NgSnlqjJ3ejHgDxlI1ySId6WspT24,77
|
3
|
+
sudoku_smt_solvers/benchmarks/benchmark_runner.py,sha256=Mc87ul-6VkWMomtlmOMc9GXmC4AwfQUwIWKDjeFvSVA,7712
|
4
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/__init__.py,sha256=Ob4Qt1GyqixpvZceGZxyYWd1doefukN_WnHns1SBDQg,236
|
5
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/dfs_solver.py,sha256=rkuCs0frivbzLg0pF5YZvi2rfVW9qRr5vtcJxvY9Crw,3890
|
6
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/hole_digger.py,sha256=yjjl9J_aeBwSVa4cc8VDMm-A7KMo2_5292zxzLaLzmQ,4793
|
7
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/las_vegas.py,sha256=koXte_GjftrbZEvKscE09VcXme1i-l4YsWcjvzIYJ8k,4999
|
8
|
+
sudoku_smt_solvers/benchmarks/sudoku_generator/sudoku_generator.py,sha256=pPZw6szPvMfnoydjcfYZu87-6jU5SVgmVwhGo3Ir5qw,4258
|
9
|
+
sudoku_smt_solvers/solvers/__init__.py,sha256=5QRpexW7hj4nBVJnWdJzdOwh2T2iheLLgIlAYKUozvg,209
|
10
|
+
sudoku_smt_solvers/solvers/cvc5_solver.py,sha256=igN27LHxUADmYw3farmEeGtjJEsrdSQEk0X0X_EItyQ,6855
|
11
|
+
sudoku_smt_solvers/solvers/dpll_solver.py,sha256=itLEx8QdomSS9nA7CrhSlU7wN-e8vDInAWiwhXX-7Ug,6144
|
12
|
+
sudoku_smt_solvers/solvers/dpllt_solver.py,sha256=8HJWOGU6Oj4hNpgkK96sI51bdiTfRLSlf5yhic7WeF0,7992
|
13
|
+
sudoku_smt_solvers/solvers/z3_solver.py,sha256=e6WPwPdN8uUipjxNm7KRYlbsNNwGyxH3s4wy5wC8PPc,4902
|
14
|
+
sudoku_smt_solvers/solvers/utils/__init__.py,sha256=TPg_Q5aZs_QOC9CEesZhf9o7uUC3c7oG_vP5nBcyc2M,65
|
15
|
+
sudoku_smt_solvers/solvers/utils/sudoku_error.py,sha256=iUcv1QCgQ7anov0b-AtIB1fbfZ3yWfci4eTp_8RuUJg,208
|
16
|
+
sudoku_smt_solvers-0.4.0.dist-info/LICENSE,sha256=PbuZlvluV1l4HMMfPAVe5yjVvFGBK9DFp20JNhoJ8bI,1067
|
17
|
+
sudoku_smt_solvers-0.4.0.dist-info/METADATA,sha256=4BGL_GK0m0dTFzKJRqYFnN0vXzELS8sXSgu6qNxL2Dw,6589
|
18
|
+
sudoku_smt_solvers-0.4.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
19
|
+
sudoku_smt_solvers-0.4.0.dist-info/top_level.txt,sha256=Ww9vs8KC4aujzfGfddMl_X8Qzh-Cywn9aBTLQgemi5A,19
|
20
|
+
sudoku_smt_solvers-0.4.0.dist-info/RECORD,,
|
@@ -1,13 +0,0 @@
|
|
1
|
-
sudoku_smt_solvers/__init__.py,sha256=HlyADLi9blzeaktg33Xn-8nl-jEXfF-lxu2eYZ2DirQ,763
|
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=QDJp4TOJp1ipYwyXomHRkIxuqO_p-Vpr9zGwJQFiEOk,186
|
5
|
-
sudoku_smt_solvers/solvers/cvc5_solver.py,sha256=igN27LHxUADmYw3farmEeGtjJEsrdSQEk0X0X_EItyQ,6855
|
6
|
-
sudoku_smt_solvers/solvers/dpll_solver.py,sha256=itLEx8QdomSS9nA7CrhSlU7wN-e8vDInAWiwhXX-7Ug,6144
|
7
|
-
sudoku_smt_solvers/solvers/dpllt_solver.py,sha256=8HJWOGU6Oj4hNpgkK96sI51bdiTfRLSlf5yhic7WeF0,7992
|
8
|
-
sudoku_smt_solvers/solvers/z3_solver.py,sha256=e6WPwPdN8uUipjxNm7KRYlbsNNwGyxH3s4wy5wC8PPc,4902
|
9
|
-
sudoku_smt_solvers-0.3.0.dist-info/LICENSE,sha256=PbuZlvluV1l4HMMfPAVe5yjVvFGBK9DFp20JNhoJ8bI,1067
|
10
|
-
sudoku_smt_solvers-0.3.0.dist-info/METADATA,sha256=8gTnQaFcWAhdZnHc0Gqtbgyu7kf9hOPbx4zkCB2Mly0,6680
|
11
|
-
sudoku_smt_solvers-0.3.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
12
|
-
sudoku_smt_solvers-0.3.0.dist-info/top_level.txt,sha256=Ww9vs8KC4aujzfGfddMl_X8Qzh-Cywn9aBTLQgemi5A,19
|
13
|
-
sudoku_smt_solvers-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|