From e3b8d12319bd884f014984f026494056cdf0bb9c Mon Sep 17 00:00:00 2001 From: Ilja Heitlager Date: Thu, 23 Nov 2023 21:23:29 +0100 Subject: [PATCH] Vector (#41) * first ideas * first heuristics * more tweaks * first heuristic solver * adding jupyter book --- .vscode/launch.json | 8 + Makefile | 1 + notebooks/heuristic_sudoku.ipynb | 238 +++++++++++++++++++++++++++++ src/sudoku/__init__.py | 36 +++-- src/sudoku/printer.py | 11 ++ src/sudoku/solvers/backtracking.py | 4 +- src/sudoku/solvers/csp.py | 4 +- src/sudoku/solvers/heuristic.py | 89 +++++++++++ tests/test_backtracking.py | 4 +- tests/test_basics.py | 15 +- tests/test_csp.py | 4 +- tests/test_heuristic.py | 48 ++++++ tests/test_printer.py | 15 +- 13 files changed, 447 insertions(+), 30 deletions(-) create mode 100644 notebooks/heuristic_sudoku.ipynb create mode 100644 src/sudoku/solvers/heuristic.py create mode 100644 tests/test_heuristic.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 15034b0..7a399c1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,14 @@ "request": "launch", "program": "${file}", "justMyCode": true + }, + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "purpose": ["debug-test"] } ] } \ No newline at end of file diff --git a/Makefile b/Makefile index f98e880..fafc0c9 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ dev_env: ## Install the dev env @python -m pip install pytest @python -m pip install coverage @python -m pip install ipykernel + @python -m pip install -r requirements.txt virtualenv: $(VIRTUALENV)/sudoku/bin/activate virtualenv $(VIRTUALENV)/sudoku diff --git a/notebooks/heuristic_sudoku.ipynb b/notebooks/heuristic_sudoku.ipynb new file mode 100644 index 0000000..385fd9a --- /dev/null +++ b/notebooks/heuristic_sudoku.ipynb @@ -0,0 +1,238 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook provides a simple sudoku solver. Let's start with a simple 4 (\\*\\*\\*\\*) star sudoku" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Parool Dinsdag 19 sept ****\n", + "sudoku_grid = [\n", + " [0, 6, 0, 0, 0, 0, 1, 9, 0],\n", + " [0, 0, 2, 6, 1, 0, 0, 0, 4],\n", + " [7, 0, 1, 0, 0, 0, 0, 0, 0],\n", + " [0, 0, 0, 0, 7, 0, 0, 1, 0],\n", + " [0, 0, 6, 0, 8, 3, 0, 0, 0],\n", + " [5, 4, 0, 0, 6, 0, 0, 0, 3],\n", + " [0, 8, 0, 0, 2, 7, 0, 3, 9],\n", + " [0, 0, 0, 4, 0, 0, 0, 7, 8],\n", + " [0, 0, 0, 0, 0, 0, 4, 0, 0]\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And let's show the grid by importing a simple pretty printer. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from sudoku.solvers import heuristic\n", + "\n", + "grid = heuristic.pencil_in_numbers(sudoku_grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "292" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "heuristic.simple_elimination(grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "heuristic.simple_elimination(grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "15" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "heuristic.hidden_single(grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "#heuristic.hidden_single(grid)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[[3, 4, 8],\n", + " [6],\n", + " [3, 4, 5, 8],\n", + " [7],\n", + " [3, 4, 5],\n", + " [2, 4, 5, 8],\n", + " [1],\n", + " [9],\n", + " [2, 5, 7]],\n", + " [[3, 8, 9], [3, 5, 9], [2], [6], [1], [5, 8, 9], [7], [5, 8], [4]],\n", + " [[7],\n", + " [3, 5, 9],\n", + " [1],\n", + " [2, 3, 5, 8, 9],\n", + " [3, 4, 5, 9],\n", + " [2, 4, 5, 8, 9],\n", + " [2, 3, 5, 6, 8],\n", + " [2, 5, 6, 8],\n", + " [2, 5, 6]],\n", + " [[2, 3, 8, 9],\n", + " [2, 3, 9],\n", + " [3, 8, 9],\n", + " [2, 5, 9],\n", + " [7],\n", + " [4],\n", + " [2, 5, 6, 8, 9],\n", + " [1],\n", + " [2, 5, 6]],\n", + " [[1, 2, 9],\n", + " [1, 2, 7, 9],\n", + " [6],\n", + " [1, 2, 5, 9],\n", + " [8],\n", + " [3],\n", + " [2, 5, 7, 9],\n", + " [4],\n", + " [2, 5, 7]],\n", + " [[5], [4], [7, 8, 9], [1, 2, 9], [6], [1, 2, 9], [2, 7, 8, 9], [2, 8], [3]],\n", + " [[1, 4, 6], [8], [4, 5], [1, 5], [2], [7], [5, 6], [3], [9]],\n", + " [[1, 2, 3, 6, 9],\n", + " [1, 2, 3, 5, 9],\n", + " [3, 5, 9],\n", + " [4],\n", + " [3, 5, 9],\n", + " [1, 5, 6, 9],\n", + " [2, 5, 6],\n", + " [7],\n", + " [8]],\n", + " [[1, 2, 3, 6, 9],\n", + " [1, 2, 3, 5, 7, 9],\n", + " [3, 5, 7, 9],\n", + " [1, 3, 5, 8, 9],\n", + " [3, 5, 9],\n", + " [1, 5, 6, 8, 9],\n", + " [4],\n", + " [2, 5, 6],\n", + " [1]]]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "grid" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from sudoku.solvers import heuristic\n", + "\n", + "heuristic.solve(grid)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/sudoku/__init__.py b/src/sudoku/__init__.py index 7581f85..acd777d 100644 --- a/src/sudoku/__init__.py +++ b/src/sudoku/__init__.py @@ -1,23 +1,31 @@ -def count_nonzero(grid): +# just get all coordinates +all_grid = [(i, j) for i in range(9) for j in range(9)] + +def n_nonzero(grid): n = 0 - for row in range(9): - for col in range(9): - if grid[row][col] != 0: - n += 1 + for (i, j) in all_grid: + if grid[i][j] != 0: + n += 1 return n -def is_complete(grid): - for row in range(9): - for col in range(9): - if grid[row][col] == 0: - return False +def is_solved(grid): + for (i, j) in all_grid: + if isinstance(grid[i][j], list) and len(grid[i][j]) != 1: + return False + elif grid[i][j] == 0 : + return False return True def find_empty_cell(grid): - for row in range(9): - for col in range(9): - if grid[row][col] == 0: - return row, col + for (i, j) in all_grid: + if grid[i][j] == 0: + return i, j return None, None + + +def flatten(grid): + for (i, j) in all_grid: + if isinstance(grid[i][j], list) and len(grid[i][j]) == 1: + grid[i][j] = grid[i][j][0] \ No newline at end of file diff --git a/src/sudoku/printer.py b/src/sudoku/printer.py index a128eea..849ab09 100644 --- a/src/sudoku/printer.py +++ b/src/sudoku/printer.py @@ -21,3 +21,14 @@ def display_pylist(grid): print(" [%s]," % ', '.join([str(x) for x in row])) print(" [%s]" % ', '.join([str(x) for x in grid[-1]])) print("]") + + +def as_string(grid): + ''' + Return as single line string + ''' + result = "" + for row in grid: + for cell in row: + result += str(cell) + return result \ No newline at end of file diff --git a/src/sudoku/solvers/backtracking.py b/src/sudoku/solvers/backtracking.py index 280cdef..fb841a5 100644 --- a/src/sudoku/solvers/backtracking.py +++ b/src/sudoku/solvers/backtracking.py @@ -1,4 +1,4 @@ -from sudoku import find_empty_cell, is_complete +from sudoku import find_empty_cell, is_solved iterations = 0 @@ -20,7 +20,7 @@ def is_valid(grid, row, col, num): def solve(grid): global iterations - if is_complete(grid): + if is_solved(grid): return True iterations += 1 diff --git a/src/sudoku/solvers/csp.py b/src/sudoku/solvers/csp.py index ae03c03..017ec1d 100644 --- a/src/sudoku/solvers/csp.py +++ b/src/sudoku/solvers/csp.py @@ -1,9 +1,9 @@ -from sudoku import is_complete +from sudoku import is_solved from constraint import * def solve(sudoku_grid): - if is_complete(sudoku_grid): + if is_solved(sudoku_grid): return True problem = Problem(BacktrackingSolver()) diff --git a/src/sudoku/solvers/heuristic.py b/src/sudoku/solvers/heuristic.py new file mode 100644 index 0000000..5f895bf --- /dev/null +++ b/src/sudoku/solvers/heuristic.py @@ -0,0 +1,89 @@ +# This is taken from https://github.com/gamescomputersplay/sudoku-solver +from sudoku import all_grid, is_solved, flatten +import copy + +# Some helper lists to iterate through houses +################################################# + +# return columns' lists of cells +all_columns = [[(i, j) for j in range(9)] for i in range(9)] + +# same for rows +all_rows = [[(i, j) for i in range(9)] for j in range(9)] + +# same for blocks +# this list comprehension is unreadable, but quite cool! +all_blocks = [[((i//3) * 3 + j//3, (i % 3)*3+j % 3) + for j in range(9)] for i in range(9)] + +# combine three +all_houses = all_columns+all_rows+all_blocks + + +# Adding candidates as list instead of zeros +def pencil_in_numbers(puzzle): + result = copy.deepcopy(puzzle) + for (i, j) in all_grid: + if puzzle[i][j] != 0: + result[i][j] = [puzzle[i][j], ] + else: + result[i][j] = [i for i in range(1, 10)] + return result + + +def simple_elimination(grid): + count = 0 + for group in all_houses: + for (i, j) in group: + if len(grid[i][j]) == 1: + for (i2, j2) in group: + if grid[i][j][0] in grid[i2][j2] and (i, j) != (i2, j2): + grid[i2][j2].remove(grid[i][j][0]) + count += 1 + return count + + +def hidden_single(grid): + + def find_only_number_in_group(group, number): + count = 0 + removed = 0 + i2, j2 = (-1, -1) + for (i, j) in group: + for n in grid[i][j]: + if n == number: + count += 1 + i2, j2 = (i, j) + if count == 1 and (i2, j2) != (-1, -1) \ + and len(grid[i2][j2]) > 1: + removed = len(grid[i2][j2]) - 1 + grid[i2][j2] = [number] + return removed + + count = 0 + for number in range(1, 10): + for group in all_houses: + count += find_only_number_in_group(group, number) + return count + +cycles = 0 + +def solve(grid): + ''' + Heuristic solver + ''' + global cycles + + count = [0, 0] + while not is_solved(grid): + cycles += 1 + c0 = simple_elimination(grid) + count[0] += c0 + c1 = hidden_single(grid) + count[1] += c1 + + if c0+c1 == 0: + return False + + flatten(grid) + return True \ No newline at end of file diff --git a/tests/test_backtracking.py b/tests/test_backtracking.py index bb18985..a349f57 100644 --- a/tests/test_backtracking.py +++ b/tests/test_backtracking.py @@ -29,8 +29,8 @@ def test_solve(): g = copy.deepcopy(problem_grid) assert backtracking.solve(g) - assert ss.is_complete(g) - assert ss.count_nonzero(g) == 81 + assert ss.is_solved(g) + assert ss.n_nonzero(g) == 81 assert ss.find_empty_cell(g) == (None, None) assert backtracking.iterations == 2522 # solved is solved diff --git a/tests/test_basics.py b/tests/test_basics.py index f9b0722..e881ee2 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,14 +12,21 @@ [0, 0, 0, 0, 0, 0, 4, 0, 0] ] +def test_allgrid(): + assert len(ss.all_grid) == 81 + def test_find_empty_cell(): assert ss.find_empty_cell(sudoku_grid) == (0, 0) -def test_is_complete(): - assert not ss.is_complete(sudoku_grid) +def test_is_solved(): + assert not ss.is_solved(sudoku_grid) + + +def test_n_nonzero(): + assert ss.n_nonzero(sudoku_grid) == 27 + + -def test_count_nonzero(): - assert ss.count_nonzero(sudoku_grid) == 27 diff --git a/tests/test_csp.py b/tests/test_csp.py index 7407d76..e3a3586 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -30,8 +30,8 @@ def test_solve(): g = copy.deepcopy(problem_grid) assert csp.solve(g) - assert ss.is_complete(g) - assert ss.count_nonzero(g) == 81 + assert ss.is_solved(g) + assert ss.n_nonzero(g) == 81 assert ss.find_empty_cell(g) == (None, None) assert g == solution_grid diff --git a/tests/test_heuristic.py b/tests/test_heuristic.py new file mode 100644 index 0000000..1dc1f30 --- /dev/null +++ b/tests/test_heuristic.py @@ -0,0 +1,48 @@ +import sudoku as ss +from sudoku.solvers import heuristic + +problem_grid = [ + [0, 6, 0, 0, 0, 0, 1, 9, 0], + [0, 0, 2, 6, 1, 0, 0, 0, 4], + [7, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 7, 0, 0, 1, 0], + [0, 0, 6, 0, 8, 3, 0, 0, 0], + [5, 4, 0, 0, 6, 0, 0, 0, 3], + [0, 8, 0, 0, 2, 7, 0, 3, 9], + [0, 0, 0, 4, 0, 0, 0, 7, 8], + [0, 0, 0, 0, 0, 0, 4, 0, 0] +] + +solution_grid = [ + [4, 6, 8, 7, 3, 5, 1, 9, 2], + [3, 5, 2, 6, 1, 9, 7, 8, 4], + [7, 9, 1, 8, 4, 2, 3, 5, 6], + [8, 3, 9, 2, 7, 4, 6, 1, 5], + [1, 2, 6, 5, 8, 3, 9, 4, 7], + [5, 4, 7, 9, 6, 1, 8, 2, 3], + [6, 8, 4, 1, 2, 7, 5, 3, 9], + [9, 1, 3, 4, 5, 6, 2, 7, 8], + [2, 7, 5, 3, 9, 8, 4, 6, 1] +] + +def test_all_columns(): + assert len(heuristic.all_columns) == 9 + + +def test_all_rows(): + assert len(heuristic.all_rows) == 9 + + +def test_all_blocks(): + assert len(heuristic.all_blocks) == 9 + + +def test_all_houses(): + assert len(heuristic.all_houses) == 9+9+9 + +def test_solve(): + g = heuristic.pencil_in_numbers(problem_grid) + assert heuristic.solve(g) + assert ss.is_solved(g) + assert heuristic.cycles == 8 + assert g == solution_grid diff --git a/tests/test_printer.py b/tests/test_printer.py index 06092ca..feb6a9b 100644 --- a/tests/test_printer.py +++ b/tests/test_printer.py @@ -1,7 +1,7 @@ from contextlib import redirect_stdout import io -from sudoku.printer import display_grid, display_pylist +from sudoku import printer sudoku_grid = [ [0, 6, 0, 0, 0, 0, 1, 9, 0], @@ -28,6 +28,8 @@ 0 0 0 | 0 0 0 | 4 0 0 ''' +single_string = "060000190002610004701000000000070010006083000540060003080027039000400078000000400" + python_string = '''sudoku_grid = [ [0, 6, 0, 0, 0, 0, 1, 9, 0], [0, 0, 2, 6, 1, 0, 0, 0, 4], @@ -44,7 +46,7 @@ def test_display_grid(): f = io.StringIO() with redirect_stdout(f): - display_grid(sudoku_grid) + printer.display_grid(sudoku_grid) out = f.getvalue() assert out == sudoku_string @@ -52,6 +54,11 @@ def test_display_grid(): def test_display_pygrid(): f = io.StringIO() with redirect_stdout(f): - display_pylist(sudoku_grid) + printer.display_pylist(sudoku_grid) out = f.getvalue() - assert out == python_string \ No newline at end of file + assert out == python_string + + +def test_as_single_string(): + result = printer.as_string(sudoku_grid) + assert result == single_string \ No newline at end of file