diff --git a/exact_cover/wrapper.py b/exact_cover/wrapper.py index 27321fb..ec0f8f1 100644 --- a/exact_cover/wrapper.py +++ b/exact_cover/wrapper.py @@ -3,12 +3,17 @@ from .error import NoSolution from .io import DTYPE_FOR_ARRAY +import numpy as np + def get_exact_cover(matrix): - if matrix.dtype == DTYPE_FOR_ARRAY: - result = raw_get_exact_cover(matrix) - else: - result = raw_get_exact_cover(matrix.astype(DTYPE_FOR_ARRAY)) + transformed = np.require( + matrix, dtype=DTYPE_FOR_ARRAY, requirements=["C_CONTIGUOUS"] + ) + assert ( + transformed.flags.c_contiguous + ), "We depend on the input array being C contiguous for raw goodness." + result = raw_get_exact_cover(transformed) if result.size == 0: raise NoSolution("No solutions found by the C code.") return result diff --git a/pyproject.toml b/pyproject.toml index ee59ee7..fbb78f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "exact_cover" -version = "1.2.2" +version = "1.3.0a0" description = "Solve exact cover problems" readme = "README.md" authors = ["Moy Easwaran"] @@ -25,6 +25,8 @@ numpy = [ ] setuptools = ">=51.1.2" +# When we build wheels, we always do so with an explicit pinned numpy version +# [tool.poetry.group.wheel_builder] optional = true diff --git a/tests/files/tiny_cover_problem.npy b/tests/files/tiny_cover_problem.npy new file mode 100644 index 0000000..35f70cd Binary files /dev/null and b/tests/files/tiny_cover_problem.npy differ diff --git a/tests/test_exact_cover.py b/tests/test_exact_cover.py index cb3a118..4713972 100644 --- a/tests/test_exact_cover.py +++ b/tests/test_exact_cover.py @@ -18,6 +18,31 @@ def test_exact_cover(): np.testing.assert_array_equal(actual, expected) +def test_exact_cover_c_order_array(): + data = np.array( + [[1, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], dtype=DTYPE_FOR_ARRAY, order="C" + ) + expected = np.array([0, 1, 3]) + actual = get_exact_cover(data) + np.testing.assert_array_equal(actual, expected) + + +def test_exact_cover_fortran_order_array(): + data = np.array( + [[1, 0, 0], [0, 1, 0], [0, 1, 1], [0, 0, 1]], dtype=DTYPE_FOR_ARRAY, order="F" + ) + expected = np.array([0, 1, 3]) + actual = get_exact_cover(data) + np.testing.assert_array_equal(actual, expected) + + +def test_exact_cover_read_from_file(): + data = np.load("tests/files/tiny_cover_problem.npy") + expected = np.array([0, 1, 3]) + actual = get_exact_cover(data) + np.testing.assert_array_equal(actual, expected) + + @given(sampled_from([np.int32, np.int8, np.bool_, None, DTYPE_FOR_ARRAY])) def test_exact_cover_different_dtypes(dtype): """ diff --git a/tests/test_exact_cover_problems.py b/tests/test_exact_cover_problems.py index c1f2564..b2243bd 100644 --- a/tests/test_exact_cover_problems.py +++ b/tests/test_exact_cover_problems.py @@ -4,7 +4,7 @@ from hypothesis import given, example from hypothesis.strategies import integers, lists, booleans from hypothesis.strategies import composite, one_of, permutations -from hypothesis.strategies import just +from hypothesis.strategies import just, sampled_from from exact_cover import get_exact_cover from exact_cover.error import NoSolution @@ -13,6 +13,19 @@ from tests.config import GLOBAL_CONFIG +@composite +def numpy_array_params(draw): + # Mix up the way we construct a numpy array a bit, to ensure stability. + order = draw(sampled_from([None, "C", "F"])) + dtype = draw(sampled_from([None, DTYPE_FOR_ARRAY, np.bool_, np.int8, np.int32])) + params = {} + if order is not None: + params["order"] = order + if dtype is not None: + params["dtype"] = dtype + return params + + @composite def exact_cover_problem(draw): width = draw(integers(min_value=1, max_value=15)) @@ -21,7 +34,9 @@ def exact_cover_problem(draw): lists(booleans(), min_size=width, max_size=width), min_size=1, max_size=30 ) ) - return np.array(data, dtype=DTYPE_FOR_ARRAY) + params = draw(numpy_array_params()) + + return np.array(data, **params) @given(exact_cover_problem()) @@ -56,7 +71,8 @@ def array_with_exact_cover(draw): cover_data = [[a == i for a in cover] for i in range(0, cover_size)] data = cover_data + dummy_data shuffled_data = draw(permutations(data)) - return np.array(shuffled_data, dtype=DTYPE_FOR_ARRAY) + params = draw(numpy_array_params()) + return np.array(shuffled_data, **params) @composite diff --git a/tests/test_trimino_based.py b/tests/test_trimino_based.py index 70bb22f..cb2bdc1 100644 --- a/tests/test_trimino_based.py +++ b/tests/test_trimino_based.py @@ -4,6 +4,7 @@ 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 DTYPE = dict(dtype=DTYPE_FOR_ARRAY) @@ -31,6 +32,7 @@ # |xx| | | # +--+--+--+ + # the exact cover matrix built manually def input1(): to_cover = [ @@ -51,27 +53,32 @@ def input1(): ] return np.array(to_cover, **DTYPE) + def input2(): return np.load("tests/files/small_trimino_problem.npy") + def test_inputs_are_equal(): m1 = input1() m2 = input2() # really equal, including their dtype - assert (np.all(np.equal(m1, m2)) - and (m1.dtype is m2.dtype) - and (m1.shape == m2.shape)) + assert ( + np.all(np.equal(m1, m2)) and (m1.dtype is m2.dtype) and (m1.shape == m2.shape) + ) + def run_on_input(array, expected): try: solution = get_exact_cover(array) assert np.all(solution == expected) - except NoSolution as exc: + except NoSolution: pytest.fail(f"no solution found for {array}") + def test_input1(): run_on_input(input1(), [5, 13]) + def test_input2(): run_on_input(input2(), [5, 13]) diff --git a/tools/run_tests.py b/tools/run_tests.py index 12303c3..0d2fbc9 100644 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -1,4 +1,5 @@ import doctest +import sys import pytest @@ -6,15 +7,17 @@ def test(): - pytest.main() + ret = pytest.main() + sys.exit(ret) def quicktest(): global GLOBAL_CONFIG GLOBAL_CONFIG["SKIP_SLOW"] = True - pytest.main(["--fail-slow", "2s"]) + ret = pytest.main(["--fail-slow", "2s"]) + sys.exit(ret) def run_doctest(): - doctest.testfile("../README.md") - doctest.testfile("../examples.md") + doctest.testfile("../README.md", raise_on_error=True) + doctest.testfile("../examples.md", raise_on_error=True)