From 5525dfa54a27d33f05fbeefc419379d51e278dce Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Wed, 15 Nov 2023 13:40:56 +0100 Subject: [PATCH 1/3] test solution_count on small_trimino problem --- tests/data.py | 53 +++++++++++++++++++++++++++++++ tests/test_solution_count.py | 7 +++++ tests/test_trimino_based.py | 61 +++++++----------------------------- 3 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 tests/data.py diff --git a/tests/data.py b/tests/data.py new file mode 100644 index 0000000..d40bd9e --- /dev/null +++ b/tests/data.py @@ -0,0 +1,53 @@ +import numpy as np + +from exact_cover.io import DTYPE_FOR_ARRAY + +# one specific problem that I had trouble with +# originally based on solving the trivial problem +# of arranging 2 identical triminos on a 3x3 board + +# +--+ +# | | +# +--+--+ +# | | | +# +--+--+ + +# +--+--+--+ +# |xx| |xx| +# +--+--+--+ +# | | | | +# +--+--+--+ +# |xx| | | +# +--+--+--+ + +# this problem has 2 solutions +# (5, 13) and (6, 12) +def small_trimino_problem(): + to_cover = [ + [1, 0, 0, 1, 1, 0, 1, 0], + [1, 0, 0, 0, 1, 1, 0, 1], + [1, 0, 0, 0, 1, 1, 1, 0], + [1, 0, 1, 0, 1, 1, 0, 0], + [1, 0, 0, 0, 1, 0, 1, 1], + [1, 0, 1, 1, 1, 0, 0, 0], # <- 5 + [1, 0, 0, 0, 0, 1, 1, 1], # <- 6 + [0, 1, 0, 1, 1, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 1], + [0, 1, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 0, 1, 1, 0, 0], + [0, 1, 0, 0, 1, 0, 1, 1], + [0, 1, 1, 1, 1, 0, 0, 0], # <- 12 + [0, 1, 0, 0, 0, 1, 1, 1], # <- 13 + ] + return dict( + data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), + solution1=[5, 13], + solution_count=2, + ) + +def small_trimino_problem_from_file(): + return dict( + data=np.load("tests/files/small_trimino_problem.npy"), + solution1=[5, 13], + solution_count=2, + ) diff --git a/tests/test_solution_count.py b/tests/test_solution_count.py index 62773ba..0ccb5d1 100644 --- a/tests/test_solution_count.py +++ b/tests/test_solution_count.py @@ -3,6 +3,7 @@ from exact_cover_impl import get_solution_count from exact_cover.io import DTYPE_FOR_ARRAY +from .data import small_trimino_problem def test_solution_count(): data = np.array([[1, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], dtype=DTYPE_FOR_ARRAY) @@ -20,3 +21,9 @@ def test_solution_count_no_solutions(): data = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0]], dtype=DTYPE_FOR_ARRAY) result = get_solution_count(data) assert result == 0 + +def test_small_trimino_problem(): + problem = small_trimino_problem() + data = problem['data'] + result = get_solution_count(data) + assert result == problem['solution_count'] diff --git a/tests/test_trimino_based.py b/tests/test_trimino_based.py index cb2bdc1..29a5734 100644 --- a/tests/test_trimino_based.py +++ b/tests/test_trimino_based.py @@ -3,60 +3,19 @@ from exact_cover import get_exact_cover from exact_cover.error import NoSolution -from exact_cover.io import DTYPE_FOR_ARRAY -# from exact_cover.helpers import is_solution +from .data import ( + # the exact cover matrix built manually + small_trimino_problem, + # the same store in a file + small_trimino_problem_from_file) -DTYPE = dict(dtype=DTYPE_FOR_ARRAY) -# from exact_cover.io import load_problem - -# from tests.config import GLOBAL_CONFIG - - -# one specific problem that I had trouble with -# originally based on solving the trivial problem -# of arranging 2 identical triminos on a 3x3 board - -# +--+ -# | | -# +--+--+ -# | | | -# +--+--+ - -# +--+--+--+ -# |xx| |xx| -# +--+--+--+ -# | | | | -# +--+--+--+ -# |xx| | | -# +--+--+--+ - - -# the exact cover matrix built manually def input1(): - to_cover = [ - [1, 0, 0, 1, 1, 0, 1, 0], - [1, 0, 0, 0, 1, 1, 0, 1], - [1, 0, 0, 0, 1, 1, 1, 0], - [1, 0, 1, 0, 1, 1, 0, 0], - [1, 0, 0, 0, 1, 0, 1, 1], - [1, 0, 1, 1, 1, 0, 0, 0], # <- 5 - [1, 0, 0, 0, 0, 1, 1, 1], - [0, 1, 0, 1, 1, 0, 1, 0], - [0, 1, 0, 0, 1, 1, 0, 1], - [0, 1, 0, 0, 1, 1, 1, 0], - [0, 1, 1, 0, 1, 1, 0, 0], - [0, 1, 0, 0, 1, 0, 1, 1], - [0, 1, 1, 1, 1, 0, 0, 0], - [0, 1, 0, 0, 0, 1, 1, 1], # <- 13 - ] - return np.array(to_cover, **DTYPE) - + return small_trimino_problem()['data'] def input2(): - return np.load("tests/files/small_trimino_problem.npy") - + return small_trimino_problem_from_file()['data'] def test_inputs_are_equal(): m1 = input1() @@ -77,8 +36,10 @@ def run_on_input(array, expected): def test_input1(): - run_on_input(input1(), [5, 13]) + expected = small_trimino_problem()['solution1'] + run_on_input(input1(), expected) def test_input2(): - run_on_input(input2(), [5, 13]) + expected = small_trimino_problem_from_file()['solution1'] + run_on_input(input2(), expected) From 26d843587f17c2dce3366e9a694ee03e1a51975d Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Wed, 15 Nov 2023 14:05:34 +0100 Subject: [PATCH 2/3] solution_count tested through another problem --- tests/data.py | 18 ++++++++++++++++++ tests/test_solution_count.py | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/tests/data.py b/tests/data.py index d40bd9e..a418973 100644 --- a/tests/data.py +++ b/tests/data.py @@ -51,3 +51,21 @@ def small_trimino_problem_from_file(): solution1=[5, 13], solution_count=2, ) + +# https://en.wikipedia.org/wiki/Exact_cover#Detailed_example +def detailed_wikipedia_problem(): + sets = [ + {1, 4, 7}, + {1, 4}, # <- 1 + {4, 5, 7}, + {3, 5, 6}, # <- 3 + {2, 3, 6, 7}, + {2, 7}, # <- 5 + ] + return dict( + data=np.array( + [[1 if i in s else 0 for i in range(1, 8)] for s in sets], + dtype=DTYPE_FOR_ARRAY), + solution1=[1, 3, 5], + solution_count=1, + ) diff --git a/tests/test_solution_count.py b/tests/test_solution_count.py index 0ccb5d1..dafe64e 100644 --- a/tests/test_solution_count.py +++ b/tests/test_solution_count.py @@ -4,6 +4,7 @@ from exact_cover.io import DTYPE_FOR_ARRAY from .data import small_trimino_problem +from .data import detailed_wikipedia_problem def test_solution_count(): data = np.array([[1, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], dtype=DTYPE_FOR_ARRAY) @@ -27,3 +28,9 @@ def test_small_trimino_problem(): data = problem['data'] result = get_solution_count(data) assert result == problem['solution_count'] + +def test_detailed_wikipedia_problem(): + problem = detailed_wikipedia_problem() + data = problem['data'] + result = get_solution_count(data) + assert result == problem['solution_count'] From 4faecc5533d1007f4a7413daa94effdb273a37a8 Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Wed, 15 Nov 2023 16:52:24 +0100 Subject: [PATCH 3/3] more tests renamed data.py into problems.py add tests/bruteforce.py to compute brute force solutions 3 more problems with 2, 9 and 16 solutions --- tests/bruteforce.py | 23 +++++++ tests/data.py | 71 -------------------- tests/problems.py | 126 +++++++++++++++++++++++++++++++++++ tests/test_solution_count.py | 23 ++++++- tests/test_trimino_based.py | 2 +- 5 files changed, 171 insertions(+), 74 deletions(-) create mode 100644 tests/bruteforce.py delete mode 100644 tests/data.py create mode 100644 tests/problems.py diff --git a/tests/bruteforce.py b/tests/bruteforce.py new file mode 100644 index 0000000..741e5d1 --- /dev/null +++ b/tests/bruteforce.py @@ -0,0 +1,23 @@ +""" +bruteforce algorithm to compute the expected solutions +and help write tests +""" + +from itertools import chain, combinations + +import numpy as np + +# from the itertools module documentation +def powerset(iterable): + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(len(s)+1)) + + +def bruteforce(data): + """ + Brute-force generator of all exact cover solutions + """ + for subset in powerset(range(data.shape[0])): + if np.all(data[list(subset)].sum(axis=0) == 1): + yield subset diff --git a/tests/data.py b/tests/data.py deleted file mode 100644 index a418973..0000000 --- a/tests/data.py +++ /dev/null @@ -1,71 +0,0 @@ -import numpy as np - -from exact_cover.io import DTYPE_FOR_ARRAY - -# one specific problem that I had trouble with -# originally based on solving the trivial problem -# of arranging 2 identical triminos on a 3x3 board - -# +--+ -# | | -# +--+--+ -# | | | -# +--+--+ - -# +--+--+--+ -# |xx| |xx| -# +--+--+--+ -# | | | | -# +--+--+--+ -# |xx| | | -# +--+--+--+ - -# this problem has 2 solutions -# (5, 13) and (6, 12) -def small_trimino_problem(): - to_cover = [ - [1, 0, 0, 1, 1, 0, 1, 0], - [1, 0, 0, 0, 1, 1, 0, 1], - [1, 0, 0, 0, 1, 1, 1, 0], - [1, 0, 1, 0, 1, 1, 0, 0], - [1, 0, 0, 0, 1, 0, 1, 1], - [1, 0, 1, 1, 1, 0, 0, 0], # <- 5 - [1, 0, 0, 0, 0, 1, 1, 1], # <- 6 - [0, 1, 0, 1, 1, 0, 1, 0], - [0, 1, 0, 0, 1, 1, 0, 1], - [0, 1, 0, 0, 1, 1, 1, 0], - [0, 1, 1, 0, 1, 1, 0, 0], - [0, 1, 0, 0, 1, 0, 1, 1], - [0, 1, 1, 1, 1, 0, 0, 0], # <- 12 - [0, 1, 0, 0, 0, 1, 1, 1], # <- 13 - ] - return dict( - data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), - solution1=[5, 13], - solution_count=2, - ) - -def small_trimino_problem_from_file(): - return dict( - data=np.load("tests/files/small_trimino_problem.npy"), - solution1=[5, 13], - solution_count=2, - ) - -# https://en.wikipedia.org/wiki/Exact_cover#Detailed_example -def detailed_wikipedia_problem(): - sets = [ - {1, 4, 7}, - {1, 4}, # <- 1 - {4, 5, 7}, - {3, 5, 6}, # <- 3 - {2, 3, 6, 7}, - {2, 7}, # <- 5 - ] - return dict( - data=np.array( - [[1 if i in s else 0 for i in range(1, 8)] for s in sets], - dtype=DTYPE_FOR_ARRAY), - solution1=[1, 3, 5], - solution_count=1, - ) diff --git a/tests/problems.py b/tests/problems.py new file mode 100644 index 0000000..9c771d4 --- /dev/null +++ b/tests/problems.py @@ -0,0 +1,126 @@ +import numpy as np + +from exact_cover.io import DTYPE_FOR_ARRAY + +# one specific problem that I had trouble with +# originally based on solving the trivial problem +# of arranging 2 identical triminos on a 3x3 board + +# +--+ +# | | +# +--+--+ +# | | | +# +--+--+ + +# +--+--+--+ +# |xx| |xx| +# +--+--+--+ +# | | | | +# +--+--+--+ +# |xx| | | +# +--+--+--+ + +# this problem has 2 solutions +# (5, 13) and (6, 12) +def small_trimino_problem(): + to_cover = [ + [1, 0, 0, 1, 1, 0, 1, 0], + [1, 0, 0, 0, 1, 1, 0, 1], + [1, 0, 0, 0, 1, 1, 1, 0], + [1, 0, 1, 0, 1, 1, 0, 0], + [1, 0, 0, 0, 1, 0, 1, 1], + [1, 0, 1, 1, 1, 0, 0, 0], # <- 5 + [1, 0, 0, 0, 0, 1, 1, 1], # <- 6 + [0, 1, 0, 1, 1, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 1], + [0, 1, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 0, 1, 1, 0, 0], + [0, 1, 0, 0, 1, 0, 1, 1], + [0, 1, 1, 1, 1, 0, 0, 0], # <- 12 + [0, 1, 0, 0, 0, 1, 1, 1], # <- 13 + ] + return dict( + data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), + solution1=[5, 13], + solution_count=2, + ) + +def small_trimino_problem_from_file(): + return dict( + data=np.load("tests/files/small_trimino_problem.npy"), + solution1=[5, 13], + solution_count=2, + ) + +# https://en.wikipedia.org/wiki/Exact_cover#Detailed_example +def detailed_wikipedia_problem(): + sets = [ + {1, 4, 7}, + {1, 4}, # <- 1 + {4, 5, 7}, + {3, 5, 6}, # <- 3 + {2, 3, 6, 7}, + {2, 7}, # <- 5 + ] + return dict( + data=np.array( + [[1 if i in s else 0 for i in range(1, 8)] for s in sets], + dtype=DTYPE_FOR_ARRAY), + solution1=[1, 3, 5], + solution_count=1, + ) + +def bruteforce_problem1(): + to_cover = [ + [1, 0, 0, 1, 0, 0, 1, 0], # <- sol1 + [0, 1, 0, 0, 1, 0, 0, 1], # <- sol1 + [0, 0, 1, 0, 0, 1, 0, 0], # <- sol1 + [0, 0, 0, 1, 0, 0, 0, 0], # <- sol2 + [1, 0, 1, 0, 1, 0, 0, 1], # <- sol2 + [0, 1, 0, 0, 0, 1, 1, 0], # <- sol2 + + ] + return dict( + data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), + solution1=[0, 1, 2], + solution_count=2, + ) + +def bruteforce_problem2(): + to_cover = [ + [1, 0, 0, 1, 0, 0, 1, 0], # <- sol1 + [0, 1, 0, 0, 1, 0, 0, 1], # <- sol1 + [0, 0, 1, 0, 0, 1, 0, 0], # <- sol1 + [0, 0, 0, 1, 0, 0, 0, 0], # <- sol2 + [1, 0, 1, 0, 1, 0, 0, 1], # <- sol2 + [0, 1, 0, 0, 0, 1, 1, 0], # <- sol2 + [1, 0, 0, 1, 0, 0, 1, 0], # <- sol1 + [0, 1, 0, 0, 1, 0, 0, 1], # <- sol1 + [0, 0, 1, 0, 0, 1, 0, 0], # <- sol1 + ] + return dict( + data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), + solution1=[0, 1, 2], + solution_count=9, + ) + +def bruteforce_problem3(): + to_cover = [ + [1, 0, 0, 1, 0, 0, 1, 0], # <- sol1 + [0, 1, 0, 0, 1, 0, 0, 1], # <- sol1 + [0, 0, 1, 0, 0, 1, 0, 0], # <- sol1 + [0, 0, 0, 1, 0, 0, 0, 0], # <- sol2 + [1, 0, 1, 0, 1, 0, 0, 1], # <- sol2 + [0, 1, 0, 0, 0, 1, 1, 0], # <- sol2 + [1, 0, 0, 1, 0, 0, 1, 0], # <- sol1 + [0, 1, 0, 0, 1, 0, 0, 1], # <- sol1 + [0, 0, 1, 0, 0, 1, 0, 0], # <- sol1 + [0, 0, 0, 1, 0, 0, 0, 0], # <- sol2 + [1, 0, 1, 0, 1, 0, 0, 1], # <- sol2 + [0, 1, 0, 0, 0, 1, 1, 0], # <- sol2 + ] + return dict( + data=np.array(to_cover, dtype=DTYPE_FOR_ARRAY), + solution1=[0, 1, 2], + solution_count=16, + ) diff --git a/tests/test_solution_count.py b/tests/test_solution_count.py index dafe64e..2822cdb 100644 --- a/tests/test_solution_count.py +++ b/tests/test_solution_count.py @@ -3,8 +3,9 @@ from exact_cover_impl import get_solution_count from exact_cover.io import DTYPE_FOR_ARRAY -from .data import small_trimino_problem -from .data import detailed_wikipedia_problem +from .problems import small_trimino_problem +from .problems import detailed_wikipedia_problem +from .problems import bruteforce_problem1 def test_solution_count(): data = np.array([[1, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], dtype=DTYPE_FOR_ARRAY) @@ -34,3 +35,21 @@ def test_detailed_wikipedia_problem(): data = problem['data'] result = get_solution_count(data) assert result == problem['solution_count'] + +def test_bruteforce_problem1(): + problem = bruteforce_problem1() + data = problem['data'] + result = get_solution_count(data) + assert result == problem['solution_count'] + +def test_bruteforce_problem2(): + problem = bruteforce_problem2() + data = problem['data'] + result = get_solution_count(data) + assert result == problem['solution_count'] + +def test_bruteforce_problem3(): + problem = bruteforce_problem3() + data = problem['data'] + result = get_solution_count(data) + assert result == problem['solution_count'] diff --git a/tests/test_trimino_based.py b/tests/test_trimino_based.py index 29a5734..39b22d8 100644 --- a/tests/test_trimino_based.py +++ b/tests/test_trimino_based.py @@ -4,7 +4,7 @@ from exact_cover import get_exact_cover from exact_cover.error import NoSolution -from .data import ( +from .problems import ( # the exact cover matrix built manually small_trimino_problem, # the same store in a file