diff --git a/.github/workflows/doc.yml b/.github/workflows/doctest.yml similarity index 88% rename from .github/workflows/doc.yml rename to .github/workflows/doctest.yml index 608cf2e..3105a53 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doctest.yml @@ -14,8 +14,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8"] - minizinc-version: ["2.8.2", "2.6.0"] + # use 3.12 until pytest-sphinx doesn't work with 3.13 + # https://github.com/twmr/pytest-sphinx/issues/67 + python-version: ["3.8", "3.12"] + minizinc-version: ["2.8.7", "2.6.0"] env: MINIZINC_URL: https://github.com/MiniZinc/MiniZincIDE/releases/download/${{ matrix.minizinc-version }}/MiniZincIDE-${{ matrix.minizinc-version }}-x86_64.AppImage @@ -59,4 +61,4 @@ jobs: pip install nox - name: Doc tests run: | - nox -s doc --reuse-existing-virtualenvs + nox -s doctest --reuse-existing-virtualenvs diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml index 7d7d67b..ce220e4 100644 --- a/.github/workflows/release-test.yml +++ b/.github/workflows/release-test.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - minizinc-version: ["2.8.2", "2.6.0"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + minizinc-version: ["2.8.7", "2.6.0"] env: MINIZINC_URL: https://github.com/MiniZinc/MiniZincIDE/releases/download/${{ matrix.minizinc-version }}/MiniZincIDE-${{ matrix.minizinc-version }}-x86_64.AppImage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8053fe5..8813e90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - minizinc-version: ["2.8.2", "2.6.0"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + minizinc-version: ["2.8.7", "2.6.0"] env: MINIZINC_URL: https://github.com/MiniZinc/MiniZincIDE/releases/download/${{ matrix.minizinc-version }}/MiniZincIDE-${{ matrix.minizinc-version }}-x86_64.AppImage diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 342975d..ee0e029 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8"] - minizinc-version: ["2.8.2", "2.6.0"] + python-version: ["3.8", "3.13"] + minizinc-version: ["2.8.7", "2.6.0"] env: MINIZINC_URL: https://github.com/MiniZinc/MiniZincIDE/releases/download/${{ matrix.minizinc-version }}/MiniZincIDE-${{ matrix.minizinc-version }}-x86_64.AppImage diff --git a/.gitignore b/.gitignore index bdc4d39..11b8975 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/source/api # PyBuilder target/ diff --git a/changelog.md b/changelog.md index cbb9593..1a56be2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,126 +1,206 @@ -### 0.4.4 -#### CI changes -- use minizinc 2.8.2 as maximum version in CI +## 0.5.0 + +#### Python interpreters support + +- Add 3.13 CPython. + +#### Added + +- Introduce the ``strict`` argument to the ``disjunctive`` constraint. +- Add ``abs``, ``exp``, ``ln``, ``log``, ``log10``, ``log2``, and ``sqrt`` functions. +- Add ``product`` function to calculate product of all elements in the array. +- Add trigonometric functions: `acos`, `asin`, `atan`, `cos`, `sin`, `tan`. + +#### Changed + +- Array indexing must now specify all indices. + +#### CI changes: + +- Use minizinc 2.8.7 instead of 2.8.2 ### 0.4.3 + #### Added + - `disjunctive` constraint + #### Fixed + - array slices with upper slice as operations should compile correctly now + #### Documentation -- simplify and fix layout of ``count``, ``cumulative`` + +- simplify and fix layout of ``count``, ``cumulative`` ``table`` and ``max`` examples - + ### 0.4.2 + #### CI changes + - use minizinc 2.7.6 as maximum version in CI (as in minizinc-python) - use minizinc 2.6.0 as minimum version in CI (as in minizinc-python) + #### Python interpreters support + - add 3.12 CPython ### 0.4.1 + #### Added + - ``table`` constraint - ``contains`` method for arrays and sets, to check if elem presented in collection - ``except_`` argument to ``all_different`` constraint ## 0.4.0 + #### Added + - ``cumulative`` constraint - ``forall`` constraint now supports enums which is not model's field + #### CI changes + - use minizinc 2.7.2 as maximum version in CI - use minizinc 2.5.4 as minimum version in CI (as in minizinc-python) ### 0.3.1 + #### Added + - var can be parametrized (you can assign values to them) + #### Fixed + - int fallback as a result of operation where both operands are float + #### CI changes + - use ruff for style checks ## 0.3 + #### Syntax and compatibility + - some arguments are made positional only + #### Fixes + - fix enum doc example - some minor fixes for internal code + #### Python interpreters support + - add 3.11 CPython - drop 3.7 CPython - drop pypy - + ### 0.2.4 + #### Added + - available_solver_tags function to get available solvers - optimization_level, n_processes, timeout and random_seed arg to solve + #### Fixed + - solve_maximize now correctly use solver arg + #### Changed + - add some type hints ### 0.2.3 + #### Added + - validation for float ranges in some constraints, e.g. forall + #### Fixed + - ranges with float values correctly set bigger limit + #### Deleted -- check for minizinc executable is available, -as it seems, python-minizinc implement it by itself. + +- check for minizinc executable is available, +as it seems, python-minizinc implement it by itself. ### 0.2.2 + #### Added + - sets support enums ### 0.2.1 + #### Added + - integer sets + #### Changed + - refactor some code ## 0.2 + #### Changed -- zython doesn't redefine builtin range function, + +- zython doesn't redefine builtin range function, use ``zn.range`` for float, zython's var/par types. + #### Python interpreters support + - drop 3.6 CPython - add 3.10 CPython ### 0.1.5 + #### Added + - possibility to choose solver - float fields support - float ranges support ### 0.1.4 + #### Changed + - an error about minizinc wasn't found in $PATH was changed to warning ### 0.1.3 + #### Added + - check for minizinc in $PATH for startup - documentation page about model parts + #### CI Changes -- Use minizinc 2.5.5 in CI. +- Use minizinc 2.5.5 in CI. ### 0.1.2 + #### Fixed + - some method of Operation and Constraint classes which were accessible by and visible for user are now hidden + #### Added + - ``increasing`` and ``decreasing`` constraints - ### 0.1.1 + #### Added + - ``allequal`` constraint - ``ndistinct`` function - ``except_0`` argument to ``alldifferent`` constraint -#### Changed: +#### Changed + - project description in readme.md - link to the html doc diff --git a/doc/readme.md b/doc/readme.md deleted file mode 100644 index 82c1f4e..0000000 --- a/doc/readme.md +++ /dev/null @@ -1,12 +0,0 @@ -To generate sphinx documentation from source doc comments -you should run following command (from project root) - -```shell -sphinx-apidoc -f -o docs/source/api zython -``` - -To build html doc (which will be in doc/build/html/index.html): - -```shell -python -m sphinx -T -E -b html -d _build/doctrees -D language=en doc/source doc/build/html -``` diff --git a/doc/source/guides/array_advanced/sudoku.rst b/doc/source/guides/array_advanced/sudoku.rst index d1977df..a9c5707 100644 --- a/doc/source/guides/array_advanced/sudoku.rst +++ b/doc/source/guides/array_advanced/sudoku.rst @@ -19,7 +19,7 @@ Python Model def __init__(self): self.a = zn.Array(zn.var(range(1, 10)), shape=(9, 9)) - self.constraints = [zn.forall(range(9), lambda i: zn.alldifferent(self.a[i])), + self.constraints = [zn.forall(range(9), lambda i: zn.alldifferent(self.a[i, :])), zn.forall(range(9), lambda i: zn.alldifferent(self.a[:, i])), zn.forall(range(3), lambda i: zn.forall(range(3), diff --git a/doc/source/guides/array_advanced/tasks_sheduling.rst b/doc/source/guides/array_advanced/tasks_sheduling.rst index 8edd7f5..881fa4e 100644 --- a/doc/source/guides/array_advanced/tasks_sheduling.rst +++ b/doc/source/guides/array_advanced/tasks_sheduling.rst @@ -2,37 +2,37 @@ Tasks Scheduling ================ The disjunctive constraint takes an array of start times for each task and -an array of their durations and makes sure that only one task is active at -any one time. +an array of their durations, ensuring that only one task is active at +any given time. .. note:: - It is suggested to use ranges and sequences of ranges instead of int, - because minizinc can return strange result when type of any arg is int + It is recommended to use ranges and sequences of ranges instead of integers, + because MiniZinc can return unexpected results when any argument is an integer. Model ----- -We will recreate the example of task sheduling problem from the -`minizinc `_ +We will recreate the example of the task scheduling problem from the +`MiniZinc `_ documentation. -The model consists of several jobs, which can be separated into several -steps. There are following restrictions: +The model consists of several jobs, which can be divided into several +steps. The following restrictions apply: -- to complete job, all steps should be executed -- different steps are independent: - if there is a job on first step, other job can be processed on second step, - without necessity to wait. -- if there is an active task on any step, not other job can be executed - on this step and should wait. +- To complete a job, all steps must be executed. +- Different steps are independent: + If there is a job on the first step, another job can be processed on the second step + without waiting. +- If there is an active task on any step, no other job can be executed + on that step and must wait. -You can think about this as a conveyor with `n_jobs` lines, -one part on every line and -`n_steps` independent machines which is shared between lines. -Every machine can complete only one manipulation with any part -and should work with a part processed by previous machine. -We are searching for the fastest way to processed all the parts. +You can think of this as a conveyor with `n_jobs` lines, +one part on every line, and +`n_steps` independent machines that are shared between lines. +Each machine can complete only one operation with any part +and must work with a part processed by the previous machine. +We are searching for the fastest way to process all the parts. Python Model ------------ @@ -76,8 +76,8 @@ Python Model return zn.forall( range(self.n_jobs), lambda i: zn.forall( - zn.range(self.n_tasks - 1), - lambda j: self.start[i, j] + self.durations[i, j] <= self.start[i, j + 1], + zn.range(self.n_tasks - 1), + lambda j: self.start[i, j] + self.durations[i, j] <= self.start[i, j + 1], ) & ( self.start[i, self.n_tasks - 1] + self.durations[i, self.n_tasks - 1] <= self.end ) @@ -92,3 +92,11 @@ Python Model .. testoutput:: Solution(objective=30, total=86, end=30, start=[[8, 9, 13, 18, 21], [5, 13, 18, 25, 27], [1, 5, 9, 13, 17], [0, 1, 2, 3, 9], [9, 16, 25, 27, 29]]) + + +Strict Mode +----------- + +The strict mode is specified by setting the `strict` argument to True. In this mode, there is a significant difference: + +- Tasks with a duration of 0 CANNOT be scheduled at any time but only when no other task is running. diff --git a/noxfile.py b/noxfile.py index 3b76add..81827d0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,22 +6,49 @@ @nox.session def lint(session): session.install("ruff") - session.run("ruff", "zython") + session.run("ruff", "check", "zython") @nox.session -def doc(session): +def test(session): + session.install("-r", "requirements.txt") + session.install("-r", "requirements_dev.txt") + session.run( + "pytest", + "-s", + "test", + "zython", + "--cov=zython", + "--cov-branch", + "--cov-report=term-missing", + "--doctest-modules", + ) + + +@nox.session +def doctest(session): session.install("-r", "requirements.txt") session.install("-r", "requirements_doc.txt") session.run("pytest", "doc", "--doctest-glob=*.rst", "--doctest-modules") @nox.session -def test(session): +def gendoc(session): session.install("-r", "requirements.txt") - session.install("-r", "requirements_dev.txt") + session.install("-r", "requirements_doc.txt") + session.run("sphinx-apidoc", "-f", "-o", "docs/source/api", "zython") session.run( - "pytest", "test", "zython", - "--cov=zython", "--cov-branch", "--cov-report=term-missing", - "--doctest-modules", + "python", + "-m", + "sphinx", + "-T", + "-E", + "-b", + "html", + "-d", + "docs/_build/doctrees", + "-D", + "language=en", + "doc/source", + "doc/build/html", ) diff --git a/pyproject.toml b/pyproject.toml index e8553b5..8c62df9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [tool.ruff] +target-version = "py38" line-length = 120 -extend-select = ["RUF100"] +lint.extend-select = ["RUF100"] # imported but unused -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" [tool.pytest.ini_options] diff --git a/requirements_dev.txt b/requirements_dev.txt index 0170c93..2ef134e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ pytest pytest-cov nox +ruff diff --git a/test/compile/test_var.py b/test/compile/test_var.py index d1a761d..56d3dd9 100644 --- a/test/compile/test_var.py +++ b/test/compile/test_var.py @@ -26,7 +26,7 @@ def __init__(self): self.b = zn.sum(self.a[:, self.start + 1:self.start * 3]) model = MyModel() - result = model.solve_satisfy(verbose=True) + result = model.solve_satisfy() assert 9 == result["b"] diff --git a/test/model/test_product.py b/test/model/test_product.py new file mode 100644 index 0000000..33d0773 --- /dev/null +++ b/test/model/test_product.py @@ -0,0 +1,53 @@ +import zython as zn + + +def test_2d(): + class MyModel(zn.Model): + def __init__(self, array): + self.a = zn.Array(array) + self.p1 = zn.product(self.a[0, 1:2]) + self.p2 = zn.product(self.a[2:, 1:]) + self.p3 = zn.product(self.a[2:, 2:]) + + model = MyModel([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) + result = model.solve_satisfy() + assert result["p1"] == 2 + assert result["p2"] == 72 + assert result["p3"] == 9 + + +def test_3(): + class MyModel(zn.Model): + def __init__(self, array): + self.a = zn.Array(array) + self.p = zn.product(self.a) + self.p1 = zn.product(self.a[:, 1:, :]) + + array = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] + model = MyModel(array) + result = model.solve_satisfy() + assert result["p"] == 40320 + assert result["p1"] == 672 + + +def test_1(): + class MyModel(zn.Model): + def __init__(self, array): + self.a = zn.Array(array) + self.p = zn.product(self.a) + + array = [1, 2, 3, 4, 5, 6, 7, 8] + model = MyModel(array) + result = model.solve_satisfy() + assert result["p"] == 40320 + + +def test_empty(): + class MyModel(zn.Model): + def __init__(self): + self.a = zn.Array([1, 2]) + self.p = zn.product(self.a[3:]) + + model = MyModel() + result = model.solve_satisfy() + assert result["p"] == 1 diff --git a/test/model/test_sum.py b/test/model/test_sum.py index 9ccf427..7bc4e4b 100644 --- a/test/model/test_sum.py +++ b/test/model/test_sum.py @@ -22,10 +22,10 @@ class MyModel(zn.Model): def __init__(self, array): self.a = zn.Array(array) self.s = zn.sum(self.a) - self.s1 = zn.sum(self.a[:, 1:]) + self.s1 = zn.sum(self.a[:, :, 1:]) array = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] model = MyModel(array) result = model.solve_satisfy() assert result["s"] == 36 - assert result["s1"] == 22 + assert result["s1"] == 20 diff --git a/test/operations/test_disjunctive.py b/test/operations/test_disjunctive.py index b252556..8ba0ceb 100644 --- a/test/operations/test_disjunctive.py +++ b/test/operations/test_disjunctive.py @@ -1,7 +1,9 @@ +import pytest import zython as zn -def test_ok(): +@pytest.mark.parametrize("array", [(1, 2, 3), ((1, 2, 3), (1, 2, 3)), zn.Array(zn.var(zn.range(10)), shape=3)]) +def test_ok(array): array = zn.Array(zn.var(zn.range(10)), shape=3) array._name = "array" - zn.disjunctive(array, [1, 2, 3]) + zn.disjunctive(array, array) diff --git a/test/operations/test_iternal.py b/test/operations/test_iternal.py index 5ff260f..c1f961d 100644 --- a/test/operations/test_iternal.py +++ b/test/operations/test_iternal.py @@ -21,7 +21,7 @@ def fn(par): return 2 - par array = zn.Array(zn.var(int), shape=(3, 4)) - iter_var, op = get_iter_var_and_op(array[2:], fn) + iter_var, op = get_iter_var_and_op(array[2:, :], fn) assert iter_var.name == "par" assert iter_var.type is int assert op.op == _Op_code.sub diff --git a/test/var/test_array.py b/test/var/test_array.py index 01cc207..5d4157d 100644 --- a/test/var/test_array.py +++ b/test/var/test_array.py @@ -6,15 +6,18 @@ from zython.operations._op_codes import _Op_code -@pytest.mark.parametrize("array", [[1, 2, 3, 4], (1, 2, 3, 4), (i + 1 for i in range(4))], - ids=["list", "tuple", "genexpr"]) +@pytest.mark.parametrize( + "array", + [[1, 2, 3, 4], (1, 2, 3, 4), (i + 1 for i in range(4))], + ids=["list", "tuple", "genexpr"], +) def test_creating(array): class MyModel(zn.Model): def __init__(self, array): self.a = zn.Array(array) model = MyModel(array) - assert model.a._shape == (4, ) + assert model.a._shape == (4,) def test_2d_correct(): @@ -46,23 +49,32 @@ def test_3d_gen(): class MyModel(zn.Model): def __init__(self, array): self.a = zn.Array(array) - self.s1 = zn.sum(self.a[0, 1:2]) - self.s2 = zn.sum(self.a[2:, 1:]) - self.s3 = zn.sum(self.a[2:, 2:]) + self.s1 = zn.sum(self.a[0, 1:2, :]) + self.s2 = zn.sum(self.a[2:, 1:, :]) + self.s3 = zn.sum(self.a[2:, 2:, :]) r = ((range(i, i + 3) for i in range(2)) for _ in range(4)) model = MyModel(((j for j in i) for i in r)) assert model.a._shape == (4, 2, 3) - assert model.a.value == [[[0, 1, 2], [1, 2, 3]], [[0, 1, 2], [1, 2, 3]], [[0, 1, 2], [1, 2, 3]], [[0, 1, 2], [1, 2, 3]]] + assert model.a.value == [ + [[0, 1, 2], [1, 2, 3]], + [[0, 1, 2], [1, 2, 3]], + [[0, 1, 2], [1, 2, 3]], + [[0, 1, 2], [1, 2, 3]], + ] -@pytest.mark.parametrize("array", ([1, 0.0], ((1, 0.0), (1, 3)), [[1, 3], [1, "a"]], (((1, "a"), ), ))) +@pytest.mark.parametrize( + "array", ([1, 0.0], ((1, 0.0), (1, 3)), [[1, 3], [1, "a"]], (((1, "a"),),)) +) def test_different_types(array): class MyModel(zn.Model): def __init__(self, array): self.a = zn.Array(array) - with pytest.raises(ValueError, match="All elements of the array should be the same type"): + with pytest.raises( + ValueError, match="All elements of the array should be the same type" + ): MyModel(array) @@ -71,20 +83,26 @@ def test_var_array_without_shape(): zn.Array(zn.var(int)) -@pytest.mark.parametrize("array", (zn.Array([[1], [2]]), zn.Array(zn.var(int), shape=(1, 2)))) +@pytest.mark.parametrize( + "array", (zn.Array([[1], [2]]), zn.Array(zn.var(int), shape=(1, 2))) +) @pytest.mark.parametrize("indexes", ((2, 0), (0, 2))) def test_array_index_error(array, indexes): with pytest.raises(IndexError): array[indexes] -@pytest.mark.parametrize("array", (((1, 0, 1), (1, 3)), [[1, 3], [1, 1, 0]], (((1, 1), 1), ))) +@pytest.mark.parametrize( + "array", (((1, 0, 1), (1, 3)), [[1, 3], [1, 1, 0]], (((1, 1), 1),)) +) def test_different_length(array): class MyModel(zn.Model): def __init__(self, array): self.a = zn.Array(array) - with pytest.raises(ValueError, match="Subarrays of different length are not supported"): + with pytest.raises( + ValueError, match="Subarrays of different length are not supported" + ): MyModel(array) @@ -94,59 +112,87 @@ def test_int_1d(self): a = a[1] assert a.pos == (1,) - def test_tuple_1d(self): + def test_2d_index_on_1d_array(self): a = zn.Array([1, 2, 3]) - with pytest.raises(ValueError, match="Array has 1 dimensions but 2 were specified"): + with pytest.raises( + ValueError, match="The array has 1 dimensions, but 2 indexes were specified" + ): _ = a[1, 0] + @pytest.mark.parametrize( + "array, pos, error", + [ + ( + zn.Array([[1, 2]]), + 4, + "The array has 2 dimensions, but 1 indexes were specified", + ), + ( + zn.Array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]), + (slice(1, 2), slice(2, 4)), + "The array has 3 dimensions, but 2 indexes were specified", + ), + ], + ) + def test_not_every_index(self, array, pos, error): + with pytest.raises(ValueError, match=error): + array[pos] + def test_neg_int(self): a = zn.Array([1, 2, 3]) - with pytest.raises(ValueError, match="Negative indexes are not supported for now"): + with pytest.raises( + ValueError, match="Negative indexes are not supported for now" + ): _ = a[-1] def test_step(self): a = zn.Array([1, 2, 3]) - with pytest.raises(ValueError, match="step other then 1 isn't supported, but it is 2"): + with pytest.raises( + ValueError, match="step other then 1 isn't supported, but it is 2" + ): _ = a[1:3:2] @pytest.mark.parametrize("pos", [slice(-1, 2), slice(1, -2), slice(-1, -2)]) - def test_neg_slice_2d(self, pos): + @pytest.mark.parametrize("before", (True, False)) + def test_neg_slice_2d(self, pos, before): a = zn.Array([[1], [2], [3]]) - with pytest.raises(ValueError, match="Negative indexes are not supported for now"): - _ = a[pos] + with pytest.raises( + ValueError, match="Negative indexes are not supported for now" + ): + _ = a[:, pos] if before else a[pos, :] @pytest.mark.parametrize("start", (10, 15)) def test_slice_wrong_start(self, start): array = zn.Array(zn.var(int), shape=(3, 2, 4)) - with pytest.raises(ValueError, match=re.escape(f"start({start}) should be smaller then stop(10)")): - _ = array[1, start:10] - - @pytest.mark.parametrize("pos, expected", [((0, 1), (0, 1)), - ((0, slice(1, 3)), (0, slice(1, 3, 1)))]) + with pytest.raises( + ValueError, + match=re.escape(f"start({start}) should be smaller then stop(10)"), + ): + _ = array[1, start:10, :] + + @pytest.mark.parametrize( + "pos, expected", [((0, 1), (0, 1)), ((0, slice(1, 3)), (0, slice(1, 3, 1)))] + ) def test_2d(self, pos, expected): a = zn.Array([[1, 2], [2, 3], [3, 4]]) a = a[pos] assert a.pos == expected - @pytest.mark.parametrize("pos", (1, slice(1, 2))) - @pytest.mark.parametrize("array", (zn.Array([[1, 2], [2, 3], [3, 4]]), zn.Array(zn.var(int), shape=(3, 2, 4)))) - def test_added_last_dim(self, pos, array): - ndim = array.ndims() - array = array[pos] - assert len(array.pos) == ndim - assert array.pos[1].start == 0 - assert array.pos[1].stop.op == _Op_code.size - assert array.pos[1].step == 1 - class TestSize: @pytest.mark.parametrize("dim", (-1, 3)) def test_wrong_dim(self, dim): a = zn.Array([[1, 2], [2, 3], [3, 4]]) - with pytest.raises(ValueError, match=f"Array has 0\\.\\.2 dimensions, but {dim} were specified"): + with pytest.raises( + ValueError, + match=f"The array has 0\\.\\.2 dimensions, but {dim} were specified", + ): a.size(dim) - @pytest.mark.parametrize("array", [zn.Array([[1, 2], [2, 3], [3, 4]]), zn.Array(zn.var(int), shape=(3, 2))]) + @pytest.mark.parametrize( + "array", + [zn.Array([[1, 2], [2, 3], [3, 4]]), zn.Array(zn.var(int), shape=(3, 2))], + ) @pytest.mark.parametrize("dim, expected", ((0, 3), (1, 2))) def test_dim(self, array, dim, expected): class MyModel(zn.Model): diff --git a/zython/__init__.py b/zython/__init__.py index 2b9744b..dd5da5d 100644 --- a/zython/__init__.py +++ b/zython/__init__.py @@ -4,8 +4,43 @@ from zython.var_par.par import par from zython.var_par.collections.array import Array from zython.var_par.collections.set import Set -from zython.operations.functions_and_predicates import exists, forall, sum, alldifferent, circuit, count, min, max,\ - allequal, ndistinct, increasing, decreasing, cumulative, disjunctive, table +from zython.operations.functions_and_predicates import ( + abs, + exp, + ln, + log, + log10, + log2, + sqrt, + acos, + # acosh, + asin, + # asinh, + atan, + # atanh, + cos, + # cosh, + sin, + # sinh, + tan, + # tanh, + exists, + forall, + sum, + product, + alldifferent, + circuit, + count, + min, + max, + allequal, + ndistinct, + increasing, + decreasing, + cumulative, + disjunctive, + table, +) from zython.model import Model from zython.result import as_original diff --git a/zython/_compile/zinc/flags.py b/zython/_compile/zinc/flags.py index 69d2a7c..6d5f8ec 100644 --- a/zython/_compile/zinc/flags.py +++ b/zython/_compile/zinc/flags.py @@ -19,6 +19,7 @@ class Flags(enum.Flag): strictly_decreasing = enum.auto() cumulative = enum.auto() disjunctive = enum.auto() + disjunctive_strict = enum.auto() table = enum.auto() float_used = enum.auto() @@ -40,6 +41,7 @@ def append(src: SourceCode, line: str): Flags.strictly_decreasing: partial(append, line='include "strictly_decreasing.mzn";'), Flags.cumulative: partial(append, line='include "cumulative.mzn";'), Flags.disjunctive: partial(append, line='include "disjunctive.mzn";'), + Flags.disjunctive_strict: partial(append, line='include "disjunctive_strict.mzn";'), Flags.table: partial(append, line='include "table.mzn";'), Flags.float_used: lambda x: x, } diff --git a/zython/_compile/zinc/to_str.py b/zython/_compile/zinc/to_str.py index b73e92b..074f56b 100644 --- a/zython/_compile/zinc/to_str.py +++ b/zython/_compile/zinc/to_str.py @@ -193,12 +193,32 @@ def __init__(self): self[_Op_code.mod] = partial(_binary_op, "mod") self[_Op_code.in_] = partial(_binary_op, "in") self[_Op_code.pow] = _pow + self[_Op_code.sqrt] = partial(_call_func, "sqrt") self[_Op_code.invert] = partial(_unary_op, "not") self[_Op_code.forall] = partial(_two_brackets_op, "forall") self[_Op_code.exists] = partial(_two_brackets_op, "exists") # minizinc 2.5.0 doesn't support 2d array counting self[_Op_code.count] = partial(_one_or_two_brackets, "count", flatten_args=True) self[_Op_code.sum_] = partial(_one_or_two_brackets, "sum") + self[_Op_code.product] = partial(_one_or_two_brackets, "product") + self[_Op_code.abs] = partial(_call_func, "abs") + self[_Op_code.exp] = partial(_call_func, "exp") + self[_Op_code.ln] = partial(_call_func, "ln") + self[_Op_code.log] = partial(_call_func, "log") + self[_Op_code.log10] = partial(_call_func, "log10") + self[_Op_code.log2] = partial(_call_func, "log2") + self[_Op_code.acos] = partial(_call_func, "acos") + self[_Op_code.acosh] = partial(_call_func, "acosh") + self[_Op_code.asin] = partial(_call_func, "asin") + self[_Op_code.asinh] = partial(_call_func, "asinh") + self[_Op_code.atan] = partial(_call_func, "atan") + self[_Op_code.atanh] = partial(_call_func, "atanh") + self[_Op_code.cos] = partial(_call_func, "cos") + self[_Op_code.cosh] = partial(_call_func, "cosh") + self[_Op_code.sin] = partial(_call_func, "sin") + self[_Op_code.sinh] = partial(_call_func, "sinh") + self[_Op_code.tan] = partial(_call_func, "tan") + self[_Op_code.tanh] = partial(_call_func, "tanh") self[_Op_code.min_] = partial(_array_comprehension_call, "min") self[_Op_code.max_] = partial(_array_comprehension_call, "max") self[_Op_code.size] = _size @@ -214,6 +234,7 @@ def __init__(self): self[_Op_code.strictly_decreasing] = partial(_global_constraint, "strictly_decreasing") self[_Op_code.cumulative] = partial(_global_constraint, "cumulative") self[_Op_code.disjunctive] = partial(_global_constraint, "disjunctive") + self[_Op_code.disjunctive_strict] = partial(_global_constraint, "disjunctive_strict") self[_Op_code.table] = partial(_global_constraint, "table", flatten_args=False) def __missing__(self, key): # pragma: no cover diff --git a/zython/operations/_op_codes.py b/zython/operations/_op_codes.py index 17112e5..c68b9ce 100644 --- a/zython/operations/_op_codes.py +++ b/zython/operations/_op_codes.py @@ -19,9 +19,29 @@ class _Op_code(enum.Enum): floordiv = enum.auto() mod = enum.auto() pow = enum.auto() + sqrt = enum.auto() + abs = enum.auto() + exp = enum.auto() + ln = enum.auto() + log = enum.auto() + log10 = enum.auto() + log2 = enum.auto() + acos = enum.auto() + acosh = enum.auto() + asin = enum.auto() + asinh = enum.auto() + atan = enum.auto() + atanh = enum.auto() + cos = enum.auto() + cosh = enum.auto() + sin = enum.auto() + sinh = enum.auto() + tan = enum.auto() + tanh = enum.auto() forall = enum.auto() # 3 params: (seq, iter_var=None, func=None) exists = enum.auto() sum_ = enum.auto() + product = enum.auto() count = enum.auto() min_ = enum.auto() max_ = enum.auto() @@ -39,4 +59,5 @@ class _Op_code(enum.Enum): strictly_decreasing = enum.auto() cumulative = enum.auto() disjunctive = enum.auto() + disjunctive_strict = enum.auto() table = enum.auto() diff --git a/zython/operations/functions_and_predicates.py b/zython/operations/functions_and_predicates.py index 5d9c3d9..313bc0b 100644 --- a/zython/operations/functions_and_predicates.py +++ b/zython/operations/functions_and_predicates.py @@ -3,19 +3,17 @@ import zython from zython.operations import _iternal from zython.operations import constraint as constraint_module -from zython.operations import operation as operation_module from zython.operations._op_codes import _Op_code from zython.operations.constraint import Constraint from zython.operations.operation import Operation -from zython.var_par.collections.array import ArrayMixin from zython.var_par.collections.set import SetVar +from zython.var_par.get_type import get_base_type, derive_operation_type from zython.var_par.types import ZnSequence from zython.var_par.var import var -def exists(seq: ZnSequence, - func: Optional[Union["Constraint", Callable]] = None) -> Constraint: - """ Specify constraint which should be true for `at least` one element in ``seq``. +def exists(seq: ZnSequence, func: Optional[Union["Constraint", Callable]] = None) -> Constraint: + """Specify constraint which should be true for `at least` one element in ``seq``. The method has the same signature as ``forall``. @@ -42,8 +40,7 @@ def exists(seq: ZnSequence, return constraint_module._exists(seq, iter_var, operation) -def forall(seq: ZnSequence, - func: Optional[Union["Constraint", Callable]] = None) -> Constraint: +def forall(seq: ZnSequence, func: Optional[Union["Constraint", Callable]] = None) -> Constraint: """ Takes expression (that is, constraint) or function which return constraint and make them a single constraint which should be true for every element in the array. @@ -77,9 +74,8 @@ def forall(seq: ZnSequence, return constraint_module._forall(seq, iter_var, operation) -def sum(seq: ZnSequence, - func: Optional[Union["Constraint", Callable]] = None) -> Operation: - """ Calculate the sum of the ``seq`` according with ``func`` +def sum(seq: ZnSequence, func: Optional[Union["Constraint", Callable]] = None) -> Operation: + """Calculate the sum of the ``seq`` according with ``func`` Iterates through elements in seq and calculate their sum, you can modify summarized expressions by specifying ``func`` parameter. @@ -89,8 +85,8 @@ def sum(seq: ZnSequence, seq: range, array of var, or sequence (list or tuple) of var sequence to sum up func: Operation or Callable, optional - Operation which will be executed with every element and later sum up. Or function which returns - such operation. + Operation which will be executed with every element and later sum up. + Or function which returns such operation. If function or lambda it should be with 0 or 1 arguments only. Returns @@ -124,17 +120,60 @@ def sum(seq: ZnSequence, Solution(objective=5, a=4, b=3, c=5) """ iter_var, operation = _iternal.get_iter_var_and_op(seq, func) - if isinstance(seq, ArrayMixin) and operation is None: - type_ = seq.type - else: - type_ = operation.type - if type_ is None: - raise ValueError("Can't derive the type of {} expression".format(func)) - return operation_module._sum(seq, iter_var, operation, type_=type_) + type_ = derive_operation_type(seq, operation) + return Operation(_Op_code.sum_, seq, iter_var, operation, type_=type_) + +def product(seq: ZnSequence, func: Optional[Union["Constraint", Callable]] = None) -> Operation: + """Calculate the product of the ``seq`` according to ``func`` + + Iterates through elements in seq and calculates their product, you can modify the expressions + being multiplied by specifying the ``func`` parameter. + + Parameters + ---------- + seq: range, array of var, or sequence (list or tuple) of var + sequence to multiply + func: Operation or Callable, optional + Operation which will be executed with every element and later multiplied. + Or function which returns such operation. + If function or lambda it should be with 0 or 1 arguments only. + + Returns + ------- + result: Operation + Operation which will calculate the product + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.Array(zn.var(range(1, 5)), shape=4) + >>> model = MyModel() + >>> model.solve_minimize(zn.product(model.a)) + Solution(objective=1, a=[1, 1, 1, 1]) + + # find the product of squares of elements + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.var(zn.range(4)) + ... self.b = zn.var(zn.range(4)) + ... self.c = zn.var(zn.range(4)) + ... self.constraints = [zn.product((self.a, self.b, self.c), lambda i: i ** 2) == 36] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=3, b=2, c=1) + """ + iter_var, operation = _iternal.get_iter_var_and_op(seq, func) + type_ = derive_operation_type(seq, operation) + return Operation(_Op_code.product, seq, iter_var, operation, type_=type_) def count(seq: ZnSequence, value: Union[int, Operation, Callable[[ZnSequence], Operation]]) -> Operation: - """ Returns the number of occurrences of ``value`` in ``seq``. + """Returns the number of occurrences of ``value`` in ``seq``. Parameters ---------- @@ -183,16 +222,16 @@ def count(seq: ZnSequence, value: Union[int, Operation, Callable[[ZnSequence], O Counter({3: 1, 2: 1, 1: 1, 0: 1}) """ iter_var, operation = _iternal.get_iter_var_and_op(seq, value) - return operation_module._count(seq, iter_var, operation, type_=int) + return Operation(_Op_code.count, seq, iter_var, operation, type_=int) def cumulative( - start_times: ZnSequence, - durations: ZnSequence, - requirements: ZnSequence, - limit: Union[int, var], + start_times: ZnSequence, + durations: ZnSequence, + requirements: ZnSequence, + limit: Union[int, var], ) -> Constraint: - """ The cumulative constraint is used for describing cumulative resource usage. + """The cumulative constraint is used for describing cumulative resource usage. It requires that a set of tasks given by start times, durations, and resource requirements, never require more than a global resource limit at any one time. @@ -240,14 +279,15 @@ def cumulative( >>> result["limit"] 2 """ - return constraint_module.cumulative(start_times, durations, requirements, limit) + return Constraint(_Op_code.cumulative, start_times, durations, requirements, limit) def disjunctive( - start_times: ZnSequence, - durations: ZnSequence, + start_times: ZnSequence, + durations: ZnSequence, + strict: bool = False, ) -> Constraint: - """ The disjunctive constraint takes an array of start times for each task and + """The disjunctive constraint takes an array of start times for each task and an array of their durations and makes sure that only one task is active at any one time. Parameters @@ -256,6 +296,9 @@ def disjunctive( Sequence with start time of the tasks durations: range, array of var, or sequence (list or tuple) of var Sequence with durations of the tasks + strict: bool + Run in strict mode, which add the following restriction: + Tasks with a duration of 0 CANNOT be scheduled at any time but only when no other task is running. Returns ------- @@ -282,6 +325,8 @@ def disjunctive( >>> result["start"] [3, 1, 0] """ + if strict: + return Constraint(_Op_code.disjunctive_strict, start_times, durations) return Constraint(_Op_code.disjunctive, start_times, durations) @@ -318,11 +363,451 @@ def table( >>> result["a"] [1, 2, 3, 4] """ - return constraint_module.table(x, t) + return Constraint(_Op_code.table, x, t) + + +def abs(x: Union[float, var]) -> Operation: + """Return absolute value of x + + Returns + ------- + result: Operation + Operation which will find the abs. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.abs(-1) + ... self.p = zn.par(1) + ... self.v = zn.var(float) + ... self.b = zn.abs(self.p) + ... self.c = zn.abs(self.v) + ... self.constraints = [self.v == zn.abs(-3)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=1, v=3.0, b=1, c=3.0) + """ + return Operation(_Op_code.abs, x, type_=get_base_type(x)) + +def exp(x: Union[float, var]) -> Operation: + """Return the exponential of x + + Returns + ------- + result: Operation + Operation which will calculate the exponential. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.exp(1) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.exp(2)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=2.718281828459045, b=7.38905609893065) + """ + return Operation(_Op_code.exp, x, type_=float) + +def ln(x: Union[float, var]) -> Operation: + """Return the natural logarithm of x + + Returns + ------- + result: Operation + Operation which will calculate the natural logarithm. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.ln(7.38905609893065) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.ln(2.718281828459045)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=2.0, b=1.0) + """ + return Operation(_Op_code.ln, x, type_=float) + +def log(x: Union[float, var], base: float) -> Operation: + """Return the logarithm of x to the given base + + Returns + ------- + result: Operation + Operation which will calculate the logarithm. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.log(8, 2) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.log(27, 3)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=3.0, b=3.0) + """ + return Operation(_Op_code.log, base, x, type_=float) + +def log10(x: Union[float, var]) -> Operation: + """Return the base-10 logarithm of x + + Returns + ------- + result: Operation + Operation which will calculate the base-10 logarithm. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.log10(100) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.log10(1000)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=2.0, b=3.0) + """ + return Operation(_Op_code.log10, x, type_=float) + +def log2(x: Union[float, var]) -> Operation: + """Return the base-2 logarithm of x + + Returns + ------- + result: Operation + Operation which will calculate the base-2 logarithm. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.log2(8) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.log2(16)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=3.0, b=4.0) + """ + return Operation(_Op_code.log2, x, type_=float) + +def sqrt(x: Union[float, var]) -> Operation: + """Return the square root of x + + Returns + ------- + result: Operation + Operation which will calculate the square root. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.sqrt(4) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.sqrt(9)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=2.0, b=3.0) + """ + return Operation(_Op_code.sqrt, x, type_=float) + +def acos(x: Union[float, var]) -> Operation: + """Return the arc cosine of x + + Returns + ------- + result: Operation + Operation which will calculate the arc cosine. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.acos(1) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.acos(0)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=0.0, b=1.570796326794897) + """ + return Operation(_Op_code.acos, x, type_=float) + +# def acosh(x: Union[float, var]) -> Operation: +# """Return the inverse hyperbolic cosine of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the inverse hyperbolic cosine. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.acosh(1) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.acosh(2)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=0.0, b=1.316957896924817) +# """ +# return Operation(_Op_code.acosh, x, type_=float) + +def asin(x: Union[float, var]) -> Operation: + """Return the arc sine of x + + Returns + ------- + result: Operation + Operation which will calculate the arc sine. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.asin(0) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.asin(1)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=0.0, b=1.570796326794897) + """ + return Operation(_Op_code.asin, x, type_=float) + +# def asinh(x: Union[float, var]) -> Operation: +# """Return the inverse hyperbolic sine of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the inverse hyperbolic sine. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.asinh(0) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.asinh(1)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=0.0, b=0.881373587019543) +# """ +# return Operation(_Op_code.asinh, x, type_=float) + +def atan(x: Union[float, var]) -> Operation: + """Return the arc tangent of x + + Returns + ------- + result: Operation + Operation which will calculate the arc tangent. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.atan(0) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.atan(1)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=0.0, b=0.7853981633974483) + """ + return Operation(_Op_code.atan, x, type_=float) + +#def atanh(x: Union[float, var]) -> Operation: +# """Return the inverse hyperbolic tangent of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the inverse hyperbolic tangent. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.atanh(0) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.atanh(0.99)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=0.0, b=2.646652412362246) +# """ +# return Operation(_Op_code.atanh, x, type_=float) + +def cos(x: Union[float, var]) -> Operation: + """Return the cosine of x + + Returns + ------- + result: Operation + Operation which will calculate the cosine. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.cos(0) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.cos(3.141592653589793)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=1.0, b=-1.0) + """ + return Operation(_Op_code.cos, x, type_=float) + +# def cosh(x: Union[float, var]) -> Operation: +# """Return the hyperbolic cosine of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the hyperbolic cosine. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.cosh(0) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.cosh(1)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=1.0, b=1.543080634815244) +# """ +# return Operation(_Op_code.cosh, x, type_=float) + +def sin(x: Union[float, var]) -> Operation: + """Return the sine of x + + Returns + ------- + result: Operation + Operation which will calculate the sine. + + Examples + -------- + + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.sin(0) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.sin(3.141592653589793 / 2)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=0.0, b=1.0) + """ + return Operation(_Op_code.sin, x, type_=float) + +# def sinh(x: Union[float, var]) -> Operation: +# """Return the hyperbolic sine of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the hyperbolic sine. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.sinh(0) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.sinh(1)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=0.0, b=1.175201193643801) +# """ +# return Operation(_Op_code.sinh, x, type_=float) + +def tan(x: Union[float, var]) -> Operation: + """Return the tangent of x + + Returns + ------- + result: Operation + Operation which will calculate the tangent. + + Examples + -------- + >>> import zython as zn + >>> class MyModel(zn.Model): + ... def __init__(self): + ... self.a = zn.tan(0) + ... self.b = zn.var(float) + ... self.constraints = [self.b == zn.tan(0.7853981633974484)] + >>> model = MyModel() + >>> model.solve_satisfy() + Solution(a=0.0, b=1.0) + """ + return Operation(_Op_code.tan, x, type_=float) + +# def tanh(x: Union[float, var]) -> Operation: +# """Return the hyperbolic tangent of x +# +# Returns +# ------- +# result: Operation +# Operation which will calculate the hyperbolic tangent. +# +# Examples +# -------- +# +# >>> import zython as zn +# >>> class MyModel(zn.Model): +# ... def __init__(self): +# ... self.a = zn.tanh(0) +# ... self.b = zn.var(float) +# ... self.constraints = [self.b == zn.tanh(1)] +# >>> model = MyModel() +# >>> model.solve_satisfy() +# Solution(a=0.0, b=0.7615941559557649) +# """ +# return Operation(_Op_code.tanh, x, type_=float) def min(seq: ZnSequence, key: Union[Operation, Callable[[ZnSequence], Operation], None] = None) -> Operation: - """ Finds the smallest object in ``seq``, according to ``key`` + """Finds the smallest object in ``seq``, according to ``key`` Parameters ---------- @@ -353,11 +838,12 @@ def min(seq: ZnSequence, key: Union[Operation, Callable[[ZnSequence], Operation] Solution(m=-3) """ iter_var, operation = _iternal.get_iter_var_and_op(seq, key) - return operation_module._min(seq, iter_var, operation, type_=int) + type_ = derive_operation_type(seq, operation) + return Operation(_Op_code.min_, seq, iter_var, operation, type_=type_) def max(seq: ZnSequence, key: Union[Operation, Callable[[ZnSequence], Operation], None] = None) -> Operation: - """ Finds the biggest object in ``seq``, according to ``key`` + """Finds the biggest object in ``seq``, according to ``key`` Parameters ---------- @@ -391,11 +877,12 @@ def max(seq: ZnSequence, key: Union[Operation, Callable[[ZnSequence], Operation] Solution(m=3) """ iter_var, operation = _iternal.get_iter_var_and_op(seq, key) - return operation_module._max(seq, iter_var, operation, type_=int) + type_ = derive_operation_type(seq, operation) + return Operation(_Op_code.max_, seq, iter_var, operation, type_=type_) class alldifferent(Constraint): - """ requires all the variables appearing in its argument to be different + """Requires all the variables appearing in its argument to be different Parameters ---------- @@ -452,11 +939,12 @@ class alldifferent(Constraint): >>> Counter(result["a"]) == {3: 1, 2: 1, 1: 2} True """ + def __init__( - self, - seq: ZnSequence, - except0: Optional[bool] = None, - except_: Union[set, zython.var_par.collections.set.Set, None] = None, + self, + seq: ZnSequence, + except0: Optional[bool] = None, + except_: Union[set, zython.var_par.collections.set.Set, None] = None, ): if all((except0, except_)): raise ValueError("Arguments `except0` and `except_` can't be set at the same time") @@ -471,7 +959,7 @@ def __init__( class allequal(Constraint): - """ requires all the variables appearing in its argument to be equal + """Requires all the variables appearing in its argument to be equal Parameters ---------- @@ -495,12 +983,13 @@ class allequal(Constraint): >>> model.solve_satisfy() Solution(a=[[5, 5, 5, 5], [5, 5, 5, 5]]) """ + def __init__(self, seq: ZnSequence): super().__init__(_Op_code.allequal, seq) class ndistinct(Operation): - """ returns the number of distinct values in ``seq``. + """Returns the number of distinct values in ``seq``. Parameters ---------- @@ -530,12 +1019,13 @@ class ndistinct(Operation): >>> len(set(result["a"])) 3 """ + def __init__(self, seq: ZnSequence): super().__init__(_Op_code.ndistinct, seq) class circuit(Constraint): - """ Constrains the elements of ``seq`` to define a circuit where x[i] = j means that j is the successor of i. + """Constrains the elements of ``seq`` to define a circuit where x[i] = j means that j is the successor of i. Examples -------- @@ -549,12 +1039,13 @@ class circuit(Constraint): >>> model.solve_satisfy() Solution(a=[2, 4, 3, 1, 0]) """ + def __init__(self, seq: ZnSequence): super().__init__(_Op_code.circuit, seq) def increasing(seq: ZnSequence, *, allow_duplicate: Optional[bool] = True) -> Constraint: - """ Requires that the sequence `seq` is in increasing order + """Requires that the sequence `seq` is in increasing order Parameters ---------- @@ -588,7 +1079,7 @@ def increasing(seq: ZnSequence, *, allow_duplicate: Optional[bool] = True) -> Co def decreasing(seq: ZnSequence, *, allow_duplicate: Optional[bool] = True) -> Constraint: - """ Requires that the sequence `seq` is in decreasing order + """Requires that the sequence `seq` is in decreasing order Parameters ---------- diff --git a/zython/operations/operation.py b/zython/operations/operation.py index 7c51f82..907f38f 100644 --- a/zython/operations/operation.py +++ b/zython/operations/operation.py @@ -1,22 +1,9 @@ -import warnings from numbers import Number -from typing import Optional, Callable, Union, Type import zython from zython.operations._op_codes import _Op_code from zython.operations.constraint import Constraint -from zython.var_par.get_type import get_base_type - - -def _get_wider_type(left, right): - t_types = get_base_type(left), get_base_type(right) - types = set(t_types) - if types == {int}: - return int - elif types == {int, float} or types == {float}: - return float - warnings.warn("_get_wider_type returns int as fallback") - return int # TODO: fix types, do not forget about int/int => float +from zython.var_par.get_type import get_wider_type class Operation(Constraint): @@ -34,12 +21,12 @@ def __rmul__(self, other): # def __truediv__(self, other): # op = _Operation(_Op_code.truediv, self, other) - # op._type = _get_wider_type(self, other) + # op._type = get_wider_type(self, other) # return op # # def __rtruediv__(self, other): # op = _Operation(_Op_code.mul, other, self) - # op._type = _get_wider_type(self, other) + # op._type = get_wider_type(self, other) # return op def __floordiv__(self, other): @@ -92,71 +79,43 @@ def __ge__(self, other): def _add(left, right): - return Operation(_Op_code.add, left, right, type_=_get_wider_type(left, right)) + return Operation(_Op_code.add, left, right, type_=get_wider_type(left, right)) def _sub(left, right): - return Operation(_Op_code.sub, left, right, type_=_get_wider_type(left, right)) + return Operation(_Op_code.sub, left, right, type_=get_wider_type(left, right)) def _pow(base, power, modulo=None): if modulo is not None: raise ValueError("modulo is not supported") - return Operation(_Op_code.pow, base, power, type_=_get_wider_type(base, power)) + return Operation(_Op_code.pow, base, power, type_=get_wider_type(base, power)) def _mul(left, right): - return Operation(_Op_code.mul, left, right, type_=_get_wider_type(left, right)) + return Operation(_Op_code.mul, left, right, type_=get_wider_type(left, right)) def _floordiv(left, right): _validate_div(left, right) - return Operation(_Op_code.floordiv, left, right, type_=_get_wider_type(left, right)) + return Operation(_Op_code.floordiv, left, right, type_=get_wider_type(left, right)) def _mod(left, right): _validate_div(left, right) - return Operation(_Op_code.mod, left, right, type_=_get_wider_type(left, right)) + return Operation(_Op_code.mod, left, right, type_=get_wider_type(left, right)) def _size(array: "zython.var_par.collections.array.ArrayMixin", dim: int): if 0 <= dim < array.ndims(): return Operation(_Op_code.size, array, dim, type_=int) - raise ValueError(f"Array has 0..{array.ndims()} dimensions, but {dim} were specified") + raise ValueError(f"The array has 0..{array.ndims()} dimensions, but {dim} were specified") def _in(item: int, array: "zython.var_par.collections.abstract._AbstractCollection"): return Operation(_Op_code.in_, item, array, type_=bool) -def _sum(seq: "zython.var_par.types.ZnSequence", - iter_var: Optional["zython.var_par.var.var"] = None, - func: Optional[Union["Operation", Callable]] = None, - type_: Optional[Type] = None): - return Operation(_Op_code.sum_, seq, iter_var, func, type_=type_) - - -def _count(seq: "zython.var_par.types.ZnSequence", - iter_var: Optional["zython.var_par.var.var"] = None, - func: Optional[Union["Operation", Callable]] = None, - type_: Optional[Type] = None): - return Operation(_Op_code.count, seq, iter_var, func, type_=type_) - - -def _min(seq: "zython.var_par.types.ZnSequence", - iter_var: Optional["zython.var_par.var.var"] = None, - func: Optional[Union["Operation", Callable]] = None, - type_: Optional[Type] = None): - return Operation(_Op_code.min_, seq, iter_var, func, type_=type_) - - -def _max(seq: "zython.var_par.types.ZnSequence", - iter_var: Optional["zython.var_par.var.var"] = None, - func: Optional[Union["Operation", Callable]] = None, - type_: Optional[Type] = None): - return Operation(_Op_code.max_, seq, iter_var, func, type_=type_) - - def _validate_div(left, right): if isinstance(right, Number) and right == 0 or getattr(right, "value", 1) == 0: raise ValueError("right part of expression can't be 0") diff --git a/zython/result.py b/zython/result.py index 8066ead..2fab6ea 100644 --- a/zython/result.py +++ b/zython/result.py @@ -1,10 +1,13 @@ from collections import namedtuple +from functools import singledispatch +from typing import Any +import typing import minizinc class Result: - """ Represents model solution + """Represents model solution Warnings -------- @@ -12,25 +15,34 @@ class Result: but they can use this class as base class for their extensions. """ + def __init__(self, mzn_result: minizinc.Result): self._original = mzn_result if mzn_result.solution is not None: if isinstance(mzn_result.solution, list): if mzn_result.solution: # several solutions - names = [name for name in vars(mzn_result.solution[0]) if not name.startswith("_")] - Solution = namedtuple("Solution", names) + names, Solution = _generate_solution_class_and_field_names( + mzn_result.solution[0] + ) solutions = [] for i in range(len(mzn_result.solution)): - solutions.append(Solution(*(mzn_result[i, name] for name in names))) + solutions.append( + Solution( + *convert_result_value( + (mzn_result[i, name]) for name in names + ) + ) + ) self._solution = solutions else: # no solutions while all_solutions=True self._solution = None else: - names = [name for name in vars(mzn_result.solution) if not name.startswith("_")] - Solution = namedtuple("Solution", names) - self._solution = Solution(*(mzn_result[name] for name in names)) + names, Solution = _generate_solution_class_and_field_names(mzn_result.solution) + self._solution = Solution( + *(convert_result_value(mzn_result[name]) for name in names) + ) else: self._solution = None @@ -57,5 +69,23 @@ def __len__(self): def as_original(mzn_result: minizinc.Result): - """ returns original result, returned by minizinc-python """ + """Returns original result, returned by minizinc-python""" return mzn_result + + +@singledispatch +def convert_result_value(value: Any) -> Any: + return value + + +@convert_result_value.register +def _(value: float) -> float: + return -value if value == -0.0 else value + + +def _generate_solution_class_and_field_names( + mzn_solution, +) -> typing.Tuple[typing.Tuple[str, ...], typing.NamedTuple]: + names = [name for name in vars(mzn_solution) if not name.startswith("_")] + Solution = namedtuple("Solution", names) + return names, Solution diff --git a/zython/var_par/collections/array.py b/zython/var_par/collections/array.py index e394e34..1db6b02 100644 --- a/zython/var_par/collections/array.py +++ b/zython/var_par/collections/array.py @@ -1,5 +1,4 @@ import inspect -import itertools from collections import deque from zython import var, par @@ -40,19 +39,19 @@ def size(self, dim=0): class ArrayView(ArrayMixin): def __init__(self, array, pos): self.array: ArrayMixin = array - # pos is a tuple with the same size as number of array dimensions, the user can specify less iterators, - # slice(None, None, 1) will be added to fit the number of dimensions, so compilers shouldn't worry about it + # pos is a tuple with the same size as number of array dimensions self.pos = self._get_pos(pos) self._type = array.type def _get_pos(self, pos): if not isinstance(pos, tuple): - pos = [pos] + pos = (pos,) + if len(pos) != self.array.ndims(): + raise ValueError( + f"The array has {self.array.ndims()} dimensions, but {len(pos)} indexes were specified" + ) self._check_for_index_error(pos) - repeat = itertools.repeat(slice(None, None, 1), self.array.ndims() - len(pos)) - pos = tuple(self._process_pos_item(dim, p) for dim, p in enumerate(itertools.chain(pos, repeat))) - if len(pos) > self.array.ndims(): - raise ValueError(f"Array has {self.array.ndims()} dimensions but {len(pos)} were specified") + pos = tuple(self._process_pos_item(dim, p) for dim, p in enumerate(pos)) return pos def _check_for_index_error(self, pos): diff --git a/zython/var_par/collections/set.py b/zython/var_par/collections/set.py index 6892b05..464793f 100644 --- a/zython/var_par/collections/set.py +++ b/zython/var_par/collections/set.py @@ -9,7 +9,7 @@ class SetMixin(_AbstractCollection): @staticmethod def _validate_type(type_): - if type_ != int and not is_range(type_) and not is_enum(type_): + if type_ is not int and not is_range(type_) and not is_enum(type_): raise ValueError(f"Unsupported type for set: {type(type_)}") diff --git a/zython/var_par/get_type.py b/zython/var_par/get_type.py index 4d6738d..9588ea5 100644 --- a/zython/var_par/get_type.py +++ b/zython/var_par/get_type.py @@ -1,5 +1,6 @@ import enum from functools import singledispatch +import warnings from zython.var_par.types import Ranges, RangesType, _range @@ -55,3 +56,24 @@ def _(arg: _range): if is_int_range(arg): return int return float + + +def get_wider_type(left, right): + t_types = get_base_type(left), get_base_type(right) + types = set(t_types) + if types == {int}: + return int + elif types == {int, float} or types == {float}: + return float + warnings.warn("_get_wider_type returns int as fallback") + return int # TODO: fix types, do not forget about int/int => float + +def derive_operation_type(seq, operation): + from zython.var_par.collections.array import ArrayMixin + if isinstance(seq, ArrayMixin) and operation is None: + type_ = seq.type + else: + type_ = operation.type + if type_ is None: + raise ValueError(f"Can't derive the type of {seq=} {operation=} expression") + return type_