├── .github
├── dependabot.yml
└── workflows
│ └── python-package.yml
├── .gitignore
├── .python-version
├── .readthedocs.yml
├── CHANGELOG.rst
├── CITATION.cff
├── LICENSE
├── README.md
├── docs
├── _static
│ └── minizinc.svg
├── advanced_usage.rst
├── api.rst
├── basic_usage.rst
├── changelog.rst
├── conf.py
├── getting_started.rst
└── index.rst
├── pyproject.toml
├── src
└── minizinc
│ ├── __init__.py
│ ├── analyse.py
│ ├── driver.py
│ ├── dzn.py
│ ├── error.py
│ ├── helpers.py
│ ├── instance.py
│ ├── json.py
│ ├── model.py
│ ├── pygments.py
│ ├── result.py
│ ├── solver.py
│ └── types.py
├── tests
├── support.py
├── test_dzn.py
├── test_errors.py
├── test_helpers.py
├── test_init.py
├── test_instance.py
├── test_solver.py
├── test_solving.py
└── test_types.py
└── uv.lock
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | groups:
13 | production:
14 | dependency-type: "production"
15 | development:
16 | dependency-type: "development"
17 | - package-ecosystem: "github-actions"
18 | directory: "/"
19 | schedule:
20 | interval: "weekly"
21 |
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | name: Python package
2 |
3 | on:
4 | push:
5 | branches: [develop]
6 | pull_request:
7 | branches: [develop]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | python-version: ["3.8", "3.11", "pypy3.10"]
15 | minizinc-version: ["2.8.7", "2.6.0"]
16 |
17 | env:
18 | MINIZINC_URL: https://github.com/MiniZinc/MiniZincIDE/releases/download/${{ matrix.minizinc-version }}/MiniZincIDE-${{ matrix.minizinc-version }}-x86_64.AppImage
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 | - name: Add bin/ to PATH
24 | run: |
25 | mkdir -p ${{ github.workspace }}/bin
26 | echo "${{ github.workspace }}/bin" >> $GITHUB_PATH
27 | - name: Install libfuse (AppImage dependency)
28 | run: |
29 | sudo apt-get update
30 | sudo apt-get install libfuse2 libegl1
31 | - name: Cache MiniZinc
32 | id: cache-minizinc
33 | uses: actions/cache@v4
34 | with:
35 | path: ${{ github.workspace }}/bin/minizinc
36 | key: ${{ env.MINIZINC_URL }}
37 | - name: Download MiniZinc
38 | if: steps.cache-minizinc.outputs.cache-hit != 'true'
39 | run: |
40 | sudo curl -o ${{ github.workspace }}/bin/minizinc -L $MINIZINC_URL
41 | sudo chmod +x ${{ github.workspace }}/bin/minizinc
42 | minizinc --version
43 | - name: Install uv
44 | uses: astral-sh/setup-uv@v6
45 | - name: Set up Python ${{ matrix.python-version }}
46 | uses: actions/setup-python@v5
47 | with:
48 | python-version: ${{ matrix.python-version }}
49 | - name: Install the project
50 | run: uv sync --dev
51 | - name: Run Pytest
52 | run: uv run pytest
53 | - name: Install numpy
54 | run: uv pip install numpy
55 | - name: Run Pytest with numpy
56 | run: uv run pytest
57 |
58 | lints:
59 | runs-on: ubuntu-latest
60 | steps:
61 | - uses: actions/checkout@v4
62 | - name: Install uv
63 | uses: astral-sh/setup-uv@v6
64 | - name: Set up Python
65 | uses: actions/setup-python@v5
66 | with:
67 | python-version-file: ".python-version"
68 | - name: Install the project
69 | run: uv sync --dev
70 | - name: Check Ruff linter
71 | run: uv run ruff check
72 | - name: Check Ruff formatter
73 | run: uv run ruff format --check
74 | - name: Check MyPy type checker
75 | run: uv run mypy .
76 |
77 | docs:
78 | runs-on: ubuntu-latest
79 | steps:
80 | - uses: actions/checkout@v4
81 | - name: Install uv
82 | uses: astral-sh/setup-uv@v6
83 | - name: Set up Python ${{ matrix.python-version }}
84 | uses: actions/setup-python@v5
85 | with:
86 | python-version-file: ".python-version"
87 | - name: Install the project
88 | run: uv sync --extra docs
89 | - name: Generate the documentation
90 | run: uv run sphinx-build -b html docs dist/docs
91 | - name: Check the documentation doesn't have broken links
92 | run: uv run sphinx-build -b linkcheck docs dist/docs
93 | - uses: actions/upload-artifact@v4
94 | with:
95 | name: documentation
96 | path: dist/docs/
97 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 | .pytest_cache/
51 |
52 | # Translations
53 | *.mo
54 | *.pot
55 |
56 | # Django stuff:
57 | *.log
58 | local_settings.py
59 | db.sqlite3
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # IPython
78 | profile_default/
79 | ipython_config.py
80 |
81 | # pyenv
82 | #.python-version
83 |
84 | # celery beat schedule file
85 | celerybeat-schedule
86 |
87 | # SageMath parsed files
88 | *.sage.py
89 |
90 | # Environments
91 | .env
92 | .venv
93 | env/
94 | venv/
95 | ENV/
96 | env.bak/
97 | venv.bak/
98 |
99 | # Spyder project settings
100 | .spyderproject
101 | .spyproject
102 |
103 | # Rope project settings
104 | .ropeproject
105 |
106 | # mkdocs documentation
107 | /site
108 |
109 | # mypy
110 | .mypy_cache/
111 | .dmypy.json
112 | dmypy.json
113 |
114 | # Pyre type checker
115 | .pyre/
116 |
117 | result/
118 |
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.13
2 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 | # You can also specify other tool versions:
13 | # nodejs: "20"
14 | # rust: "1.70"
15 | # golang: "1.20"
16 |
17 | # Build documentation in the "docs/" directory with Sphinx
18 | sphinx:
19 | configuration: docs/conf.py
20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
21 | # builder: "dirhtml"
22 | # Fail on all warnings to avoid broken references
23 | # fail_on_warning: true
24 |
25 | # Optionally build your docs in additional formats such as PDF and ePub
26 | # formats:
27 | # - pdf
28 | # - epub
29 |
30 | # Optional but recommended, declare the Python requirements required
31 | # to build your documentation
32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
33 | python:
34 | install:
35 | - method: pip
36 | path: .
37 | extra_requirements:
38 | - docs
39 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 | =========
3 |
4 | All notable changes to this project will be documented in this file.
5 |
6 | The format is based on `Keep a Changelog `_ and
7 | this project adheres to `Semantic Versioning `_.
8 |
9 | Unreleased_
10 | ------------
11 |
12 | 0.10.0_ - 2025-02-25
13 | -------------------
14 |
15 | Added
16 | ^^^^^
17 |
18 | - Add the ``Instance.diverse_solutions`` method to use the MiniZinc
19 | experimental feature to model diversity and try and find a set of diverse
20 | solutions for the given problem.
21 |
22 | Removed
23 | ^^^^^^^
24 |
25 | - **BREAKING:** The project no longer supports Python 3.7. This change will make
26 | it easier to manage MiniZinc Python's build system.
27 |
28 | Changed
29 | ^^^^^^^
30 |
31 | - Minimum supported version of MiniZinc has increased from 2.5.4 to 2.6.0.
32 | - ``Instance.solutions`` is now able to use the ``intermediate_solutions`` flag
33 | correctly and will only yield a single solution when it is set to ``False``.
34 | - ``helpers.check_solution`` now includes an optional time limit flag. If the
35 | time limit is exceeded, then a ``TimeoutError`` exception will be raised.
36 | - ``helpers.check_solution`` will no longer capture MiniZinc exceptions.
37 | Capturing these exceptions sometimes hid problems.
38 | - The ``timeout`` parameter has been renamed to ``time_limit`` in
39 | ``Instance.solve``, ``Instance.solve_async``, and ``Instance.solutions``. The
40 | ``timeout`` parameter is still accepted, but will add a
41 | ``DeprecationWarning`` and will be removed in future versions.
42 | - The ``intermediate_solutions`` parameter can now be explicitly set to
43 | ``False`` to avoid the ``-i`` flag to be passed to MiniZinc, which is
44 | generally added to ensure that a final solution is available.
45 |
46 | Fixed
47 | ^^^^^
48 |
49 | - Fix problem where some exceptions when creating processes where hidden and
50 | would then cause errors where the ``proc`` variable did not exist.
51 | - Fix issue where MiniZinc would not correctly be terminated on Windows when
52 | the Python process was interrupted.
53 |
54 | 0.9.0_ - 2023-04-04
55 | -------------------
56 |
57 | Added
58 | ^^^^^
59 |
60 | - Add support for MiniZinc tuple and record types.
61 |
62 | Changed
63 | ^^^^^^^
64 |
65 | - Minimum supported version of MiniZinc has increased from 2.5.0 to 2.5.4.
66 |
67 | Fixed
68 | ^^^^^
69 |
70 | - Ensure time events send using the JSON stream are parsed as ``timedelta``
71 | objects.
72 | - Pass JSON definitions in using JSON instead of generated DZN files..
73 |
74 | 0.8.0_ - 2023-02-06
75 | -------------------
76 |
77 | Removed
78 | ^^^^^^^
79 |
80 | - **BREAKING:** The project no longer supports Python 3.6. This change will make
81 | it easier to manage MiniZinc Python's build system.
82 |
83 | Fixed
84 | ^^^^^
85 |
86 | - Fix the conversion when using a Python enumerated type in MiniZinc that uses
87 | non-ascii identifiers.
88 |
89 | 0.7.0_ - 2022-07-13
90 | -------------------
91 |
92 | Added
93 | ^^^^^
94 |
95 | - Add additional ``Driver.executable`` property to safely access the location
96 | of the executable used by the ``Driver`` object.
97 | - Support for input and output of complex MiniZinc enumerated types, such as
98 | anonymous enumerated types or enumerated types using constructor functions.
99 |
100 | Removed
101 | ^^^^^^^
102 |
103 | - **BREAKING:** The project no longer contains the (uncompleted) direct library
104 | connection to libminizinc. With this change come some simplications in methods of
105 | relocated from ``CLIDriver`` and ``CLIInstance``, and the move of ``find_driver`` to
106 | ``Driver.find``.
107 |
108 | Fixed
109 | ^^^^^
110 |
111 | - Store statistics as a string when incorrectly reported by the solver.
112 | - Fix the conversion of statistics when using MiniZinc 2.6+
113 |
114 | 0.6.0_ - 2022-03-02
115 | -------------------
116 |
117 | Added
118 | ^^^^^
119 |
120 | - Add support for the usage of JSON Stream output when using MiniZinc 2.6+
121 |
122 | Fixed
123 | ^^^^^
124 |
125 | - Do not raise error about unsupported solver flags when MiniZinc driver would
126 | not raise an error.
127 | - Fix warnings caused by unterminated coroutines when using the asynchronous
128 | iterators
129 |
130 |
131 | 0.5.0_ - 2021-10-07
132 | -------------------
133 |
134 | Added
135 | ^^^^^
136 |
137 | - Add (generated) ``__version__`` field to the package to abide by the PEP
138 | recommendations.
139 | - Add support for using NumPy types to instantiate Models. (``np.array`` and
140 | any type that falls under ``np.generic``).
141 | - Add ``available_solvers`` method to the ``Driver`` objects to explicitly
142 | report the available solvers according to the current environment.
143 | - Add support for Python 3.10.
144 |
145 | Changed
146 | ^^^^^^^
147 |
148 | - **BREAKING:** Update minimal supported MiniZinc version to 2.5.0 to ensure
149 | full functionality.
150 | - Remove the (additional) hard time-out in from the CLI driver. MiniZinc should
151 | correctly enforce set time limit.
152 | - ``Solver.lookup`` now has an extra ``refresh`` argument to signal whether
153 | the driver should refresh the found solver configurations.
154 |
155 | Fixed
156 | ^^^^^
157 |
158 | - Always close temporary files before removing them, so that if an exception is
159 | raised while a file is still open, it gets removed correctly on Windows.
160 | - Set the required process creation flags on Windows to allow MiniZinc to
161 | terminate its subprocesses correctly.
162 | - Pass ``--intermediate-solutions`` flag when ``-i`` or ``-a`` is supported by
163 | the solver.
164 | - Pygments parser generated by Iro did not contain the correct ``#pop`` rules.
165 | The parser was manually edited to work correctly, but we can no longer
166 | generate is automatically unless the bug in upstream Iro is resolved.
167 | - Resolve a syntax error (a missing semicolon) in the meta-heuristics example in
168 | the documentation.
169 | - Correctly pass the ``-O0`` flag to the CLI when ``optimisation_level`` is set
170 | to zero.
171 | - Set type of MiniZinc annotation output type `ann` to `str` in Python in
172 | accordance with the JSON output format.
173 |
174 | 0.4.2_ - 2020-11-25
175 | -------------------
176 |
177 | Fixed
178 | ^^^^^
179 |
180 | - Terminate the MiniZinc process when stopping early (instead of killing it).
181 | This allows MiniZinc to correctly stop any solver processes.
182 |
183 | Changed
184 | ^^^^^^^
185 |
186 | - Revert change from 0.4.1 where enumerated types unknown to Python would be
187 | made stored as anonymous enumerations. Interoperability between the MiniZinc
188 | driver and the MiniZinc Python has instead changed to allow JSON strings as
189 | valid input for enumerated types. (required MiniZinc 2.5.3)
190 |
191 | 0.4.1_ - 2020-11-11
192 | -------------------
193 |
194 | Added
195 | ^^^^^
196 | - Support for Python 3.9. (MiniZinc Python will aim to support all versions of
197 | Python that are not deprecated)
198 | - Experimental support for capturing the error output of the MiniZinc process
199 | in ``CLIInstance``.
200 | - Experimental support for verbose compiler and solver output (using the ``-v``
201 | flag) in ``CLIInstance``.
202 |
203 | Changed
204 | ^^^^^^^
205 | - The MiniZinc Python repository moved from GitLab to GitHub, replacing GitLab
206 | CI for GitHub Actions for the continuous testing.
207 | - Values of an enumerated type defined in MiniZinc will now appear in solutions
208 | as a member of a singular anonymous ``enum.Enum`` class.
209 |
210 | Fixed
211 | ^^^^^
212 | - Handle the cancellation of asynchronous solving and correctly dispose of the
213 | process
214 | - Correct the JSON representation of sets of with ``IntEnum`` members. (Lists
215 | are still not correctly represented).
216 | - ``check_solution`` will now correctly handle solution values of an enumerated
217 | type defined in MiniZinc.
218 |
219 | 0.4.0_ - 2020-10-06
220 | -------------------
221 |
222 | Changed
223 | ^^^^^^^
224 | - The ``check_solution`` has been split into two separate functions. The
225 | ``check_result`` function allows the user to check the correctness of a
226 | ``Result`` object and the new ``check_solution`` function can check the
227 | correctness of an individual solution in the form of a data class object or a
228 | dictionary.
229 | - ``Model.add_file`` no longer has its ``parse_data`` flag enabled by default.
230 |
231 | Fixed
232 | ^^^^^
233 | - Catch lark ``ImportError`` before ``LarkError`` during ``Model.add_file()`` since
234 | ``LarkError`` will not exist if the import failed.
235 | - Ensure a DZN file does not get included if its data is parsed.
236 |
237 | 0.3.3_ - 2020-08-17
238 | -------------------
239 |
240 | Added
241 | ^^^^^
242 | - Add ``requiredFlags`` field to the ``Solver`` data class.
243 |
244 | Fixed
245 | ^^^^^
246 | - Ignore extra (undocumented) fields from MiniZinc's ``--solvers-json`` output
247 | when initialising ``Solver`` objects.
248 |
249 | 0.3.2_ - 2020-08-14
250 | -------------------
251 |
252 | Fixed
253 | ^^^^^
254 | - Add full support for string input and output. The usage of strings would
255 | previously incorrectly give a warning.
256 |
257 | 0.3.1_ - 2020-07-21
258 | -------------------
259 |
260 | Changed
261 | ^^^^^^^
262 | - Store path of loaded solver configuration paths so that no configuration file
263 | has to be generated if no changes are made to the solver.
264 |
265 | Fixed
266 | ^^^^^
267 | - Ensure generated solver configurations exists during the full existence of
268 | the created asynchronous process.
269 |
270 |
271 | 0.3.0_ - 2020-07-21
272 | -------------------
273 |
274 | Added
275 | ^^^^^
276 | - Add support for different MiniZinc compiler optimisation levels. All methods that
277 | compile an instance now have an additional `optimisation_level` argument.
278 |
279 | Changed
280 | ^^^^^^^
281 | - The DZN parser functionality has been moved into the ``dzn`` extra. If your
282 | application requires parsed ``dzn`` information, then you have to ensure your
283 | MiniZinc Python is installed with this extra enabled:
284 | ``pip install minizinc[dzn]``.
285 | - ``Solver`` has been turned into a ``dataclass`` and has been updated with all
286 | attributes used in the compiler.
287 |
288 | Fixed
289 | ^^^^^
290 | - Resolve relative paths when directly loading a solver configuration. This
291 | ensures that when a temporary solver configuration is created, the paths are
292 | correct.
293 |
294 | 0.2.3_ - 2020-03-31
295 | -------------------
296 |
297 | Changed
298 | ^^^^^^^
299 | - Add text to the empty MiniZincError that occurs when MiniZinc exits with a non-zero
300 | exit status
301 |
302 | Fixed
303 | ^^^^^
304 | - Close generated solver configuration before handing it to MiniZinc. This fixes the
305 | usage of generated solver configurations on Windows.
306 | - The DZN parser now constructs correct range objects. The parser was off by one due to
307 | the exclusive upper bound in Python range objects.
308 | - Rewrite MiniZinc fields that are keywords in Python. These names cannot be used
309 | directly as members of a dataclass. Python keywords used in MiniZinc are rewritten to
310 | ``"mzn_" + {keyword}`` and a warning is thrown.
311 |
312 | 0.2.2_ - 2020-02-17
313 | -------------------
314 |
315 | Added
316 | ^^^^^
317 | - Add output property to ``CLIInstance`` to expose the output interface given by
318 | MiniZinc.
319 |
320 | Changed
321 | ^^^^^^^
322 | - Improved interaction with solution checker models. Solution checkers can
323 | now be added to an ```Instance``/``Model`` and an ``check`` method will be
324 | added to the generated solution objects.
325 | - Change the Python packaging system back to setuptools due to the excessive
326 | required dependencies of Poetry.
327 |
328 | Fixed
329 | ^^^^^
330 | - Fix the MiniZinc output parsing of sets of an enumerated type.
331 | - Fix the TypeError that occurred when a hard timeout occurred.
332 | - Allow trailing commas for sets and arrays in DZN files.
333 |
334 | 0.2.1_ - 2020-01-13
335 | -------------------
336 |
337 | Added
338 | ^^^^^
339 | - Add support for other command line flags for ``CLIInstance.flatten()``
340 | through the use of ``**kwargs``.
341 | - Add initial ``Checker`` class to allow the usage of MiniZinc solution
342 | checkers.
343 |
344 | Changed
345 | ^^^^^^^
346 | - The string method for ``Result`` will now refer to the string method of its
347 | ``Solution`` attribute.
348 |
349 | Fixed
350 | ^^^^^
351 | - Ensure the event loop selection on Windows to always selects
352 | ``ProactorEventLoop``. This ensures the usage on Windows when the python
353 | version ``<= 3.8.0``.
354 |
355 | 0.2.0_ - 2019-12-13
356 | -------------------
357 |
358 | Added
359 | ^^^^^
360 | - Support and testing for Python 3.8
361 | - Logging of started processes and attributes of generated output items
362 | - Export `Pygments `_ Lexer for MiniZinc
363 |
364 | Changed
365 | ^^^^^^^
366 | - ``Driver.check_version`` now raises an ``ConfigurationError`` exception
367 | when an incompatible function is detected; otherwise, the method not return a
368 | value.
369 | - Output classes generated by ``CLIIinstance.analyse()`` no longer contain
370 | the `_output_item` `str` attribute when MiniZinc does not find a output item.
371 | (New in MiniZinc 2.3.3)
372 | - Improved parsing of non-standard (numerical) statistical information
373 | provided by the solver.
374 |
375 | Fixed
376 | ^^^^^
377 | - ``CLIInstance.solutions()``: The separator detection is now OS independent.
378 | The separator previously included a ``\n`` literal instead of ``\r\n`` on
379 | Windows.
380 | - Solve an issue in ``CLIInstance.solution()`` where a solution with a size
381 | bigger than the buffer size would result in a ``LimitOverrunError`` exception.
382 | - Correctly catch the ``asyncio.TimeoutError`` and kill the process when
383 | reaching a hard timeout. (i.e., the solver and ``minizinc`` do not stop in
384 | time)
385 | - Check if file exists before opening file when an error occurs. (File might
386 | have been part of a compiled solver)
387 | - Ensure the ``objective`` attribute is only added to the generated solution
388 | type once
389 | - Remove '\r' characters from input when parsing statistics (Windows Specific).
390 |
391 |
392 | 0.1.0_ - 2019-10-11
393 | ---------------------
394 |
395 | Initial release of MiniZinc Python. This release contains an initial
396 | functionality to use MiniZinc directly from Python using an interface to the
397 | ``minizinc`` command line application. The exact functionality available in this
398 | release is best described in the `documentation
399 | `_.
400 |
401 |
402 | .. _0.10.0: https://github.com/MiniZinc/minizinc-python/compare/0.9.0...0.10.0
403 | .. _0.9.0: https://github.com/MiniZinc/minizinc-python/compare/0.8.0...0.9.0
404 | .. _0.8.0: https://github.com/MiniZinc/minizinc-python/compare/0.7.0...0.8.0
405 | .. _0.7.0: https://github.com/MiniZinc/minizinc-python/compare/0.6.0...0.7.0
406 | .. _0.6.0: https://github.com/MiniZinc/minizinc-python/compare/0.5.0...0.6.0
407 | .. _0.5.0: https://github.com/MiniZinc/minizinc-python/compare/0.4.2...0.5.0
408 | .. _0.4.2: https://github.com/MiniZinc/minizinc-python/compare/0.4.1...0.4.2
409 | .. _0.4.1: https://github.com/MiniZinc/minizinc-python/compare/0.4.0...0.4.1
410 | .. _0.4.0: https://github.com/MiniZinc/minizinc-python/compare/0.3.3...0.4.0
411 | .. _0.3.3: https://github.com/MiniZinc/minizinc-python/compare/0.3.2...0.3.3
412 | .. _0.3.2: https://github.com/MiniZinc/minizinc-python/compare/0.3.1...0.3.2
413 | .. _0.3.1: https://github.com/MiniZinc/minizinc-python/compare/0.3.0...0.3.1
414 | .. _0.3.0: https://github.com/MiniZinc/minizinc-python/compare/0.2.3...0.3.0
415 | .. _0.2.3: https://github.com/MiniZinc/minizinc-python/compare/0.2.2...0.2.3
416 | .. _0.2.2: https://github.com/MiniZinc/minizinc-python/compare/0.2.1...0.2.2
417 | .. _0.2.1: https://github.com/MiniZinc/minizinc-python/compare/0.2.0...0.2.1
418 | .. _0.2.0: https://github.com/MiniZinc/minizinc-python/compare/0.1.0...0.2.0
419 | .. _0.1.0: https://github.com/MiniZinc/minizinc-python/compare/d14654d65eb747470e11c10747e6dd49baaab0b4...0.1.0
420 | .. _Unreleased: https://github.com/MiniZinc/minizinc-python/compare/stable...develop
421 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | # This file contains metadata to help users cite MiniZinc in academic
2 | # publications. To add more details, see https://citation-file-format.github.io
3 | # for help.
4 |
5 | cff-version: 1.2.0
6 | title: MiniZinc Python
7 | message: >-
8 | If you use this software, please cite it using the
9 | metadata from this file.
10 | type: software
11 | authors:
12 | - given-names: Jip J.
13 | family-names: Dekker
14 | email: jip.dekker@monash.edu
15 | affiliation: Monash University
16 | orcid: 'https://orcid.org/0000-0002-0053-6724'
17 | identifiers:
18 | - type: doi
19 | value: 10.5281/zenodo.7608747
20 | description: Zenodo Software Archival DOI
21 | repository-code: 'https://github.com/MiniZinc/minizinc-python'
22 | url: 'https://www.minizinc.org/'
23 | abstract: >-
24 | MiniZinc Python provides an interface from Python to the
25 | MiniZinc driver. The most important goal of this project
26 | are to allow easy access to MiniZinc using native Python
27 | structures. This will allow you to more easily make
28 | scripts to run MiniZinc, but will also allow the
29 | integration of MiniZinc models within bigger (Python)
30 | projects. This module also aims to expose an interface for
31 | meta-search. For problems that are hard to solve,
32 | meta-search can provide solutions to reach more or better
33 | solutions quickly.
34 | keywords:
35 | - MiniZinc
36 | - Python
37 | - Constraint Programming
38 | - Optimisation
39 | license: MPL-2.0
40 | version: 0.10.0
41 | date-released: '2025-02-25'
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | The python package that allows you to access all of MiniZinc's functionalities directly from Python.
12 |
13 | Explore the docs »
14 |
15 |
16 | Report Bug
17 | ·
18 | Request Feature
19 |
20 |
21 |
22 |
23 |
24 | ## Table of Contents
25 |
26 | * [About the Project](#about-the-project)
27 | * [Getting Started](#getting-started)
28 | * [Installation](#installation)
29 | * [Usage](#usage)
30 | * [Testing](#testing)
31 | * [Roadmap](#roadmap)
32 | * [Contributing](#contributing)
33 | * [License](#license)
34 | * [Contact](#contact)
35 |
36 |
37 |
38 |
39 | ## About The Project
40 |
41 | _MiniZinc Python_ provides an interface from Python to the MiniZinc driver. The
42 | most important goal of this project are to allow easy access to MiniZinc using
43 | native Python structures. This will allow you to more easily make scripts to run
44 | MiniZinc, but will also allow the integration of MiniZinc models within bigger
45 | (Python) projects. This module also aims to expose an interface for meta-search.
46 | For problems that are hard to solve, meta-search can provide solutions to reach
47 | more or better solutions quickly.
48 |
49 |
50 |
51 | ## Getting Started
52 |
53 | To get a MiniZinc Python up and running follow these simple steps.
54 |
55 | ### Installation
56 |
57 | _MiniZinc Python_ can be installed by running `pip install minizinc`. It
58 | requires [MiniZinc](https://www.minizinc.org/) 2.6+ and
59 | [Python](https://www.python.org/) 3.8+ to be installed on the system. MiniZinc
60 | python expects the `minizinc` executable to be available on the executable path,
61 | the `$PATH` environmental variable, or in a default installation location.
62 |
63 | _For more information, please refer to the
64 | [Documentation](https://python.minizinc.dev/en/latest/)_
65 |
66 |
67 | ### Usage
68 |
69 | Once all prerequisites and MiniZinc Python are installed, a `minizinc` module
70 | will be available in Python. The following Python code shows how to run a
71 | typical MiniZinc model.
72 |
73 | ```python
74 | import minizinc
75 |
76 | # Create a MiniZinc model
77 | model = minizinc.Model()
78 | model.add_string("""
79 | var -100..100: x;
80 | int: a; int: b; int: c;
81 | constraint a*(x*x) + b*x = c;
82 | solve satisfy;
83 | """)
84 |
85 | # Transform Model into a instance
86 | gecode = minizinc.Solver.lookup("gecode")
87 | inst = minizinc.Instance(gecode, model)
88 | inst["a"] = 1
89 | inst["b"] = 4
90 | inst["c"] = 0
91 |
92 | # Solve the instance
93 | result = inst.solve(all_solutions=True)
94 | for i in range(len(result)):
95 | print("x = {}".format(result[i, "x"]))
96 | ```
97 |
98 | _For more examples, please refer to the
99 | [Documentation](https://python.minizinc.dev/en/latest/)_
100 |
101 |
102 | ## Testing
103 |
104 | MiniZinc Python uses [uv](https://docs.astral.sh/uv/) to manage its
105 | dependencies. To install the development dependencies run `uv sync --dev`.
106 |
107 | Although continuous integration will test any code, it can be convenient to run
108 | the tests locally. The following commands can be used to test the MiniZinc
109 | Python package.
110 |
111 | - We use [PyTest](https://docs.pytest.org/en/stable/) to run a suite of unit
112 | tests. You can run these tests by executing:
113 | ```bash
114 | uv run pytest
115 | ```
116 | - We use [Ruff](https://docs.astral.sh/ruff/) to test against a range of Python
117 | style and performance guidelines. You can run the general linting using:
118 | ```bash
119 | uv run ruff check
120 | ```
121 | You can format the codebase to be compatible using:
122 | ```bash
123 | uv run ruff format
124 | ```
125 | (The continous integration will test that the code is correctly formatted using
126 | the `--check` flag.)
127 | - We use [Mypy](https://mypy.readthedocs.io/en/stable/) to check the type
128 | correctness of the codebase (for as far as possible). You can run the type
129 | checking using:
130 | ```bash
131 | uv run mypy .
132 | ```
133 |
134 |
135 | ## Roadmap
136 |
137 | See the [open issues](https://github.com/MiniZinc/minizinc-python/issues) for a
138 | list of proposed features (and known issues).
139 |
140 |
141 |
142 | ## Contributing
143 |
144 | Contributions are what make the open source community such an amazing place to
145 | be learn, inspire, and create. Any contributions you make are **greatly
146 | appreciated**.
147 |
148 | 1. Fork the Project
149 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
150 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
151 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
152 | 5. Open a Pull Request
153 |
154 |
155 |
156 | ## License
157 |
158 | Distributed under the Mozilla Public License Version 2.0. See `LICENSE` for more information.
159 |
160 |
161 |
162 | ## Contact
163 | 👤 **Jip J. Dekker**
164 | * Twitter: [@DekkerOne](https://twitter.com/DekkerOne)
165 | * Github: [Dekker1](https://github.com/Dekker1)
166 |
167 | 🏛 **MiniZinc**
168 | * Website: [https://www.minizinc.org/](https://www.minizinc.org/)
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/docs/_static/minizinc.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/advanced_usage.rst:
--------------------------------------------------------------------------------
1 | Advanced Usage
2 | ==============
3 |
4 | This page provides examples of usages of MiniZinc Python other than solving just
5 | a basic model instance.
6 |
7 |
8 | Custom Output Type
9 | ------------------
10 |
11 | You can change the type in which MiniZinc Python will provide its solutions. By
12 | default the output type will automatically be generated for every mode, but it
13 | can be changed by setting the `output_type` attribute of a model or instance.
14 | This can be useful if you require the data in a particular format for later use.
15 | The following example solves a task assignment problem and its result will store
16 | the solutions in a class with an additional method to check that the tasks in
17 | the solution are scheduled uniquely.
18 |
19 |
20 | .. code-block:: python
21 |
22 | from dataclasses import InitVar, dataclass
23 | from typing import List
24 |
25 | from minizinc import Instance, Model, Solver
26 |
27 |
28 | @dataclass
29 | class TaskAssignment:
30 | task: List[int]
31 | objective: int
32 | __output_item: InitVar[str] = None
33 |
34 | def check(self) -> bool:
35 | return len(set(self.task)) == len(self.task)
36 |
37 |
38 | gecode = Solver.lookup("gecode")
39 | model = Model()
40 | model.add_string(
41 | """
42 | int: n;
43 | set of int: DOM = 1..n;
44 | int: m;
45 | set of int: COD = 1..m;
46 | array[DOM,COD] of int: profit;
47 |
48 | array[DOM] of var COD: task;
49 |
50 | include "all_different.mzn";
51 | constraint all_different(task);
52 |
53 | solve maximize sum(w in DOM)
54 | (profit[w,task[w]]);
55 | """
56 | )
57 | model.output_type = TaskAssignment
58 |
59 | inst = Instance(gecode, model)
60 | inst["n"] = 4
61 | inst["m"] = 5
62 | inst["profit"] = [[7, 1, 3, 4, 6], [8, 2, 5, 1, 4], [4, 3, 7, 2, 5], [3, 1, 6, 3, 6]]
63 |
64 |
65 | sol = inst.solve().solution
66 | assert type(sol) == TaskAssignment
67 |
68 | if sol.check:
69 | print("A valid assignment!")
70 | else:
71 | print("A bad assignment!")
72 |
73 |
74 | .. _meta-heuristics:
75 |
76 | Defining Meta-Heuristics
77 | ------------------------
78 |
79 | Modellers will sometimes require the use of meta-heuristics to more
80 | efficiently solve their problem instances. MiniZinc Python can assist in the
81 | formatting of Meta-Heuristics by the use of the :func:`Instance.branch`
82 | method. This method allows you to make incremental (temporary) changes to a
83 | :class:`Instance` object. This could, for example allow you to explore a
84 | different part of the search space.
85 |
86 | The following example shows a manual implementation of the branch-and-bound
87 | algorithm used in various solvers. It first looks for any solution. Once a
88 | solution is found, a new constraint is added to ensure that the next solution
89 | has a higher objective value. The second step is repeated until no solutions
90 | can be found.
91 |
92 | .. code-block:: python
93 |
94 | from minizinc import Instance, Model, Result, Solver, Status
95 |
96 | gecode = Solver.lookup("gecode")
97 | m = Model()
98 | m.add_string(
99 | """
100 | array[1..4] of var 1..10: x;
101 | var int: obj;
102 |
103 | constraint obj = sum(x);
104 | output ["\\(obj)"];
105 | """
106 | )
107 | inst = Instance(gecode, m)
108 |
109 | res: Result = inst.solve()
110 | print(res.solution)
111 | while res.status == Status.SATISFIED:
112 | with inst.branch() as child:
113 | child.add_string(f"constraint obj > {res['obj']};")
114 | res = child.solve()
115 | if res.solution is not None:
116 | print(res.solution)
117 |
118 | Note that all constraints added to the child instance are removed once the
119 | with-context ends. For branch-and-bound the added constraints are
120 | complementary and do not have to be retracted. For other search algorithms
121 | this is not the case. The following example performs a simple Large
122 | Neighbourhood search. After finding an initial solution, the search will
123 | randomly fix 70% of its variables and try and find a better solution. If no
124 | better solution is found in the last 3 iterations, it will stop.
125 |
126 | .. code-block:: python
127 |
128 | import random
129 |
130 | from minizinc import Instance, Model, Result, Solver, Status
131 |
132 | gecode = Solver.lookup("gecode")
133 | m = Model()
134 | m.add_string(
135 | """
136 | array[1..10] of var 1..10: x;
137 | var int: obj;
138 |
139 | constraint obj = sum(x);
140 | output ["\\(obj)"]
141 | """
142 | )
143 | inst = Instance(gecode, m)
144 |
145 | res: Result = inst.solve()
146 | incumbent = res.solution
147 | i = 0
148 | print(incumbent)
149 | while i < 10:
150 | with inst.branch() as child:
151 | for i in random.sample(range(10), 7):
152 | child.add_string(f"constraint x[{i + 1}] == {incumbent.x[i]};\n")
153 | child.add_string(f"solve maximize obj;\n")
154 | res = child.solve()
155 | if res.solution is not None and res["obj"] > incumbent.obj:
156 | i = 0
157 | incumbent = res.solution
158 | print(incumbent)
159 | else:
160 | i += 1
161 |
162 | Getting Diverse Solutions
163 | -------------------------
164 |
165 | It is sometimes useful to find multiple solutions to a problem
166 | that exhibit some desired measure of diversity. For example, in a
167 | satisfaction problem, we may wish to have solutions that differ in
168 | the assignments to certain variables but we might not care about some
169 | others. Another important case is where we wish to find a diverse set
170 | of close-to-optimal solutions.
171 |
172 | The following example demonstrates a simple optimisation problem where
173 | we wish to find a set of 5 diverse, close to optimal solutions.
174 | First, to define the diversity metric, we annotate the solve item with
175 | the :func:`diverse_pairwise(x, "hamming_distance")` annotation to indicate that
176 | we wish to find solutions that have the most differences to each other.
177 | The `diversity.mzn` library also defines the "manhattan_distance"
178 | diversity metric which computes the sum of the absolution difference
179 | between solutions.
180 | Second, to define how many solutions, and how close to optimal we wish the
181 | solutions to be, we use the :func:`diversity_incremental(5, 1.0)` annotation.
182 | This indicates that we wish to find 5 diverse solutions, and we will
183 | accept solutions that differ from the optimal by 100% (Note that this is
184 | the ratio of the optimal solution, not an optimality gap).
185 |
186 | .. code-block:: minizinc
187 |
188 | % AllDiffOpt.mzn
189 | include "alldifferent.mzn";
190 | include "diversity.mzn";
191 |
192 | array[1..5] of var 1..5: x;
193 | constraint alldifferent(x);
194 |
195 | solve :: diverse_pairwise(x, "hamming_distance")
196 | :: diversity_incremental(5, 1.0) % number of solutions, gap %
197 | minimize x[1];
198 |
199 | The :func:`Instance.diverse_solutions` method will use these annotations
200 | to find the desired set of diverse solutions. If we are solving an
201 | optimisation problem and want to find "almost" optimal solutions we must
202 | first acquire the optimal solution. This solution is then passed to
203 | the :func:`diverse_solutions()` method in the :func:`reference_solution` parameter.
204 | We loop until we see a duplicate solution.
205 |
206 | .. code-block:: python
207 |
208 | import asyncio
209 | import minizinc
210 |
211 | async def main():
212 | # Create a MiniZinc model
213 | model = minizinc.Model("AllDiffOpt.mzn")
214 |
215 | # Transform Model into a instance
216 | gecode = minizinc.Solver.lookup("gecode")
217 | inst = minizinc.Instance(gecode, model)
218 |
219 | # Solve the instance
220 | result = await inst.solve_async(all_solutions=False)
221 | print(result.objective)
222 |
223 | # Solve the instance to obtain diverse solutions
224 | sols = []
225 | async for divsol in inst.diverse_solutions(reference_solution=result):
226 | if divsol["x"] not in sols:
227 | sols.append(divsol["x"])
228 | else:
229 | print("New diverse solution already in the pool of diverse solutions. Terminating...")
230 | break
231 | print(divsol["x"])
232 |
233 | asyncio.run(main())
234 |
235 |
236 | Concurrent Solving
237 | ------------------
238 |
239 | MiniZinc Python provides asynchronous methods for solving MiniZinc instances.
240 | These methods can be used to concurrently solve an instances and/or use some of
241 | pythons other functionality. The following code sample shows a MiniZinc instance
242 | that is solved by two solvers at the same time. The solver that solves the
243 | instance the fastest is proclaimed the winner!
244 |
245 | .. code-block:: python
246 |
247 | import asyncio
248 | import minizinc
249 |
250 | # Lookup solvers to compete
251 | chuffed = minizinc.Solver.lookup("chuffed")
252 | gecode = minizinc.Solver.lookup("gecode")
253 |
254 | # Create model
255 | model = minizinc.Model(["nqueens.mzn"])
256 | model["n"] = 16
257 |
258 |
259 | async def solver_race(model, solvers):
260 | tasks = set()
261 | for solver in solvers:
262 | # Create an instance of the model for every solver
263 | inst = minizinc.Instance(solver, model)
264 |
265 | # Create a task for the solving of each instance
266 | task = asyncio.create_task(inst.solve_async())
267 | task.solver = solver.name
268 | tasks.add(task)
269 |
270 | # Wait on the first task to finish and cancel the other tasks
271 | done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
272 | for t in pending:
273 | t.cancel()
274 |
275 | # Declare the winner
276 | for t in done:
277 | print("{} finished solving the problem first!".format(t.solver))
278 |
279 |
280 | asyncio.run(solver_race(model, [chuffed, gecode]))
281 |
282 |
283 | Concurrent Solutions
284 | --------------------
285 |
286 | MiniZinc Python provides an asynchronous generator to receive the generated
287 | solutions. The generator allows users to process solutions as they come in. The
288 | following example solves the n-queens problem and displays a board with the
289 | letter Q on any position that contains a queen.
290 |
291 | .. code-block:: python
292 |
293 | import asyncio
294 | import minizinc
295 |
296 |
297 | async def show_queens(n):
298 | # Create model
299 | model = minizinc.Model(["nqueens.mzn"])
300 | model["n"] = n
301 | # Lookup solver
302 | gecode = minizinc.Solver.lookup("gecode")
303 | instance = minizinc.Instance(gecode, model)
304 |
305 | async for result in instance.solutions(all_solutions=True):
306 | if result.solution is None:
307 | continue
308 | queens = result["q"]
309 |
310 | for row in range(len(queens)):
311 | # Print line
312 | print("".join(["--" for _ in range(len(queens))]) + "-")
313 | print("|", end="")
314 | for col in range(len(queens)):
315 | if queens[row] == col:
316 | print("Q|", end="")
317 | else:
318 | print(" |", end="")
319 | print("")
320 |
321 | print("".join(["--" for _ in range(len(queens))]) + "-")
322 |
323 | print("\n --------------------------------- \n")
324 |
325 |
326 | asyncio.run(show_queens(6))
327 |
328 |
329 | .. _multiple-minizinc:
330 |
331 | Using multiple MiniZinc versions
332 | --------------------------------
333 |
334 | MiniZinc Python is designed to be flexible in its communication with MiniZinc.
335 | That is why it is possible to switch to a different version of MiniZinc, or even
336 | use multiple versions of MiniZinc at the same time. This can be useful to
337 | compare different versions of MiniZinc.
338 |
339 | In MiniZinc Python a MiniZinc executable or shared library is represented by a
340 | :class:`Driver` object. The :func:`Driver.find` function can help finding a
341 | compatible MiniZinc executable or shared library and create an associated Driver
342 | object. The following example shows how to load two versions of MiniZinc and
343 | sets one as the new default.
344 |
345 | .. code-block:: python
346 |
347 | from minizinc import Driver, Instance, Solver, default_driver
348 |
349 | print(default_driver.minizinc_version)
350 |
351 | v23: Driver = Driver.find(["/minizinc/2.3.2/bin"])
352 | print(v23.minizinc_version)
353 | gecode = Solver.lookup("gecode", driver=v23)
354 | v23_instance = Instance(gecode, driver=v23)
355 |
356 | v24: Driver = Driver.find(["/minizinc/2.4.0/bin"])
357 | print(v24.minizinc_version)
358 | gecode = Solver.lookup("gecode", driver=v24)
359 | v24_instance = Instance(gecode, driver=v24)
360 |
361 | v24.make_default()
362 | print(default_driver.minizinc_version)
363 | gecode = Solver.lookup("gecode") # using the new default_driver
364 | instance = Instance(gecode)
365 |
366 |
367 | Debugging the Python to MiniZinc connection
368 | -------------------------------------------
369 |
370 | This package has been setup to contain useful logging features to find any
371 | potential issues in the connections from Python to MiniZinc. The logging is
372 | implemented using Python's default `logging` package and is always enabled. To
373 | view this log one has to set the preferred output mode and event level before
374 | importing the MiniZinc package. The following example will view all logged
375 | events and write them to a file:
376 |
377 | .. code-block:: python
378 |
379 | import logging
380 |
381 | logging.basicConfig(filename="minizinc-python.log", level=logging.DEBUG)
382 |
383 | import minizinc
384 |
385 | model = minizinc.Model(["nqueens.mzn"])
386 | solver = minizinc.Solver.lookup("gecode")
387 | instance = minizinc.Instance(solver, model)
388 | instance["n"] = 9
389 | print(instance.solve())
390 |
391 |
392 | The generated file will contain all remote calls to the MiniZinc executable:
393 |
394 | .. code-block:: none
395 |
396 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
397 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
398 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
399 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
400 | DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', )]
401 | DEBUG:asyncio:Using selector: KqueueSelector
402 | DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn"
403 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
404 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
405 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
406 | DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', )]
407 | DEBUG:asyncio:Using selector: KqueueSelector
408 | DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn /var/folders/gj/cmhh026j5ddb28sw1z95pygdy5kk20/T/mzn_datau1ss0gze.json"
409 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --version", timeout None
410 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --solvers-json", timeout None
411 | DEBUG:minizinc:CLIDriver:run -> command: "/Applications/MiniZincIDE.app/Contents/Resources/minizinc --solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --model-interface-only nqueens.mzn", timeout None
412 | DEBUG:minizinc:CLIInstance:analyse -> output fields: [('q', typing.List[int]), ('_checker', )]
413 | DEBUG:asyncio:Using selector: KqueueSelector
414 | DEBUG:minizinc:CLIDriver:create_process -> program: /Applications/MiniZincIDE.app/Contents/Resources/minizinc args: "--solver org.gecode.gecode@6.1.1 --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s nqueens.mzn /var/folders/gj/cmhh026j5ddb28sw1z95pygdy5kk20/T/mzn_datamnhikzwo.json"
415 |
416 |
417 | If MiniZinc Python instances are instantiated directly from Python, then the
418 | CLI driver will generate temporary files to use with the created MiniZinc
419 | process. When something seems wrong with MiniZinc Python it is often a good
420 | idea to retrieve these objects to both check if the files are generated
421 | correctly and to recreate the exact MiniZinc command that is ran. For a
422 | ``CLIInstance`` object, you can use the ``files()`` method to generate the
423 | files. You can then inspect or copy these files:
424 |
425 | .. code-block:: python
426 |
427 | import minizinc
428 | from pathlib import Path
429 |
430 | model = minizinc.Model(["nqueens.mzn"])
431 | solver = minizinc.Solver.lookup("gecode")
432 | instance = minizinc.Instance(solver, model)
433 | instance["n"] = 9
434 |
435 | with instance.files() as files:
436 | store = Path("./tmp")
437 | store.mkdir()
438 | for f in files:
439 | f.link_to(store / f.name)
440 |
441 |
442 | Finally, if you are looking for bugs related to the behaviour of MiniZinc,
443 | solvers, or solver libraries, then it could be helpful to have a look at the
444 | direct MiniZinc output on ``stderr``. The ``CLIInstance`` class now has
445 | experimental support for to write the output from ``stderr`` to a file and to
446 | enable verbose compilation and solving (using the command line ``-v`` flag).
447 | The former is enable by providing a ``pathlib.Path`` object as the
448 | ``debug_output`` parameter to any solving method. Similarly, the verbose flag
449 | is triggered by setting the ``verbose`` parameter to ``True``. The following
450 | fragment shows capture the verbose output of a model that contains trace
451 | statements (which are also captured on ``stderr``):
452 |
453 | .. code-block:: python
454 |
455 | import minizinc
456 | from pathlib import Path
457 |
458 | gecode = minizinc.Solver.lookup("gecode")
459 | instance = minizinc.Instance(gecode)
460 | instance.add_string("""
461 | constraint trace("--- TRACE: This is a trace statement\\n");
462 | """)
463 |
464 | instance.solve(verbose=True, debug_output=Path("./debug_output.txt"))
465 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. module:: minizinc
5 |
6 | This part of the documentation lists the full API reference of all public
7 | classes and functions.
8 |
9 | Solvers
10 | -------
11 |
12 | .. autoclass:: minizinc.solver.Solver
13 | :members:
14 |
15 | Models
16 | ------
17 |
18 | .. autoclass:: minizinc.model.Method
19 |
20 | .. autoclass:: minizinc.model.Model
21 | :members:
22 | :special-members: __getitem__, __setitem__
23 |
24 | Instances
25 | ---------
26 |
27 | .. autoclass:: minizinc.instance.Instance
28 | :members:
29 | :special-members: __getitem__, __setitem__
30 |
31 | Results
32 | -------
33 |
34 | .. autoclass:: minizinc.result.Result
35 | :members:
36 | :special-members: __getitem__, __len__
37 |
38 | .. autoclass:: minizinc.result.Status
39 | :members:
40 |
41 | Drivers
42 | -------
43 |
44 | .. autoclass:: minizinc.driver.Driver
45 | :members:
46 |
47 | Errors
48 | ------
49 |
50 | .. autoexception:: minizinc.error.ConfigurationError
51 | :members:
52 |
53 | .. autofunction:: minizinc.error.parse_error
54 |
55 | .. autoexception:: minizinc.error.MiniZincError
56 | :members:
57 |
58 | .. autoclass:: minizinc.error.Location
59 |
60 | .. autoexception:: minizinc.error.EvaluationError
61 | :members:
62 |
63 | .. autoexception:: minizinc.error.AssertionError
64 | :members:
65 |
66 | .. autoexception:: minizinc.error.TypeError
67 | :members:
68 |
69 | .. autoexception:: minizinc.error.SyntaxError
70 | :members:
71 |
72 | Helper Functions
73 | ----------------
74 |
75 | .. autofunction:: minizinc.helpers.check_result
76 |
77 | .. autofunction:: minizinc.helpers.check_solution
78 |
--------------------------------------------------------------------------------
/docs/basic_usage.rst:
--------------------------------------------------------------------------------
1 | Basic Usage
2 | ============
3 |
4 | This page provides examples of the basic of MiniZinc Python. It will show
5 | example of the types available in MiniZinc Python and shows how to use features
6 | that are often used.
7 |
8 | Using sets
9 | -----------
10 |
11 | There are two types in Python that are associated with MiniZinc's sets: ``set``
12 | and ``range``. Generally a set in MiniZinc will be of the type ``set``. For
13 | example, the minizinc set ``{-2, 4, 12}`` will be represented in MiniZinc Python
14 | as ``{-2, 4, 12}`` or ``set([-2, 4, 12])``. However, contiguous sets, like index sets in MiniZinc,
15 | can be more efficiently represented in a ``range`` object, as this only records
16 | the start and end of the set. For example, the MiniZinc set ``-90..310`` is
17 | represented using ``range(-90, 311)`` in MiniZinc Python. When creating a set in
18 | Python, either object can be translated to a MiniZinc set.
19 |
20 | .. note::
21 |
22 | The end given Python ``range`` objects is non-inclusive. This means the
23 | object ``range(1, 3)`` only contains 1 and 2. This is unlike the MiniZinc
24 | range syntax, which is inclusive. The MiniZinc set ``1..3`` contains 1, 2,
25 | and 3.
26 |
27 | The following example shows how to assign set parameters and how to use the
28 | solutions for set variables.
29 |
30 | .. code-block:: python
31 |
32 | from minizinc import Instance, Model, Solver
33 |
34 | gecode = Solver.lookup("gecode")
35 |
36 | model = Model()
37 | model.add_string(
38 | """
39 | include "all_different.mzn";
40 | set of int: A;
41 | set of int: B;
42 | array[A] of var B: arr;
43 | var set of B: X;
44 | var set of B: Y;
45 |
46 | constraint all_different(arr);
47 | constraint forall (i in index_set(arr)) ( arr[i] in X );
48 | constraint forall (i in index_set(arr)) ( (arr[i] mod 2 = 0) <-> arr[i] in Y );
49 | """
50 | )
51 |
52 | instance = Instance(gecode, model)
53 | instance["A"] = range(3, 8) # MiniZinc: 3..7
54 | instance["B"] = {4, 3, 2, 1, 0} # MiniZinc: {4, 3, 2, 1, 0}
55 |
56 | result = instance.solve()
57 | print(result["X"]) # range(0, 5)
58 | assert isinstance(result["X"], range)
59 | print(result["Y"]) # {0, 2, 4}
60 | assert isinstance(result["Y"], set)
61 |
62 |
63 | Using enumerated types
64 | -----------------------
65 |
66 | The support for enumerated types in MiniZinc Python is still limited. It is,
67 | however, already supported to assign enumerated types in MiniZinc using a Python
68 | enumeration. When a enumeration is assigned, the values in the solution are
69 | ensured to be of the assigned enumerated type. This is demonstrated in the
70 | following example:
71 |
72 | .. code-block:: python
73 |
74 | import enum
75 |
76 | from minizinc import Instance, Model, Solver
77 |
78 | gecode = Solver.lookup("gecode")
79 |
80 | model = Model()
81 | model.add_string(
82 | """
83 | enum DAY;
84 | var DAY: d;
85 | constraint d = min(DAY);
86 | """
87 | )
88 | instance = Instance(gecode, model)
89 |
90 | Day = enum.Enum("Day", ["Mo", "Tu", "We", "Th", "Fr"])
91 | instance["DAY"] = Day
92 |
93 | result = instance.solve()
94 | print(result["d"]) # Day.Mo
95 | assert isinstance(result["d"], Day)
96 |
97 | Enumerations that are defined in MiniZinc are currently not translated into
98 | Python enumerations. Their values are currently returned as strings. The
99 | following adaptation of the previous example declares an enumerated type in
100 | MiniZinc and contains a string in it's solution.
101 |
102 |
103 | .. code-block:: python
104 |
105 | from minizinc import Instance, Model, Solver
106 |
107 | gecode = Solver.lookup("gecode")
108 |
109 | model = Model()
110 | model.add_string(
111 | """
112 | enum DAY = {Mo, Tu, We, Th, Fr};
113 | var DAY: d;
114 | constraint d = min(DAY);
115 | """
116 | )
117 | instance = Instance(gecode, model)
118 |
119 | result = instance.solve()
120 | print(result["d"]) # Mo
121 | assert isinstance(result["d"], str)
122 |
123 |
124 | Finding all optimal solutions
125 | -----------------------------
126 |
127 | MiniZinc does not support finding all *optimal* solutions for a specific
128 | optimisation problem. However, a scheme that is often used to find all
129 | optimal solutions is to first find one optimal solution and then find all
130 | other solutions with the same optimal value. The following example shows this
131 | process for a toy model that maximises the value of an array of unique integers:
132 |
133 | .. code-block:: python
134 |
135 | from minizinc import Instance, Model, Solver
136 |
137 | gecode = Solver.lookup("gecode")
138 |
139 | model = Model()
140 | model.add_string(
141 | """
142 | include "all_different.mzn";
143 | array[1..4] of var 1..10: x;
144 | constraint all_different(x);
145 | """
146 | )
147 | instance = Instance(gecode, model)
148 |
149 | with instance.branch() as opt:
150 | opt.add_string("solve maximize sum(x);\n")
151 | res = opt.solve()
152 | obj = res["objective"]
153 |
154 | instance.add_string(f"constraint sum(x) = {obj};\n")
155 |
156 | result = instance.solve(all_solutions=True)
157 | for sol in result.solution:
158 | print(sol.x)
159 |
160 | .. seealso::
161 |
162 | In the example the :func:`Instance.branch` method is used to temporarily
163 | add a search goal to the :class:`Instance` object. More information about
164 | the usage of this method can be found in the :ref:`advanced examples
165 | `.
166 |
167 | Using a Solution Checker
168 | ------------------------
169 |
170 | MiniZinc Python supports the use of MiniZinc solution checkers. The solution
171 | checker file can be added to a ``Model``/``Instance`` just like any normal
172 | MiniZinc file. Once a checker has been added, any ``solve`` operation will
173 | automatically run the checker. The output of the Solution checker can be
174 | accessed using the ``Solution.check()`` method. The following example follows
175 | shows the usage for a trivial Solution checking model.
176 |
177 | .. code-block:: python
178 |
179 | from minizinc import Instance, Model, Solver
180 |
181 | gecode = Solver.lookup("gecode")
182 |
183 | model = Model()
184 | # --- small_alldifferent.mzn ---
185 | # include "all_different.mzn";
186 | # array[1..4] of var 1..10: x;
187 | # constraint all_different(x);
188 | # ------------------------------
189 | model.add_file("small_alldifferent.mzn")
190 | # --- check_all_different.mzc.mzn ---
191 | # array[int] of int: x;
192 | # output[
193 | # if forall(i,j in index_set(x) where i`_ 2.6 (or higher)
8 | - `Python `_ 3.8 (or higher)
9 |
10 | .. note::
11 |
12 | MiniZinc is expected to be in its default location. If you are on a Linux
13 | machine or changed this location, then you will have to ensure that the
14 | ``minizinc`` executable is located in a folder in the ``$PATH``
15 | environmental variable. When MiniZinc cannot be located, the following
16 | warning will be shown: **MiniZinc was not found on the system: no default
17 | driver could be initialised**. The path can manually be provided using
18 | ``Driver.find`` function.
19 |
20 | Installation
21 | ------------
22 |
23 | MiniZinc Python can be found on `PyPI `_. If
24 | you have the ``pip`` package manager installed, then the simplest way of
25 | installing MiniZinc Python is using the following command:
26 |
27 | .. code-block:: bash
28 |
29 | $ pip install minizinc
30 |
31 | .. note::
32 |
33 | On machines that have both Python 2 and Python 3 installed you might have to
34 | use ``pip3`` instead of ``pip``
35 |
36 | .. note::
37 |
38 | If you require the parsed information of ``.dzn`` files within your python
39 | environment, then you have to install the ``dzn`` extra with the MiniZinc
40 | package: ``pip install minizinc[dzn]``
41 |
42 | A basic example
43 | ---------------
44 |
45 | To test everything is working let's run a basic example. The n-Queens problem is
46 | a famous problem within the constraint programming community. In the MiniZinc
47 | Examples we can find the following model for this problem:
48 |
49 | .. code-block:: minizinc
50 |
51 | int: n; % The number of queens.
52 |
53 | array [1..n] of var 1..n: q;
54 |
55 | include "alldifferent.mzn";
56 |
57 | constraint alldifferent(q);
58 | constraint alldifferent(i in 1..n)(q[i] + i);
59 | constraint alldifferent(i in 1..n)(q[i] - i);
60 |
61 |
62 | The following Python code will use MiniZinc Python to:
63 |
64 | 1. Load the model from a file (``nqueens.mzn``)
65 | 2. Create an instance of the model for the Gecode solver
66 | 3. Assign the value 4 to ``n`` in the instance
67 | 4. Print the positions of the Queens store in the array ``q``
68 |
69 | .. code-block:: python
70 |
71 | from minizinc import Instance, Model, Solver
72 |
73 | # Load n-Queens model from file
74 | nqueens = Model("./nqueens.mzn")
75 | # Find the MiniZinc solver configuration for Gecode
76 | gecode = Solver.lookup("gecode")
77 | # Create an Instance of the n-Queens model for Gecode
78 | instance = Instance(gecode, nqueens)
79 | # Assign 4 to n
80 | instance["n"] = 4
81 | result = instance.solve()
82 | # Output the array q
83 | print(result["q"])
84 |
85 |
86 | Using different solvers
87 | ------------------------
88 |
89 | One of MiniZinc's key features is the ability to use multiple solvers. MiniZinc
90 | Python allows you to use all of MiniZinc's solver using a `solver configuration
91 | `.
92 | Solver configurations were introduces in MiniZinc 2.2. In MiniZinc Python there
93 | are three ways of accessing a solver using solver configurations:
94 |
95 | I. You can ``lookup`` a solver configuration that is known to MiniZinc. These
96 | are solver configurations that are placed on standard locations or in a
97 | folder included in the ``$MZN_SOLVER_PATH`` environmental variable. This is
98 | the most common way of accessing solvers.
99 | II. You can ``load`` a solver configuration directly from a solver configuration
100 | file, ``.msc``. A description of the formatting of such files can be found
101 | in the `MiniZinc documentation
102 | `_.
103 | The :meth:`minizinc.Solver.output_configuration` method can be used to
104 | generate a valid solver configuration.
105 | III. You can create a new solver configuration, ``Solver``.
106 |
107 | .. note::
108 |
109 | Solver loaded from file (2) or created in MiniZinc Python (3). Cannot share
110 | the combination of identifier and version with a solver known to MiniZinc
111 | (1). In these cases the solver configuration as known to MiniZinc will be
112 | used.
113 |
114 | The following example shows an example of each method. It will lookup the
115 | Chuffed solver, then load a solver configuration from a file located at
116 | ``./solvers/or-tools.msc``, and, finally, create a new solver configuration for
117 | a solver named "My Solver".
118 |
119 | .. code-block:: python
120 |
121 | from minizinc import Solver
122 | from pathlib import Path
123 |
124 | # Lookup Chuffed among MiniZinc solver configurations.
125 | # The argument can be a solver tag, its full identifier, or the last part of
126 | # its identifier
127 | chuffed = Solver.lookup("chuffed")
128 |
129 | # Load solver configuration from file
130 | or_tools = Solver.load(Path("./solvers/or-tools.msc"))
131 |
132 | # Create a new solver configuration
133 | # Arguments: name, version, identifier, executable
134 | my_solver = Solver(
135 | "My Solver",
136 | "0.7",
137 | "com.example.mysolver",
138 | "/usr/local/bin/fzn-my-solver",
139 | )
140 |
141 | # You can now change other options in the solver created configuration
142 | my_solver.mznlib = "/usr/local/share/mysolver/mznlib"
143 | my_solver.stdFlags = ["-a", "-t", "-s"]
144 |
145 |
146 | Finding all solutions
147 | ---------------------
148 |
149 | Sometimes we don't just require one solution for the given MiniZinc instance,
150 | but all possible solutions. The following variation of the previous example uses
151 | the ``all_solutions=True`` parameter to ask for all solutions to the problem
152 | instance.
153 |
154 | .. code-block:: python
155 |
156 | from minizinc import Instance, Model, Solver
157 |
158 | gecode = Solver.lookup("gecode")
159 |
160 | nqueens = Model("./nqueens.mzn")
161 | instance = Instance(gecode, nqueens)
162 | instance["n"] = 4
163 |
164 | # Find and print all possible solutions
165 | result = instance.solve(all_solutions=True)
166 | for i in range(len(result)):
167 | print(result[i, "q"])
168 |
169 | The use of the ``all_solutions=True`` parameter is limited to satisfaction
170 | models (``solve satisfy``). MiniZinc currently does not support looking for all
171 | solutions for an optimisation model.
172 |
173 | Similarly, in a optimisation model (``solve maximize`` or ``solve minimize``) we
174 | could want access to the intermediate solutions created by the solver during the
175 | optimisation process. (This could provide insight into the progress the solver
176 | makes). In this case the ``intermediate_solutions=True`` parameter can be used.
177 | The following example prints the intermediate solutions that Gecode found to the
178 | trivial problem of find the highest uneven number between 1 and 10, but trying
179 | smaller values first.
180 |
181 | .. code-block:: python
182 |
183 | from minizinc import Instance, Model, Solver
184 |
185 | gecode = Solver.lookup("gecode")
186 |
187 | trivial = Model()
188 | trivial.add_string(
189 | """
190 | var 1..10: x;
191 | constraint (x mod 2) = 1;
192 | solve ::int_search([x], input_order, indomain_min) maximize x;
193 | """
194 | )
195 | instance = Instance(gecode, trivial)
196 |
197 | # Find and print all intermediate solutions
198 | result = instance.solve(intermediate_solutions=True)
199 | for i in range(len(result)):
200 | print(result[i, "x"])
201 |
202 | .. note::
203 |
204 | Not all solver support the finding of all solutions and the printing of
205 | intermediate solutions. Solvers that support these functionalities will have
206 | ``-a`` among the standard flags supported by the solvers. MiniZinc Python
207 | will automatically check if this flag is available. If this is not the case,
208 | then an exception will be thrown when the requesting all or intermediate
209 | solutions.
210 |
211 | .. seealso::
212 |
213 | For information about other parameters that are available when solving a
214 | model instance, see :meth:`minizinc.Instance.solve`
215 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | MiniZinc Python
2 | ===============
3 |
4 | MiniZinc Python provides a native python interface for the MiniZinc toolchain.
5 | The package can interface with MiniZinc using the command line interface, the
6 | ``minizinc`` executable. The main goal of this library is to allow users to
7 | use all of MiniZinc's capabilities directly from Python. This allows you to
8 | use MiniZinc in your application, but also enables you to use MiniZinc in new
9 | ways! Using MiniZinc in a procedural language allows you to use incremental
10 | solving techniques that can be used to implement different kinds of
11 | meta-heuristics.
12 |
13 | .. note::
14 |
15 | The development of MiniZinc Python is still in its early stages. Although
16 | the module is fully supported and the functionality is stabilising, we
17 | will not guarantee that changes made before version 1.0 are backwards
18 | compatible. Similarly, the functionality of this module is closely
19 | connected to the releases of the main MiniZinc bundle. An update to this
20 | module might require an update to your MiniZinc installation.
21 |
22 | Once the project reaches version 1.0, it will abide by `Semantic Versioning
23 | `_. All (breaking) changes are recorded in the
24 | :ref:`changelog `.
25 |
26 |
27 | Documentation
28 | -------------
29 |
30 | This part of the documentation guides you through all of the library’s usage
31 | patterns.
32 |
33 | .. toctree::
34 | :maxdepth: 2
35 |
36 | getting_started
37 | basic_usage
38 | advanced_usage
39 |
40 | API Reference
41 | -------------
42 |
43 | If you are looking for information on a specific function, class, or method,
44 | this part of the documentation is for you.
45 |
46 | .. toctree::
47 | :maxdepth: 2
48 |
49 | api
50 |
51 | Changelog
52 | ---------
53 |
54 | All changes made to this project are recorded in the changelog.
55 |
56 | .. toctree::
57 | :maxdepth: 1
58 |
59 | changelog
60 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "minizinc"
7 | version = "0.10.0"
8 | description = "Access MiniZinc directly from Python"
9 | readme = "README.md"
10 | authors = [{ name = "Jip J. Dekker", email = "jip@dekker.one" }]
11 | license = { text = "MPL-2.0" }
12 | requires-python = ">=3.8"
13 | classifiers = [
14 | "Development Status :: 4 - Beta",
15 | "Programming Language :: Python :: 3",
16 | "Programming Language :: Python :: 3.8",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: 3.10",
19 | "Programming Language :: Python :: 3.11",
20 | "Programming Language :: Python :: 3.12",
21 | "Programming Language :: Python :: 3.13",
22 | "Programming Language :: Python :: Implementation :: CPython",
23 | "Programming Language :: Python :: Implementation :: PyPy",
24 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
25 | "Operating System :: OS Independent",
26 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
27 | "Topic :: Scientific/Engineering :: Mathematics",
28 | ]
29 |
30 | [project.entry-points."pygments.lexers"]
31 | minizinclexer = "minizinc.pygments:MiniZincLexer"
32 |
33 | [project.optional-dependencies]
34 | dzn = ["lark-parser>=0.12.0"]
35 | docs = ["setuptools>=75.3.0", "sphinx>=7.1.2", "sphinx-rtd-theme>=3.0.2"]
36 |
37 | [project.urls]
38 | Homepage = "https://www.minizinc.org/"
39 | Documentation = "https://python.minizinc.dev/"
40 | Repository = "https://github.com/MiniZinc/minizinc-python.git"
41 | Issues = "https://github.com/MiniZinc/minizinc-python/issues"
42 | Changelog = "https://raw.githubusercontent.com/MiniZinc/minizinc-python/refs/heads/develop/CHANGELOG.rst"
43 |
44 | [dependency-groups]
45 | dev = [
46 | "minizinc",
47 | "mypy>=1.13.0",
48 | "pytest>=8.3.4",
49 | "ruff>=0.8.1",
50 | "types-setuptools>=75.6.0.20241126",
51 | ]
52 |
53 | [tool.mypy]
54 | python_version = "3.8"
55 | platform = "linux"
56 | # do not follow imports (except for ones found in typeshed)
57 | follow_imports = "skip"
58 | # since we're ignoring imports, writing .mypy_cache doesn't make any sense
59 | cache_dir = "/dev/null"
60 | # suppress errors about unsatisfied imports
61 | ignore_missing_imports = true
62 |
63 | [tool.ruff]
64 | line-length = 80
65 | lint.ignore = ['E501', 'C901']
66 | lint.select = ['B', 'C', 'E', 'I', 'F', 'W']
67 |
68 | [tool.uv.sources]
69 | minizinc = { workspace = true }
70 |
--------------------------------------------------------------------------------
/src/minizinc/__init__.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 | import logging
5 | import warnings
6 | from typing import Optional
7 |
8 | from .driver import Driver
9 | from .error import ConfigurationError, MiniZincError
10 | from .instance import Instance
11 | from .model import Method, Model
12 | from .result import Result, Status
13 | from .solver import Solver
14 | from .types import AnonEnum, ConstrEnum
15 |
16 | __version__ = "0.10.0"
17 |
18 | logger = logging.getLogger("minizinc")
19 |
20 | #: Default MiniZinc driver used by the python package
21 | try:
22 | default_driver: Optional[Driver] = Driver.find()
23 | if default_driver is not None:
24 | default_driver.make_default()
25 | else:
26 | warnings.warn(
27 | "MiniZinc was not found on the system. No default driver could be "
28 | "initialised.",
29 | RuntimeWarning,
30 | stacklevel=1,
31 | )
32 | except ConfigurationError as err:
33 | warnings.warn(
34 | f"The MiniZinc version found on the system is incompatible with "
35 | f"MiniZinc Python:\n{err}\n No default driver could be initialised.",
36 | RuntimeWarning,
37 | stacklevel=1,
38 | )
39 |
40 | __all__ = [
41 | "__version__",
42 | "default_driver",
43 | "AnonEnum",
44 | "ConstrEnum",
45 | "Driver",
46 | "Instance",
47 | "Method",
48 | "MiniZincError",
49 | "Model",
50 | "Result",
51 | "Solver",
52 | "Status",
53 | "Diversity",
54 | ]
55 |
--------------------------------------------------------------------------------
/src/minizinc/analyse.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 | import json
5 | import os
6 | import platform
7 | import shutil
8 | import subprocess
9 | from enum import Enum, auto
10 | from pathlib import Path
11 | from typing import Any, Dict, List, Optional, Union
12 |
13 | from .driver import MAC_LOCATIONS, WIN_LOCATIONS
14 | from .error import ConfigurationError, MiniZincError
15 |
16 |
17 | class InlineOption(Enum):
18 | DISABLED = auto()
19 | NON_LIBRARY = auto()
20 | ALL = auto()
21 |
22 |
23 | class MznAnalyse:
24 | """Python interface to the mzn-analyse executable
25 |
26 | This tool is used to retrieve information about or transform a MiniZinc
27 | instance. This is used, for example, to diverse solutions to the given
28 | MiniZinc instance using the given solver configuration.
29 | """
30 |
31 | _executable: Path
32 |
33 | def __init__(self, executable: Path):
34 | self._executable = executable
35 | if not self._executable.exists():
36 | raise ConfigurationError(
37 | f"No MiniZinc data annotator executable was found at '{self._executable}'."
38 | )
39 |
40 | @classmethod
41 | def find(
42 | cls, path: Optional[List[str]] = None, name: str = "mzn-analyse"
43 | ) -> Optional["MznAnalyse"]:
44 | """Finds the mzn-analyse executable on default or specified path.
45 |
46 | The find method will look for the mzn-analyse executable to create an
47 | interface for MiniZinc Python. If no path is specified, then the paths
48 | given by the environment variables appended by default locations will be
49 | tried.
50 |
51 | Args:
52 | path: List of locations to search. name: Name of the executable.
53 |
54 | Returns:
55 | Optional[MznAnalyse]: Returns a MznAnalyse object when found or None.
56 | """
57 |
58 | if path is None:
59 | path = os.environ.get("PATH", "").split(os.pathsep)
60 | # Add default MiniZinc locations to the path
61 | if platform.system() == "Darwin":
62 | path.extend(MAC_LOCATIONS)
63 | elif platform.system() == "Windows":
64 | path.extend(WIN_LOCATIONS)
65 |
66 | # Try to locate the MiniZinc executable
67 | executable = shutil.which(name, path=os.pathsep.join(path))
68 | if executable is not None:
69 | return cls(Path(executable))
70 | return None
71 |
72 | def run(
73 | self,
74 | mzn_files: List[Path],
75 | inline_includes: InlineOption = InlineOption.DISABLED,
76 | remove_litter: bool = False,
77 | get_diversity_anns: bool = False,
78 | get_solve_anns: bool = True,
79 | output_all: bool = True,
80 | mzn_output: Optional[Path] = None,
81 | remove_anns: Optional[List[str]] = None,
82 | remove_items: Optional[List[str]] = None,
83 | ) -> Dict[str, Any]:
84 | # Do not change the order of the arguments 'inline-includes', 'remove-items:output', 'remove-litter' and 'get-diversity-anns'
85 | tool_run_cmd: List[Union[str, Path]] = [
86 | str(self._executable),
87 | "json_out:-",
88 | ]
89 |
90 | for f in mzn_files:
91 | tool_run_cmd.append(str(f))
92 |
93 | if inline_includes == InlineOption.ALL:
94 | tool_run_cmd.append("inline-all_includes")
95 | elif inline_includes == InlineOption.NON_LIBRARY:
96 | tool_run_cmd.append("inline-includes")
97 |
98 | if remove_items is not None and len(remove_items) > 0:
99 | tool_run_cmd.append(f"remove-items:{','.join(remove_items)}")
100 | if remove_anns is not None and len(remove_anns) > 0:
101 | tool_run_cmd.append(f"remove-anns:{','.join(remove_anns)}")
102 |
103 | if remove_litter:
104 | tool_run_cmd.append("remove-litter")
105 | if get_diversity_anns:
106 | tool_run_cmd.append("get-diversity-anns")
107 |
108 | if mzn_output is not None:
109 | tool_run_cmd.append(f"out:{str(mzn_output)}")
110 | else:
111 | tool_run_cmd.append("no_out")
112 |
113 | # Extract the diversity annotations.
114 | proc = subprocess.run(
115 | tool_run_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE
116 | )
117 | if proc.returncode != 0:
118 | raise MiniZincError(message=str(proc.stderr))
119 | return json.loads(proc.stdout)
120 |
--------------------------------------------------------------------------------
/src/minizinc/driver.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import os
6 | import platform
7 | import re
8 | import shutil
9 | import subprocess
10 | import sys
11 | from asyncio import create_subprocess_exec
12 | from asyncio.subprocess import PIPE, Process
13 | from dataclasses import fields
14 | from json import loads
15 | from pathlib import Path
16 | from typing import Any, Dict, List, Optional, Tuple, Union
17 |
18 | import minizinc
19 |
20 | from .error import ConfigurationError, parse_error
21 | from .json import decode_json_stream
22 | from .solver import Solver
23 |
24 | #: MiniZinc version required by the python package
25 | CLI_REQUIRED_VERSION = (2, 6, 0)
26 | #: Default locations on MacOS where the MiniZinc packaged release would be installed
27 | MAC_LOCATIONS = [
28 | str(Path("/Applications/MiniZincIDE.app/Contents/Resources")),
29 | str(Path("/Applications/MiniZincIDE.app/Contents/Resources/bin")),
30 | str(Path("~/Applications/MiniZincIDE.app/Contents/Resources").expanduser()),
31 | str(
32 | Path(
33 | "~/Applications/MiniZincIDE.app/Contents/Resources/bin"
34 | ).expanduser()
35 | ),
36 | ]
37 | #: Default locations on Windows where the MiniZinc packaged release would be installed
38 | WIN_LOCATIONS = [
39 | str(Path("c:/Program Files/MiniZinc")),
40 | str(Path("c:/Program Files/MiniZinc IDE (bundled)")),
41 | str(Path("c:/Program Files (x86)/MiniZinc")),
42 | str(Path("c:/Program Files (x86)/MiniZinc IDE (bundled)")),
43 | ]
44 |
45 |
46 | class Driver:
47 | """Driver that interfaces with MiniZinc through the command line interface.
48 |
49 | The command line driver will interact with MiniZinc and its solvers through
50 | the use of a ``minizinc`` executable. Driving MiniZinc using its executable
51 | is non-incremental and can often trigger full recompilation and might
52 | restart the solver from the beginning when changes are made to the instance.
53 |
54 | Raises:
55 | ConfigurationError: If an the driver version is found to be incompatible with
56 | MiniZinc Python
57 |
58 | Attributes:
59 | _executable (Path): The path to the executable used to access the MiniZinc
60 | """
61 |
62 | _executable: Path
63 | _solver_cache: Optional[Dict[str, List[Solver]]] = None
64 | _version: Optional[Tuple[int, ...]] = None
65 |
66 | def __init__(self, executable: Path):
67 | self._executable = executable
68 | if not self._executable.exists():
69 | raise ConfigurationError(
70 | f"No MiniZinc executable was found at '{self._executable}'."
71 | )
72 |
73 | if self.parsed_version < CLI_REQUIRED_VERSION:
74 | raise ConfigurationError(
75 | f"The MiniZinc driver found at '{self._executable}' has "
76 | f"version {self.parsed_version}. The minimal required version is "
77 | f"{CLI_REQUIRED_VERSION}."
78 | )
79 |
80 | def make_default(self) -> None:
81 | """Method to override the current default MiniZinc Python driver with the
82 | current driver.
83 | """
84 | minizinc.default_driver = self
85 |
86 | @property
87 | def executable(self) -> Path:
88 | """Reports the Path of the MiniZinc executable used by the Driver object
89 |
90 | Returns:
91 | Path: location of the MiniZinc executable
92 | """
93 | return self._executable
94 |
95 | @property
96 | def minizinc_version(self) -> str:
97 | """Reports the version text of the MiniZinc Driver
98 |
99 | Report the full version text of MiniZinc as reported by the driver,
100 | including the driver name, the semantic version, the build reference,
101 | and its authors.
102 |
103 | Returns:
104 | str: the version of as reported by the MiniZinc driver
105 | """
106 | # Note: cannot use "_run" as it already required the parsed version
107 | return subprocess.run(
108 | [str(self._executable), "--version"],
109 | stdin=None,
110 | stdout=PIPE,
111 | stderr=PIPE,
112 | ).stdout.decode()
113 |
114 | @property
115 | def parsed_version(self) -> Tuple[int, ...]:
116 | """Reports the version of the MiniZinc Driver
117 |
118 | Report the parsed version of the MiniZinc Driver as a tuple of integers.
119 | The tuple is ordered: major, minor, patch.
120 |
121 | Returns:
122 | Tuple[int, ...]: the parsd version reported by the MiniZinc driver
123 | """
124 | if self._version is None:
125 | match = re.search(
126 | r"version (\d+)\.(\d+)\.(\d+)", self.minizinc_version
127 | )
128 | assert match
129 | self._version = tuple([int(i) for i in match.groups()])
130 | return self._version
131 |
132 | def available_solvers(self, refresh=False):
133 | """Returns a list of available solvers
134 |
135 | This method returns the list of solvers available to the Driver object
136 | according to the current environment. Note that the list of solvers might
137 | be cached for future usage. The refresh argument can be used to ignore
138 | the current cache.
139 |
140 | Args:
141 | refresh (bool): When set to true, the Driver will rediscover the
142 | available solvers from the current environment.
143 |
144 | Returns:
145 | Dict[str, List[Solver]]: A dictionary that maps solver tags to MiniZinc
146 | solver configurations that can be used with the Driver object.
147 | """
148 | if not refresh and self._solver_cache is not None:
149 | return self._solver_cache
150 |
151 | # Find all available solvers
152 | output = self._run(["--solvers-json"])
153 | solvers = loads(output.stdout)
154 |
155 | # Construct Solver objects
156 | self._solver_cache = {}
157 | allowed_fields = {f.name for f in fields(Solver)}
158 | for s in solvers:
159 | obj = Solver(
160 | **{
161 | key: value
162 | for (key, value) in s.items()
163 | if key in allowed_fields
164 | }
165 | )
166 | if obj.version == "":
167 | obj._identifier = obj.id
168 | else:
169 | obj._identifier = obj.id + "@" + obj.version
170 |
171 | names = s.get("tags", [])
172 | names.extend([s["id"], s["id"].split(".")[-1]])
173 | for name in names:
174 | self._solver_cache.setdefault(name, []).append(obj)
175 |
176 | return self._solver_cache
177 |
178 | def _run(
179 | self,
180 | args: List[Union[str, Path]],
181 | solver: Optional[Solver] = None,
182 | ):
183 | """Start a driver process with given arguments
184 |
185 | Args:
186 | args (List[str]): direct arguments to the driver
187 | solver (Union[str, Path, None]): Solver configuration string
188 | guaranteed by the user to be valid until the process has ended.
189 | """
190 | # TODO: Add documentation
191 | windows_spawn_options: Dict[str, Any] = {}
192 | if sys.platform == "win32":
193 | # On Windows, MiniZinc terminates its subprocesses by generating a
194 | # Ctrl+C event for its own console using GenerateConsoleCtrlEvent.
195 | # Therefore, we must spawn it in its own console to avoid receiving
196 | # that Ctrl+C ourselves.
197 | #
198 | # On POSIX systems, MiniZinc terminates its subprocesses by sending
199 | # SIGTERM to the solver's process group, so this workaround is not
200 | # necessary as we won't receive that signal.
201 | windows_spawn_options = {
202 | "startupinfo": subprocess.STARTUPINFO(
203 | dwFlags=subprocess.STARTF_USESHOWWINDOW,
204 | wShowWindow=subprocess.SW_HIDE,
205 | ),
206 | "creationflags": subprocess.CREATE_NEW_CONSOLE,
207 | }
208 |
209 | args.append("--json-stream")
210 |
211 | if solver is None:
212 | cmd = [str(self._executable), "--allow-multiple-assignments"] + [
213 | str(arg) for arg in args
214 | ]
215 | minizinc.logger.debug(
216 | f'CLIDriver:run -> command: "{" ".join(cmd)}"'
217 | )
218 | output = subprocess.run(
219 | cmd,
220 | stdin=None,
221 | stdout=PIPE,
222 | stderr=PIPE,
223 | **windows_spawn_options,
224 | )
225 | else:
226 | with solver.configuration() as conf:
227 | cmd = [
228 | str(self._executable),
229 | "--solver",
230 | conf,
231 | "--allow-multiple-assignments",
232 | ] + [str(arg) for arg in args]
233 | minizinc.logger.debug(
234 | f'CLIDriver:run -> command: "{" ".join(cmd)}"'
235 | )
236 | output = subprocess.run(
237 | cmd,
238 | stdin=None,
239 | stdout=PIPE,
240 | stderr=PIPE,
241 | **windows_spawn_options,
242 | )
243 | if output.returncode != 0:
244 | # Error will (usually) be raised in json stream
245 | for _ in decode_json_stream(output.stdout):
246 | pass
247 | raise parse_error(output.stderr)
248 | return output
249 |
250 | async def _create_process(
251 | self, args: List[Union[str, Path]], solver: Optional[str] = None
252 | ) -> Process:
253 | """Start an asynchronous driver process with given arguments
254 |
255 | Args:
256 | args (List[str]): direct arguments to the driver
257 | solver (Union[str, Path, None]): Solver configuration string
258 | guaranteed by the user to be valid until the process has ended.
259 | """
260 |
261 | windows_spawn_options: Dict[str, Any] = {}
262 | if sys.platform == "win32":
263 | # See corresponding comment in run()
264 | windows_spawn_options = {
265 | "startupinfo": subprocess.STARTUPINFO(
266 | dwFlags=subprocess.STARTF_USESHOWWINDOW,
267 | wShowWindow=subprocess.SW_HIDE,
268 | ),
269 | "creationflags": subprocess.CREATE_NEW_CONSOLE,
270 | }
271 |
272 | args.append("--json-stream")
273 |
274 | if solver is None:
275 | minizinc.logger.debug(
276 | f"CLIDriver:create_process -> program: {str(self._executable)} "
277 | f'args: "--allow-multiple-assignments '
278 | f'{" ".join(str(arg) for arg in args)}"'
279 | )
280 | proc = await create_subprocess_exec(
281 | str(self._executable),
282 | "--allow-multiple-assignments",
283 | *[str(arg) for arg in args],
284 | stdin=None,
285 | stdout=PIPE,
286 | stderr=PIPE,
287 | **windows_spawn_options,
288 | )
289 | else:
290 | minizinc.logger.debug(
291 | f"CLIDriver:create_process -> program: {str(self._executable)} "
292 | f'args: "--solver {solver} --allow-multiple-assignments '
293 | f'{" ".join(str(arg) for arg in args)}"'
294 | )
295 | proc = await create_subprocess_exec(
296 | str(self._executable),
297 | "--solver",
298 | solver,
299 | "--allow-multiple-assignments",
300 | *[str(arg) for arg in args],
301 | stdin=None,
302 | stdout=PIPE,
303 | stderr=PIPE,
304 | **windows_spawn_options,
305 | )
306 | return proc
307 |
308 | @classmethod
309 | def find(
310 | cls, path: Optional[List[str]] = None, name: str = "minizinc"
311 | ) -> Optional["Driver"]:
312 | """Finds MiniZinc Driver on default or specified path.
313 |
314 | Find driver will look for the MiniZinc executable to create a Driver for
315 | MiniZinc Python. If no path is specified, then the paths given by the
316 | environment variables appended by MiniZinc's default locations will be tried.
317 |
318 | Args:
319 | path: List of locations to search.
320 | name: Name of the executable.
321 |
322 | Returns:
323 | Optional[Driver]: Returns a Driver object when found or None.
324 | """
325 | if path is None:
326 | path = os.environ.get("PATH", "").split(os.pathsep)
327 | # Add default MiniZinc locations to the path
328 | if platform.system() == "Darwin":
329 | path.extend(MAC_LOCATIONS)
330 | elif platform.system() == "Windows":
331 | path.extend(WIN_LOCATIONS)
332 |
333 | # Try to locate the MiniZinc executable
334 | executable = shutil.which(name, path=os.pathsep.join(path))
335 | if executable is not None:
336 | return cls(Path(executable))
337 | return None
338 |
--------------------------------------------------------------------------------
/src/minizinc/dzn.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | from pathlib import Path
6 | from typing import Union
7 |
8 | from lark import Lark, Transformer
9 |
10 | from minizinc.model import UnknownExpression
11 |
12 | dzn_grammar = r"""
13 | items: [item (";" item)* ";"?]
14 | item: ident "=" value | ident "=" unknown
15 | ident: /([A-Za-z][A-Za-z0-9_]*)|(\'[^\']*\')/
16 | value: array
17 | | array2d
18 | | set
19 | | int
20 | | float
21 | | string
22 | | "true" -> true
23 | | "false" -> false
24 | list: [value ("," value)* ","?]
25 | array: "[" list "]"
26 | array2d: "[" "|" [ list ("|" list)*] "|" "]"
27 | set: "{" list "}"
28 | | int ".." int
29 |
30 | int: /-?((0o[0-7]+)|(0x[0-9A-Fa-f]+)|(\d+))/
31 | float: /-?((\d+\.\d+[Ee][-+]?\d+)|(\d+[Ee][-+]?\d+)|(\d+\.\d+))/
32 | string: ESCAPED_STRING
33 |
34 | unknown: /[^[{;]+[^;]*/
35 |
36 | %import common.ESCAPED_STRING
37 | %import common.WS
38 | %ignore WS
39 | COMMENT: "%" /[^\n]/*
40 | %ignore COMMENT
41 | """
42 |
43 |
44 | def arg1_construct(cls):
45 | return lambda self, s: cls(s[0])
46 |
47 |
48 | class TreeToDZN(Transformer):
49 | @staticmethod
50 | def int(s):
51 | i = s[0]
52 | if i.startswith("0o") or i.startswith("-0o"):
53 | return int(i, 8)
54 | elif i.startswith("0x") or i.startswith("-0x"):
55 | return int(i, 16)
56 | else:
57 | return int(i)
58 |
59 | @staticmethod
60 | def item(s):
61 | return s[0], s[1]
62 |
63 | @staticmethod
64 | def array2d(s):
65 | return list(s)
66 |
67 | @staticmethod
68 | def set(s):
69 | if len(s) == 1:
70 | return set(s[0])
71 | else:
72 | return range(s[0], s[1] + 1)
73 |
74 | @staticmethod
75 | def string(s):
76 | return str(s[0][1:-1])
77 |
78 | @staticmethod
79 | def true(_):
80 | return True
81 |
82 | @staticmethod
83 | def false(_):
84 | return False
85 |
86 | items = dict
87 | unknown = arg1_construct(UnknownExpression)
88 | list = list
89 | array = arg1_construct(lambda i: i)
90 | ident = arg1_construct(str)
91 | float = arg1_construct(float)
92 | value = arg1_construct(lambda i: i)
93 |
94 |
95 | dzn_parser = Lark(dzn_grammar, start="items", parser="lalr")
96 |
97 |
98 | def parse_dzn(dzn: Union[Path, str]):
99 | if isinstance(dzn, Path):
100 | dzn = dzn.read_text()
101 | tree = dzn_parser.parse(dzn)
102 | dzn_dict = TreeToDZN().transform(tree)
103 | return dzn_dict
104 |
--------------------------------------------------------------------------------
/src/minizinc/error.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import re
6 | from dataclasses import dataclass
7 | from pathlib import Path
8 | from typing import Optional, Tuple
9 |
10 |
11 | @dataclass
12 | class Location:
13 | """Representation of a location within a file
14 |
15 | Attributes:
16 | file (Optional[Path]): Path to the file
17 | line (int): Line within the file (default: ``0``)
18 | columns (Tuple[int,int]): Columns on the line, from/to (default:
19 | ``(0, 0)``)
20 |
21 | """
22 |
23 | file: Optional[Path]
24 | lines: Tuple[int, int] = (0, 0)
25 | columns: Tuple[int, int] = (0, 0)
26 |
27 |
28 | class ConfigurationError(Exception):
29 | """Exception raised during the configuration of MiniZinc
30 |
31 | Attributes:
32 | message (str): Explanation of the error
33 | """
34 |
35 | message: str
36 |
37 |
38 | class MiniZincError(Exception):
39 | """Exception raised for errors caused by a MiniZinc Driver
40 |
41 | Attributes:
42 | location (Optional[Location]): File location of the error
43 | message (str): Explanation of the error
44 | """
45 |
46 | location: Optional[Location]
47 | message: str
48 |
49 | def __init__(self, location: Optional[Location] = None, message: str = ""):
50 | super().__init__(message)
51 | self.location = location
52 |
53 |
54 | class MiniZincWarning(Warning):
55 | """Warning created for warnings originating from a MiniZinc Driver"""
56 |
57 |
58 | class EvaluationError(MiniZincError):
59 | """Exception raised for errors due to an error during instance evaluation by
60 | the MiniZinc Driver"""
61 |
62 | pass
63 |
64 |
65 | class AssertionError(EvaluationError):
66 | """Exception raised for MiniZinc assertions that failed during instance
67 | evaluation"""
68 |
69 | pass
70 |
71 |
72 | class TypeError(MiniZincError):
73 | """Exception raised for type errors found in an MiniZinc Instance"""
74 |
75 | pass
76 |
77 |
78 | class IncludeError(MiniZincError):
79 | """Exception raised for type errors found in an MiniZinc Instance"""
80 |
81 | pass
82 |
83 |
84 | class CyclicIncludeError(MiniZincError):
85 | """Exception raised for type errors found in an MiniZinc Instance"""
86 |
87 | pass
88 |
89 |
90 | class SyntaxError(MiniZincError):
91 | """Exception raised for syntax errors found in an MiniZinc Instance"""
92 |
93 | pass
94 |
95 |
96 | def parse_error(error_txt: bytes) -> MiniZincError:
97 | """Parse error from bytes array (raw string)
98 |
99 | Parse error scans the output from a MiniZinc driver to generate the
100 | appropriate MiniZincError. It will make the distinction between different
101 | kinds of errors as found by MiniZinc and tries to parse the relevant
102 | information to the error. The different kinds of errors are represented by
103 | different sub-classes of MiniZincError.
104 |
105 | Args:
106 | error_txt (bytes): raw string containing a MiniZinc error. Generally
107 | this should be the error stream of a driver.
108 |
109 | Returns:
110 | An error generated from the string
111 |
112 | """
113 | error = MiniZincError
114 | if b"MiniZinc: evaluation error:" in error_txt:
115 | error = EvaluationError
116 | if b"Assertion failed:" in error_txt:
117 | error = AssertionError
118 | elif b"MiniZinc: type error:" in error_txt:
119 | error = TypeError
120 | elif b"Error: syntax error" in error_txt:
121 | error = SyntaxError
122 |
123 | location = None
124 | match = re.search(rb"([^\s]+):(\d+)(.(\d+)-(\d+))?:\s", error_txt)
125 | if match:
126 | columns = (0, 0)
127 | if match[3]:
128 | columns = (int(match[4].decode()), int(match[5].decode()))
129 | lines = (int(match[2].decode()), int(match[2].decode()))
130 | location = Location(Path(match[1].decode()), lines, columns)
131 |
132 | message = error_txt.decode().strip()
133 | if not message:
134 | message = (
135 | "MiniZinc stopped with a non-zero exit code, but did not output an "
136 | "error message. "
137 | )
138 | elif (
139 | location is not None
140 | and location.file is not None
141 | and location.file.exists()
142 | ):
143 | with location.file.open() as f:
144 | for _ in range(location.lines[0] - 2):
145 | f.readline()
146 | message += "\nFile fragment:\n"
147 | for nr in range(
148 | max(1, location.lines[0] - 1), location.lines[1] + 2
149 | ):
150 | line = f.readline()
151 | if line == "":
152 | break
153 | message += f"{nr}: {line.rstrip()}\n"
154 | diff = location.columns[1] - location.columns[0]
155 | if (
156 | (location.lines[0] == location.lines[1])
157 | and nr == location.lines[0]
158 | and diff > 0
159 | ):
160 | message += (
161 | " " * (len(str(nr)) + 2 + location.columns[0] - 1)
162 | + "^" * (diff + 1)
163 | + "\n"
164 | )
165 |
166 | return error(location, message)
167 |
168 |
169 | def error_from_stream_obj(obj):
170 | """Convert object from JSON stream into MiniZinc Python error
171 |
172 | Args:
173 | obj (Dict): Parsed JSON object from the ``--json-stream``
174 | mode of MiniZinc.
175 |
176 | Returns:
177 | An error generated from the object
178 |
179 | """
180 | assert obj["type"] == "error"
181 | error = MiniZincError
182 | if obj["what"] == "syntax error":
183 | error = SyntaxError
184 | elif obj["what"] == "type error":
185 | error = TypeError
186 | elif obj["what"] == "include error":
187 | error = IncludeError
188 | elif obj["what"] == "cyclic include error":
189 | error = CyclicIncludeError
190 | elif obj["what"] == "evaluation error":
191 | error = EvaluationError
192 | elif obj["what"] == "assertion failed":
193 | error = AssertionError
194 |
195 | location = None
196 | if "location" in obj:
197 | location = Location(
198 | obj["location"]["filename"],
199 | (obj["location"]["firstLine"], obj["location"]["lastLine"]),
200 | (obj["location"]["firstColumn"], obj["location"]["lastColumn"]),
201 | )
202 | # TODO: Process stack information
203 |
204 | message = (
205 | "MiniZinc stopped with a non-zero exit code, but did not output an "
206 | "error message. "
207 | )
208 | if "message" in obj:
209 | message = obj["message"]
210 | elif "cycle" in obj:
211 | message = " includes ".join(obj["cycle"])
212 |
213 | return error(location, message)
214 |
--------------------------------------------------------------------------------
/src/minizinc/helpers.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from dataclasses import asdict, is_dataclass
3 | from datetime import timedelta
4 | from typing import Any, Dict, Iterable, List, Optional, Sequence, Union
5 |
6 | import minizinc
7 |
8 | if sys.version_info >= (3, 8):
9 | from typing import Protocol
10 |
11 | class DataClass(Protocol):
12 | # Checking for this attribute is currently the most reliable way to
13 | # ascertain that something is a dataclass
14 | __dataclass_fields__: Dict
15 |
16 | else:
17 | DataClass = Any
18 |
19 |
20 | def check_result(
21 | model: minizinc.Model,
22 | result: minizinc.Result,
23 | solver: minizinc.Solver,
24 | solution_nrs: Optional[Sequence[int]] = None,
25 | ) -> bool:
26 | """Checks a result object for a model using the given solver.
27 |
28 | Check the correctness of the solving process using a (different) solver
29 | configuration. The solver configuration is now used to confirm is
30 | assignment of the variables is correct. By default only the last solution
31 | will be checked. A sequence of solution numbers can be provided to check
32 | multiple solutions.
33 |
34 | Args:
35 | model (Model): To model for which the solution was provided
36 | result (Result): The solution to be checked
37 | solver (Solver): The solver configuration used to check the
38 | solutions.
39 | solution_nrs: The index set of solutions to be checked. (default:
40 | ``-1``)
41 |
42 | Returns:
43 | bool: True if the given result object is correctly verified.
44 |
45 | """
46 | if solution_nrs is None:
47 | solution_nrs = [-1]
48 |
49 | solutions = (
50 | result.solution
51 | if isinstance(result.solution, list)
52 | else [result.solution]
53 | )
54 |
55 | for i in solution_nrs:
56 | sol = solutions[i]
57 | if not check_solution(model, sol, result.status, solver):
58 | return False
59 |
60 | return True
61 |
62 |
63 | class TimeoutError(Exception):
64 | """Exception raised for timeout errors (UNKNOWN status) when checking solutions"""
65 |
66 | pass
67 |
68 |
69 | def check_solution(
70 | model: minizinc.Model,
71 | solution: Union[DataClass, Dict[str, Any]],
72 | status: minizinc.Status,
73 | solver: minizinc.Solver,
74 | time_limit: Optional[timedelta] = timedelta(seconds=30),
75 | ) -> bool:
76 | """Checks a solution for a model using the given solver.
77 |
78 | Check the correctness of the solving process using a (different) solver
79 | configuration. A new model instance is created and will be assigned all
80 | available values from the given solution. The Instance.solve() method is
81 | then used to ensure that the same solution with the same expected status is
82 | reached. Note that this method will not check the optimality of a solution.
83 |
84 | Args:
85 | model (Model): The model for which the solution was provided.
86 | solution (Union[DataClass, Dict[str, Any]]): The solution to be checked.
87 | status (Status): The expected (compatible) MiniZinc status.
88 | solver (Solver): The solver configuration used to check the
89 | solution.
90 | time_limit (Optional(timedelta)): An optional time limit to check the
91 | solution.
92 |
93 | Returns:
94 | bool: True if the given solution are correctly verified.
95 |
96 | Raises:
97 | TimeoutError: the given time limit was exceeded.
98 | """
99 | instance = minizinc.Instance(solver, model)
100 | if is_dataclass(solution):
101 | solution = asdict(solution)
102 |
103 | assert isinstance(solution, dict)
104 | for k, v in solution.items():
105 | if k not in ("objective", "_output_item", "_checker"):
106 | instance[k] = v
107 | check = instance.solve(time_limit=time_limit)
108 |
109 | if check.status is minizinc.Status.UNKNOWN:
110 | raise TimeoutError(
111 | f"Solution checking failed because the checker exceeded the allotted time limit of {time_limit}"
112 | )
113 | elif status == check.status:
114 | return True
115 | return check.status in [
116 | minizinc.Status.SATISFIED,
117 | minizinc.Status.OPTIMAL_SOLUTION,
118 | ] and status in [
119 | minizinc.Status.SATISFIED,
120 | minizinc.Status.OPTIMAL_SOLUTION,
121 | minizinc.Status.ALL_SOLUTIONS,
122 | ]
123 |
124 |
125 | def _add_diversity_to_opt_model(
126 | inst: minizinc.Instance,
127 | obj_annots: Dict[str, Any],
128 | vars: List[Dict[str, Any]],
129 | sol_fix: Optional[Dict[str, Iterable]] = None,
130 | ):
131 | for var in vars:
132 | # Current and previous variables
133 | varname = var["name"]
134 | varprevname = var["prev_name"]
135 |
136 | # Add the 'previous solution variables'
137 | inst[varprevname] = []
138 |
139 | # Fix the solution to given once
140 | if sol_fix is not None:
141 | inst.add_string(
142 | f"constraint {varname} == {list(sol_fix[varname])};\n"
143 | )
144 |
145 | # Add the optimal objective.
146 | if obj_annots["sense"] != "0":
147 | obj_type = obj_annots["type"]
148 | inst.add_string(f"{obj_type}: div_orig_opt_objective :: output;\n")
149 | inst.add_string(
150 | f"constraint div_orig_opt_objective == {obj_annots['name']};\n"
151 | )
152 | if obj_annots["sense"] == "-1":
153 | inst.add_string(f"solve minimize {obj_annots['name']};\n")
154 | else:
155 | inst.add_string(f"solve maximize {obj_annots['name']};\n")
156 | else:
157 | inst.add_string("solve satisfy;\n")
158 |
159 | return inst
160 |
161 |
162 | def _add_diversity_to_div_model(
163 | inst: minizinc.Instance,
164 | vars: List[Dict[str, Any]],
165 | div_anns: Dict[str, Any],
166 | gap: Union[int, float],
167 | sols: Dict[str, Any],
168 | ):
169 | # Add the 'previous solution variables'
170 | for var in vars:
171 | # Current and previous variables
172 | varname = var["name"]
173 | varprevname = var["prev_name"]
174 | varprevisfloat = "float" in var["prev_type"]
175 |
176 | distfun = var["distance_function"]
177 | prevsols = sols[varprevname] + [sols[varname]]
178 | prevsol = (
179 | __round_elements(prevsols, 6) if varprevisfloat else prevsols
180 | ) # float values are rounded to six decimal places to avoid infeasibility due to decimal errors.
181 |
182 | # Add the previous solutions to the model code.
183 | inst[varprevname] = prevsol
184 |
185 | # Add the diversity distance measurement to the model code.
186 | dim = __num_dim(prevsols)
187 | dotdots = ", ".join([".." for _ in range(dim - 1)])
188 | varprevtype = "float" if "float" in var["prev_type"] else "int"
189 | inst.add_string(
190 | f"array [1..{len(prevsol)}] of var {varprevtype}: dist_{varname} :: output = [{distfun}({varname}, {varprevname}[sol,{dotdots}]) | sol in 1..{len(prevsol)}];\n"
191 | )
192 |
193 | # Add minimum distance to the diversity distance measurement in the model code
194 | if var["lb"] != "infinity":
195 | inst.add_string(
196 | f"constraint forall(sol in 1..{len(prevsol)})( dist_{varname}[sol] >= {var['lb']});"
197 | )
198 |
199 | # Add maximum distance to the diversity distance measurement in the model code
200 | if var["ub"] != "infinity":
201 | inst.add_string(
202 | f"constraint forall(sol in 1..{len(prevsol)})( dist_{varname}[sol] <= {var['ub']});"
203 | )
204 |
205 | obj_sense = div_anns["objective"]["sense"]
206 | aggregator = (
207 | div_anns["aggregator"] if div_anns["aggregator"] != "" else "sum"
208 | )
209 | combinator = (
210 | div_anns["combinator"] if div_anns["combinator"] != "" else "sum"
211 | )
212 |
213 | # Add the bound on the objective.
214 | if obj_sense == "-1":
215 | inst.add_string(f"constraint div_orig_objective <= {gap};\n")
216 | elif obj_sense == "1":
217 | inst.add_string(f"constraint div_orig_objective >= {gap};\n")
218 |
219 | # Add new objective: maximize diversity.
220 | div_combinator = ", ".join(
221 | [f"{var['coef']} * dist_{var['name']}[sol]" for var in vars]
222 | )
223 | dist_total = f"{aggregator}([{combinator}([{div_combinator}]) | sol in 1..{len(prevsol)}])"
224 | inst.add_string(f"solve maximize {dist_total};\n")
225 |
226 | return inst
227 |
228 |
229 | def __num_dim(x: List) -> int:
230 | i = 1
231 | while isinstance(x[0], list):
232 | i += 1
233 | x = x[0]
234 | return i
235 |
236 |
237 | def __round_elements(x: List, p: int) -> List:
238 | for i in range(len(x)):
239 | if isinstance(x[i], list):
240 | x[i] = __round_elements(x[i], p)
241 | elif isinstance(x[i], float):
242 | x[i] = round(x[i], p)
243 | return x
244 |
--------------------------------------------------------------------------------
/src/minizinc/json.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import asyncio
6 | import warnings
7 | from enum import Enum
8 | from json import JSONDecodeError, JSONDecoder, JSONEncoder, loads
9 | from types import ModuleType
10 | from typing import Optional
11 |
12 | from .error import MiniZincError, MiniZincWarning, error_from_stream_obj
13 | from .types import AnonEnum, ConstrEnum
14 |
15 | try:
16 | import numpy
17 | except ImportError:
18 | numpy: Optional[ModuleType] = None # type: ignore
19 |
20 |
21 | class MZNJSONEncoder(JSONEncoder):
22 | def default(self, o):
23 | if isinstance(o, Enum):
24 | return {"e": o.name}
25 | if isinstance(o, AnonEnum):
26 | return {"e": o.enumName, "i": o.value}
27 | if isinstance(o, ConstrEnum):
28 | return {"c": o.constructor, "e": o.argument}
29 | if isinstance(o, set) or isinstance(o, range):
30 | return {
31 | "set": [{"e": i.name} if isinstance(i, Enum) else i for i in o]
32 | }
33 | if numpy is not None:
34 | if isinstance(o, numpy.ndarray):
35 | return o.tolist()
36 | if isinstance(o, numpy.generic):
37 | return o.item()
38 | return super().default(o)
39 |
40 |
41 | class MZNJSONDecoder(JSONDecoder):
42 | def __init__(self, enum_map=None, *args, **kwargs):
43 | if enum_map is None:
44 | self.enum_map = {}
45 | else:
46 | self.enum_map = enum_map
47 | kwargs["object_hook"] = self.mzn_object_hook
48 | JSONDecoder.__init__(self, *args, **kwargs)
49 |
50 | def transform_enum_object(self, obj):
51 | # TODO: This probably is an enum, but could still be a record
52 | if "e" in obj:
53 | if len(obj) == 1:
54 | return self.enum_map.get(obj["e"], obj["e"])
55 | elif len(obj) == 2 and "c" in obj:
56 | return ConstrEnum(obj["c"], obj["e"])
57 | elif len(obj) == 2 and "i" in obj:
58 | return AnonEnum(obj["e"], obj["i"])
59 | return obj
60 |
61 | def mzn_object_hook(self, obj):
62 | if isinstance(obj, dict):
63 | if len(obj) == 1 and "set" in obj:
64 | li = []
65 | for item in obj["set"]:
66 | if isinstance(item, list):
67 | assert len(item) == 2
68 | li.extend(list(range(item[0], item[1] + 1)))
69 | elif isinstance(item, dict):
70 | li.append(self.transform_enum_object(item))
71 | else:
72 | li.append(item)
73 | return set(li)
74 | else:
75 | return self.transform_enum_object(obj)
76 | return obj
77 |
78 |
79 | def decode_json_stream(byte_stream: bytes, cls=None, **kw):
80 | for line in byte_stream.split(b"\n"):
81 | line = line.strip()
82 | if line != b"":
83 | try:
84 | obj = loads(line, cls=cls, **kw)
85 | except JSONDecodeError as e:
86 | raise MiniZincError(
87 | message=f"MiniZinc driver output a message that cannot be parsed as JSON:\n{repr(line)}"
88 | ) from e
89 | if obj["type"] == "warning" or (
90 | obj["type"] == "error" and obj["what"] == "warning"
91 | ):
92 | # TODO: stack trace and location
93 | warnings.warn(obj["message"], MiniZincWarning, stacklevel=1)
94 | elif obj["type"] == "error":
95 | raise error_from_stream_obj(obj)
96 | else:
97 | yield obj
98 |
99 |
100 | async def decode_async_json_stream(
101 | stream: asyncio.StreamReader, cls=None, **kw
102 | ):
103 | buffer: bytes = b""
104 | while not stream.at_eof():
105 | try:
106 | buffer += await stream.readuntil(b"\n")
107 | buffer = buffer.strip()
108 | if buffer == b"":
109 | continue
110 | try:
111 | obj = loads(buffer, cls=cls, **kw)
112 | except JSONDecodeError as e:
113 | raise MiniZincError(
114 | message=f"MiniZinc driver output a message that cannot be parsed as JSON:\n{repr(buffer)}"
115 | ) from e
116 | if obj["type"] == "warning" or (
117 | obj["type"] == "error" and obj["what"] == "warning"
118 | ):
119 | # TODO: stack trace and location
120 | warnings.warn(obj["message"], MiniZincWarning, stacklevel=1)
121 | elif obj["type"] == "error":
122 | raise error_from_stream_obj(obj)
123 | else:
124 | yield obj
125 | buffer = b""
126 | except asyncio.LimitOverrunError as err:
127 | buffer += await stream.readexactly(err.consumed)
128 |
--------------------------------------------------------------------------------
/src/minizinc/model.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import json
6 | import threading
7 | import warnings
8 | from enum import Enum, EnumMeta
9 | from pathlib import Path
10 | from typing import Any, Dict, List, Optional, Type, Union
11 |
12 | ParPath = Union[Path, str]
13 |
14 |
15 | class Method(Enum):
16 | """Enumeration that represents of a solving method.
17 |
18 | Attributes:
19 | SATISFY: Represents a satisfaction problem.
20 | MINIMIZE: Represents a minimization problem.
21 | MAXIMIZE: Represents a maximization problem.
22 | """
23 |
24 | SATISFY = 1
25 | MINIMIZE = 2
26 | MAXIMIZE = 3
27 |
28 | @classmethod
29 | def from_string(cls, s: str):
30 | """Get Method represented by the string s.
31 |
32 | Args:
33 | s: String expected to contain either "sat", "min", or "max".
34 |
35 | Returns:
36 | Method: Method represented by s
37 | """
38 | if s == "sat":
39 | return cls.SATISFY
40 | elif s == "min":
41 | return cls.MINIMIZE
42 | elif s == "max":
43 | return cls.MAXIMIZE
44 | else:
45 | raise ValueError(
46 | f"Unknown Method {s}, valid options are 'sat', 'min', or 'max'"
47 | )
48 |
49 |
50 | class UnknownExpression(str):
51 | pass
52 |
53 |
54 | class Model:
55 | """The representation of a MiniZinc model in Python
56 |
57 | Attributes:
58 | output_type (Type): the type used to store the solution values created
59 | in the process of solving the Instance. This attribute is
60 | particularly helpful when comparing the results of multiple
61 | instances together. The type must support initialisation with the
62 | assignments returned by MiniZinc. These assignments currently
63 | always include "__output_item" and include "objective" if the
64 | instance is not a satisfaction problem.
65 |
66 | Raises:
67 | MiniZincError: when an error occurs during the parsing or
68 | type checking of the model object.
69 | """
70 |
71 | output_type: Optional[Type] = None
72 |
73 | _code_fragments: List[str]
74 | _data: Dict[str, Any]
75 | _enum_map: Dict[str, Enum]
76 | _includes: List[Path]
77 | _lock: threading.Lock
78 | _checker: bool = False
79 |
80 | def __init__(self, files: Optional[Union[ParPath, List[ParPath]]] = None):
81 | self._data = {}
82 | self._includes = []
83 | self._code_fragments = []
84 | self._enum_map = {}
85 | self._lock = threading.Lock()
86 | if isinstance(files, Path) or isinstance(files, str):
87 | self._add_file(files)
88 | elif files is not None:
89 | for file in files:
90 | self._add_file(file)
91 |
92 | def __setitem__(self, key: str, value: Any):
93 | """Set parameter of Model.
94 |
95 | This method overrides the default implementation of item access
96 | (``obj[key] = value``) for models. Item access on a Model can be used to
97 | set parameters of the Model.
98 |
99 | Args:
100 | key (str): Identifier of the parameter.
101 | value (Any): Value to be assigned to the parameter.
102 | """
103 | with self._lock:
104 | if self._data.get(key, None) is None:
105 | if isinstance(value, EnumMeta):
106 | self._register_enum_values(value)
107 | self._data.__setitem__(key, value)
108 | else:
109 | if self._data[key] != value:
110 | # TODO: Fix the error type and document
111 | raise AssertionError(
112 | f"The parameter '{key}' cannot be assigned multiple values. "
113 | f"If you are changing the model, consider using the branch "
114 | f"method before assigning the parameter."
115 | )
116 |
117 | def _register_enum_values(self, t: EnumMeta):
118 | for name in t.__members__:
119 | if name in self._enum_map:
120 | # TODO: Fix the error type and document
121 | raise AssertionError(
122 | f"Identifier '{name}' is used in multiple enumerated types"
123 | f"within the same model. Identifiers in enumerated types "
124 | f"have to be unique."
125 | )
126 | self._enum_map[name] = t.__members__[name]
127 |
128 | def __getitem__(self, key: str) -> Any:
129 | """Get parameter of Model.
130 |
131 | This method overrides the default implementation of item access
132 | (``obj[key]``) for models. Item access on a Model can be used to get
133 | parameters of the Model.
134 |
135 | Args:
136 | key (str): Identifier of the parameter.
137 |
138 | Returns:
139 | The value assigned to the parameter.
140 |
141 | Raises:
142 | KeyError: The parameter you are trying to access is not known.
143 |
144 | """
145 | return self._data.__getitem__(key)
146 |
147 | def add_file(self, file: ParPath, parse_data: bool = False) -> None:
148 | """Adds a MiniZinc file (``.mzn``, ``.dzn``, or ``.json``) to the Model.
149 |
150 | Args:
151 | file (Union[Path, str]): Path to the file to be added
152 | parse_data (bool): Signal if the data should be parsed for usage
153 | within Python. This option is ignored if the extra `dzn` is
154 | not enabled.
155 | Raises:
156 | MiniZincError: when an error occurs during the parsing or
157 | type checking of the model object.
158 | """
159 | return self._add_file(file, parse_data)
160 |
161 | def _add_file(self, file: ParPath, parse_data: bool = False) -> None:
162 | if not isinstance(file, Path):
163 | file = Path(file)
164 | assert file.exists()
165 | if not parse_data:
166 | with self._lock:
167 | self._includes.append(file)
168 | return
169 | if file.suffix == ".json":
170 | data = json.load(file.open())
171 | for k, v in data.items():
172 | self.__setitem__(k, v)
173 | elif file.suffix == ".dzn" and parse_data:
174 | try:
175 | from lark.exceptions import LarkError
176 |
177 | from .dzn import parse_dzn
178 |
179 | try:
180 | data = parse_dzn(file)
181 | for k, v in data.items():
182 | self.__setitem__(k, v)
183 | except LarkError:
184 | warnings.warn(
185 | f"Could not parse {file}. Parameters included within this file "
186 | f"are not available in Python",
187 | stacklevel=1,
188 | )
189 | with self._lock:
190 | self._includes.append(file)
191 | except ImportError:
192 | with self._lock:
193 | self._includes.append(file)
194 | elif file.suffix not in [".dzn", ".mzn", ".mzc"]:
195 | raise NameError("Unknown file suffix %s", file.suffix)
196 | else:
197 | with self._lock:
198 | if ".mzc" in file.suffixes:
199 | self._checker = True
200 | self._includes.append(file)
201 |
202 | def add_string(self, code: str) -> None:
203 | """Adds a string of MiniZinc code to the Model.
204 |
205 | Args:
206 | code (str): A string contain MiniZinc code
207 | Raises:
208 | MiniZincError: when an error occurs during the parsing or
209 | type checking of the model object.
210 | """
211 | with self._lock:
212 | self._code_fragments.append(code)
213 |
214 | def __copy__(self):
215 | copy = self.__class__()
216 | copy._includes = self._includes[:]
217 | copy._code_fragments = self._code_fragments[:]
218 | copy._data = dict.copy(self._data)
219 | return copy
220 |
--------------------------------------------------------------------------------
/src/minizinc/result.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | from dataclasses import dataclass
6 | from datetime import timedelta
7 | from enum import Enum, auto
8 | from typing import Any, Dict, Optional, Union
9 |
10 | from .model import Method
11 |
12 | StatisticsType = Union[float, int, str, timedelta]
13 |
14 | StdStatisticTypes = {
15 | # Number of search nodes
16 | "nodes": int,
17 | # Number of leaf nodes that were failed
18 | "failures": int,
19 | # Number of times the solver restarted the search
20 | "restarts": int,
21 | # Number of variables
22 | "variables": int,
23 | # Number of integer variables created by the solver
24 | "intVariables": int,
25 | # Number of Boolean variables created by the solver
26 | "boolVariables": int,
27 | # Number of floating point variables created by the solver
28 | "floatVariables": int,
29 | # Number of set variables created by the solver
30 | "setVariables": int,
31 | # Number of propagators created by the solver
32 | "propagators": int,
33 | # Number of propagator invocations
34 | "propagations": int,
35 | # Peak depth of search tree
36 | "peakDepth": int,
37 | # Number of nogoods created
38 | "nogoods": int,
39 | # Number of backjumps
40 | "backjumps": int,
41 | # Peak memory (in Mbytes)
42 | "peakMem": float,
43 | # Initialisation time
44 | "initTime": timedelta,
45 | # Solving time
46 | "solveTime": timedelta,
47 | # Flattening time
48 | "flatTime": timedelta,
49 | # Number of paths generated
50 | "paths": int,
51 | # Number of Boolean variables in the flat model
52 | "flatBoolVars": int,
53 | # Number of floating point variables in the flat model
54 | "flatFloatVars": int,
55 | # Number of integer variables in the flat model
56 | "flatIntVars": int,
57 | # Number of set variables in the flat model
58 | "flatSetVars": int,
59 | # Number of Boolean constraints in the flat model
60 | "flatBoolConstraints": int,
61 | # Number of floating point constraints in the flat model
62 | "flatFloatConstraints": int,
63 | # Number of integer constraints in the flat model
64 | "flatIntConstraints": int,
65 | # Number of set constraints in the flat model
66 | "flatSetConstraints": int,
67 | # Optimisation method in the Flat Model
68 | "method": str,
69 | # Number of reified constraints evaluated during flattening
70 | "evaluatedReifiedConstraints": int,
71 | # Number of half-reified constraints evaluated during flattening
72 | "evaluatedHalfReifiedConstraints": int,
73 | # Number of implications removed through chain compression
74 | "eliminatedImplications": int,
75 | # Number of linear constraints removed through chain compression
76 | "eliminatedLinearConstraints": int,
77 | }
78 |
79 |
80 | def set_stat(stats: Dict[str, StatisticsType], name: str, value: str):
81 | """Set statistical value in the result object.
82 |
83 | Set solving statiscal value within the result object. This value is
84 | converted from string to the appropriate type as suggested by the
85 | statistical documentation in MiniZinc. Timing statistics, expressed through
86 | floating point numbers in MiniZinc, will be converted to timedelta objects.
87 |
88 | Args:
89 | stats: The dictionary to be extended with the new statistical value
90 | name: The name under which the statistical value will be stored.
91 | value: The value for the statistical value to be converted and stored.
92 |
93 | """
94 | value = value.strip('"')
95 | tt = StdStatisticTypes.get(name, None)
96 | if tt is None and ("time" in name or "Time" in name):
97 | tt = timedelta
98 | try:
99 | if tt is timedelta:
100 | time_us = int(float(value) * 1000000)
101 | stats[name] = timedelta(microseconds=time_us)
102 | elif tt is not None:
103 | stats[name] = tt(value)
104 | else:
105 | try:
106 | stats[name] = int(value)
107 | except ValueError:
108 | stats[name] = float(value)
109 | except ValueError:
110 | stats[name] = value
111 |
112 |
113 | class Status(Enum):
114 | """Enumeration to represent the status of the solving process.
115 |
116 | Attributes:
117 | ERROR: An error occurred during the solving process.
118 | UNKNOWN: No solutions have been found and search has terminated without
119 | exploring the whole search space.
120 | UNBOUNDED: The objective of the optimisation problem is unbounded.
121 | UNSATISFIABLE: No solutions have been found and the whole search space
122 | was explored.
123 | SATISFIED: A solution was found, but possibly not the whole search
124 | space was explored.
125 | ALL_SOLUTIONS: All solutions in the search space have been found.
126 | OPTIMAL_SOLUTION: A solution has been found that is optimal according
127 | to the objective.
128 |
129 | """
130 |
131 | ERROR = auto()
132 | UNKNOWN = auto()
133 | UNBOUNDED = auto()
134 | UNSATISFIABLE = auto()
135 | SATISFIED = auto()
136 | ALL_SOLUTIONS = auto()
137 | OPTIMAL_SOLUTION = auto()
138 |
139 | @classmethod
140 | def from_output(cls, output: bytes, method: Method):
141 | """Determines the solving status from the output of a MiniZinc process
142 |
143 | Determines the solving status according to the standard status output
144 | strings defined by MiniZinc. The output of the MiniZinc will be scanned
145 | in a defined order to best determine the status of the solving process.
146 | UNKNOWN will be returned if no status can be determined.
147 |
148 | Args:
149 | output (bytes): the standard output of a MiniZinc process.
150 | method (Method): the objective method used in the optimisation
151 | problem.
152 |
153 | Returns:
154 | Optional[Status]: Status that could be determined from the output.
155 |
156 | """
157 | s = None
158 | if b"=====ERROR=====" in output:
159 | s = cls.ERROR
160 | elif b"=====UNKNOWN=====" in output:
161 | s = cls.UNKNOWN
162 | elif b"=====UNSATISFIABLE=====" in output:
163 | s = cls.UNSATISFIABLE
164 | elif (
165 | b"=====UNSATorUNBOUNDED=====" in output
166 | or b"=====UNBOUNDED=====" in output
167 | ):
168 | s = cls.UNBOUNDED
169 | elif method is Method.SATISFY:
170 | if b"==========" in output:
171 | s = cls.ALL_SOLUTIONS
172 | elif b"----------" in output:
173 | s = cls.SATISFIED
174 | else:
175 | if b"==========" in output:
176 | s = cls.OPTIMAL_SOLUTION
177 | elif b"----------" in output:
178 | s = cls.SATISFIED
179 | return s
180 |
181 | @classmethod
182 | def from_str(cls, status: str):
183 | s = None
184 | if status == "ERROR":
185 | s = cls.ERROR
186 | elif status == "UNKNOWN":
187 | s = cls.UNKNOWN
188 | elif status == "UNBOUNDED" or status == "UNSAT_OR_UNBOUNDED":
189 | s = cls.UNBOUNDED
190 | elif status == "UNSATISFIABLE":
191 | s = cls.UNSATISFIABLE
192 | elif status == "SATISFIED":
193 | s = cls.SATISFIED
194 | elif status == "ALL_SOLUTIONS":
195 | s = cls.ALL_SOLUTIONS
196 | elif status == "OPTIMAL_SOLUTION":
197 | s = cls.OPTIMAL_SOLUTION
198 | return s
199 |
200 | def __str__(self):
201 | return self.name
202 |
203 | def has_solution(self) -> bool:
204 | """Returns true if the status suggest that a solution has been found."""
205 | if self in [self.SATISFIED, self.ALL_SOLUTIONS, self.OPTIMAL_SOLUTION]:
206 | return True
207 | return False
208 |
209 |
210 | @dataclass
211 | class Result:
212 | """Representation of a MiniZinc solution in Python
213 |
214 | Attributes:
215 | status (Status): The solving status of the MiniZinc instance
216 | solution (Any): Variable assignments
217 | made to form the solution
218 | statistics (Dict[str, Union[float, int, timedelta]]): Statistical
219 | information generated during the search for the Solution
220 |
221 | """
222 |
223 | status: Status
224 | solution: Any
225 | statistics: Dict[str, Union[float, int, timedelta]]
226 |
227 | @property
228 | def objective(self) -> Optional[Union[int, float]]:
229 | """Returns objective of the solution
230 |
231 | Returns the objective of the solution when possible. If no solutions
232 | have been found or the problem did not have an objective, then None is
233 | returned instead.
234 |
235 | Returns:
236 | Optional[Union[int, float]]: best objective found or None
237 |
238 | """
239 | if self.solution is not None:
240 | if isinstance(self.solution, list):
241 | return getattr(self.solution[-1], "objective", None)
242 | else:
243 | return getattr(self.solution, "objective", None)
244 | else:
245 | return None
246 |
247 | def __getitem__(self, key):
248 | """Retrieves solution or a member of a solution.
249 |
250 | Overrides the default implementation of item access (obj[key]) to
251 | retrieve a solution object or member of a solution from the result
252 | object.
253 |
254 | - If the Result object does not contain any solutions, then a
255 | KeyError will always be raised.
256 | - If the Result object contains a single solutions, then the names of a
257 | variable can be used in this method to retrieve its value in the
258 | solution.
259 | - If the Result object contains multiple solutions, then a single
260 | integer can be used to retrieve the solution object or a tuple of an
261 | integer and the name of a variable can be used to retrieve the value
262 | of that variable in the numbered solution object.
263 |
264 | Args:
265 | key: solution number or name of the solution member.
266 |
267 | Returns:
268 | Solution object or the value of the member in the solution.
269 |
270 | Raises:
271 | KeyError: No solution was found, solution number is out of range,
272 | or no solution member with this name exists.
273 |
274 | """
275 | try:
276 | if self.solution is not None:
277 | if isinstance(self.solution, list):
278 | if isinstance(key, tuple):
279 | return getattr(
280 | self.solution.__getitem__(key[0]), key[1]
281 | )
282 | else:
283 | return self.solution.__getitem__(key)
284 | else:
285 | return getattr(self.solution, key)
286 | else:
287 | raise KeyError
288 | except AttributeError:
289 | raise KeyError from None
290 |
291 | def __len__(self):
292 | """Returns the number of solutions included in the Result object
293 |
294 | Returns:
295 | int: number of solution that can be accessed
296 |
297 | """
298 | if self.solution is None:
299 | return 0
300 | elif isinstance(self.solution, list):
301 | return len(self.solution)
302 | else:
303 | return 1
304 |
305 | def __str__(self):
306 | return str(self.solution)
307 |
--------------------------------------------------------------------------------
/src/minizinc/solver.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import contextlib
6 | import json
7 | import os
8 | import tempfile
9 | from dataclasses import dataclass, field
10 | from pathlib import Path
11 | from typing import Iterator, List, Optional, Tuple
12 |
13 | import minizinc
14 |
15 |
16 | @dataclass
17 | class Solver:
18 | """The representation of a MiniZinc solver configuration in MiniZinc Python.
19 |
20 | Attributes:
21 | name (str): The name of the solver.
22 | version (str): The version of the solver.
23 | id (str): A unique identifier for the solver, “reverse domain name”
24 | notation.
25 | executable (Optional[str]): The executable for this solver that can run
26 | FlatZinc files. This can be just a file name (in which case the
27 | solver has to be on the current ``$PATH``), or an absolute path to
28 | the executable, or a relative path (which is interpreted relative
29 | to the location of the configuration file). This attribute is set
30 | to ``None`` if the solver is integrated into MiniZinc.
31 | mznlib (str): The solver-specific library of global constraints and
32 | redefinitions. This should be the name of a directory (either an
33 | absolute path or a relative path, interpreted relative to the
34 | location of the configuration file). For solvers whose libraries
35 | are installed in the same location as the MiniZinc standard
36 | library, this can also take the form -G, e.g., -Ggecode (this is
37 | mostly the case for solvers that ship with the MiniZinc binary
38 | distribution).
39 | mznlibVersion (int): *Currently undocumented in the MiniZinc
40 | documentation.*
41 | description (str): *Currently undocumented in the MiniZinc
42 | documentation.*
43 | tags (List[str]): Each solver can have one or more tags that describe
44 | its features in an abstract way. Tags can be used for selecting a
45 | solver using the --solver option. There is no fixed list of tags,
46 | however we recommend using the following tags if they match the
47 | solver’s behaviour:
48 | - ``cp``: for Constraint Programming solvers
49 | - ``mip``: for Mixed Integer Programming solvers
50 | - ``float``: for solvers that support float variables
51 | - ``api``: for solvers that use the internal C++ API
52 | stdFlags (List[str]): Which of the standard solver command line flags
53 | are supported by this solver. The standard flags are ``-a``,
54 | ``-n``, ``-s``, ``-v``, ``-p``, ``-r``, ``-f``.
55 | extraFlags (List[Tuple[str,str,str,str]]): Extra command line flags
56 | supported by the solver. Each entry must be a tuple of four
57 | strings. The first string is the name of the option (e.g.
58 | ``--special-algorithm``). The second string is a description that
59 | can be used to generate help output (e.g. "which special algorithm
60 | to use"). The third string specifies the type of the argument
61 | (``int``, ``bool``, ``float`` or ``string``). The fourth string is
62 | the default value.
63 | requiredFlags (List[str]): *Currently undocumented in the MiniZinc
64 | documentation.*
65 | supportsMzn (bool): Whether the solver can run MiniZinc directly (i.e.,
66 | it implements its own compilation or interpretation of the model).
67 | supportsFzn (bool): Whether the solver can run FlatZinc. This should be
68 | the case for most solvers.
69 | supportsNL (bool): Whether the solver conforms to the AMPL NL standard.
70 | The NL format is used if ``supportsFZN`` is ``False``.
71 | needsSolns2Out (bool): Whether the output of the solver needs to be
72 | passed through the MiniZinc output processor.
73 | needsMznExecutable (bool): Whether the solver needs to know the
74 | location of the MiniZinc executable. If true, it will be passed to
75 | the solver using the ``mzn-executable`` option.
76 | needsStdlibDir (bool): Whether the solver needs to know the location of
77 | the MiniZinc standard library directory. If true, it will be passed
78 | to the solver using the ``stdlib-dir`` option.
79 | needsPathsFile (bool): *Currently undocumented in the MiniZinc
80 | documentation.*
81 | isGUIApplication (bool): Whether the solver has its own graphical user
82 | interface, which means that MiniZinc will detach from the process
83 | and not wait for it to finish or to produce any output.
84 | _identifier (Optional[str]): A string to specify the solver to MiniZinc
85 | driver. If set to None, then a solver configuration file should be
86 | generated.
87 | """
88 |
89 | name: str
90 | version: str
91 | id: str
92 | executable: Optional[str] = None
93 | mznlib: str = ""
94 | mznlibVersion: int = 1
95 | description: str = ""
96 | tags: List[str] = field(default_factory=list)
97 | stdFlags: List[str] = field(default_factory=list)
98 | extraFlags: List[Tuple[str, str, str, str]] = field(default_factory=list)
99 | requiredFlags: List[str] = field(default_factory=list)
100 | inputType: str = "FZN"
101 | supportsMzn: bool = False
102 | supportsFzn: bool = True
103 | supportsNL: bool = False
104 | needsSolns2Out: bool = False
105 | needsMznExecutable: bool = False
106 | needsStdlibDir: bool = False
107 | needsPathsFile: bool = False
108 | isGUIApplication: bool = False
109 | _identifier: Optional[str] = None
110 |
111 | @classmethod
112 | def lookup(cls, tag: str, driver=None, refresh=False):
113 | """Lookup a solver configuration in the driver registry.
114 |
115 | Access the MiniZinc driver's known solver configuration and find the
116 | configuation matching the given tag. Tags are matched in similar to
117 | ``minizinc --solver tag``. The order of solver configuration attributes
118 | that are considered is: full id, id ending, tags.
119 |
120 | Args:
121 | tag (str): tag (or id) of a solver configuration to look up.
122 | driver (Driver): driver which registry will be searched for the
123 | solver. If set to None, then ``default_driver`` will be used.
124 | refresh (bool): Forces the driver to refresh the cache of available solvers.
125 |
126 | Returns:
127 | Solver: MiniZinc solver configuration compatible with the driver.
128 |
129 | Raises:
130 | LookupError: No configuration could be located with the given tag.
131 |
132 | """
133 | if driver is None:
134 | driver = minizinc.default_driver
135 | assert driver is not None
136 |
137 | tag_map = driver.available_solvers(refresh)
138 |
139 | if tag not in tag_map or len(tag_map[tag]) < 1:
140 | raise LookupError(
141 | f"No solver id or tag '{tag}' found, available options: "
142 | f"{sorted(tag_map.keys())}"
143 | )
144 |
145 | return tag_map[tag][0]
146 |
147 | @classmethod
148 | def load(cls, path: Path):
149 | """Loads a solver configuration from a file.
150 |
151 | Load solver configuration from a MiniZinc solver configuration given by
152 | the file on the given location.
153 |
154 | Args:
155 | path (str): location to the solver configuration file to be loaded.
156 |
157 | Returns:
158 | Solver: MiniZinc solver configuration compatible with the driver.
159 |
160 | Raises:
161 | FileNotFoundError: Solver configuration file not found.
162 | ValueError: File contains an invalid solver configuration.
163 | """
164 | if not path.exists():
165 | raise FileNotFoundError
166 | solver = json.loads(path.read_bytes())
167 | # Resolve relative paths
168 | for key in ["executable", "mznlib"]:
169 | if key in solver:
170 | p = Path(solver[key])
171 | if not p.is_absolute():
172 | p = path.parent / p
173 | if p.exists():
174 | solver[key] = str(p.resolve())
175 |
176 | solver = cls(**solver)
177 | solver._identifier = str(path.resolve())
178 | return solver
179 |
180 | @contextlib.contextmanager
181 | def configuration(self) -> Iterator[str]:
182 | """Gives the identifier for the current solver configuration.
183 |
184 | Gives an identifier argument that can be used by a CLIDriver to
185 | identify the solver configuration. If the configuration was loaded
186 | using the driver and is thus already known, then the identifier will be
187 | yielded. If the configuration was changed or started from scratch, the
188 | configuration will be saved to a file and it will yield the name of the
189 | file.
190 |
191 | Yields:
192 | str: solver identifier to be used for the ``--solver `` flag.
193 |
194 | """
195 | try:
196 | file = None
197 | if self._identifier is not None:
198 | yield self._identifier
199 | else:
200 | file = tempfile.NamedTemporaryFile(
201 | prefix="minizinc_solver_", suffix=".msc", delete=False
202 | )
203 | file.write(self.output_configuration().encode())
204 | file.close()
205 | yield file.name
206 | finally:
207 | if file is not None:
208 | os.remove(file.name)
209 |
210 | def output_configuration(self) -> str:
211 | """Formulates a valid JSON specification for the Solver
212 |
213 | Formulates a JSON specification of the solver configuration meant to be
214 | used by MiniZinc. When stored in a ``.msc`` file it can be used
215 | directly as a argument to the ``--solver`` flag or stored on the
216 | MiniZinc solver configuration path. In the latter case it will be
217 | usable directly from the executable and visible when in ``minizinc
218 | --solvers``.
219 |
220 | Returns:
221 | str: JSON string containing the solver specification that can be
222 | read by MiniZinc
223 |
224 | """
225 | return json.dumps(
226 | {
227 | # TODO: Output inputType flag when fully supported
228 | "name": self.name,
229 | "version": self.version,
230 | "id": self.id,
231 | "executable": self.executable,
232 | "mznlib": self.mznlib,
233 | "tags": self.tags,
234 | "stdFlags": self.stdFlags,
235 | "extraFlags": self.extraFlags,
236 | "supportsMzn": self.supportsMzn,
237 | "supportsFzn": self.supportsFzn,
238 | "needsSolns2Out": self.needsSolns2Out,
239 | "needsMznExecutable": self.needsMznExecutable,
240 | "needsStdlibDir": self.needsStdlibDir,
241 | "isGUIApplication": self.isGUIApplication,
242 | },
243 | sort_keys=True,
244 | indent=4,
245 | )
246 |
247 | def __setattr__(self, key, value):
248 | if (
249 | key
250 | in [
251 | "version",
252 | "executable",
253 | "mznlib",
254 | "tags",
255 | "stdFlags",
256 | "extraFlags",
257 | "inputType",
258 | "supportsMzn",
259 | "supportsFzn",
260 | "needsSolns2Out",
261 | "needsMznExecutable",
262 | "needsStdlibDir",
263 | "isGUIApplication",
264 | ]
265 | and getattr(self, key, None) is not value
266 | ):
267 | self._identifier = None
268 | return super().__setattr__(key, value)
269 |
--------------------------------------------------------------------------------
/src/minizinc/types.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | from dataclasses import dataclass
6 | from typing import Any
7 |
8 |
9 | @dataclass(frozen=True)
10 | class AnonEnum:
11 | """Representation of anonymous enumeration values in MiniZinc"""
12 |
13 | enumName: str
14 | value: int
15 |
16 | def __str__(self):
17 | return f"to_enum({self.enumName},{self.value})"
18 |
19 |
20 | @dataclass(frozen=True)
21 | class ConstrEnum:
22 | """Representation of constructor function enumerated values in MiniZinc"""
23 |
24 | constructor: str
25 | argument: Any
26 |
27 | def __str__(self):
28 | return f"{self.constructor}({self.argument})"
29 |
--------------------------------------------------------------------------------
/tests/support.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import unittest
6 | from typing import ClassVar
7 |
8 | from minizinc import Instance, Solver
9 |
10 |
11 | class InstanceTestCase(unittest.TestCase):
12 | code = ""
13 | instance: Instance
14 | solver: ClassVar[Solver]
15 |
16 | @classmethod
17 | def setUpClass(cls):
18 | cls.solver = Solver.lookup("gecode")
19 |
20 | def setUp(self):
21 | self.instance = Instance(self.solver)
22 | self.instance.add_string(self.code)
23 |
--------------------------------------------------------------------------------
/tests/test_dzn.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import pytest
6 |
7 | from minizinc.model import UnknownExpression
8 |
9 | lark = pytest.importorskip("lark")
10 | from minizinc.dzn import parse_dzn # noqa: E402
11 |
12 |
13 | def test_dzn_empty():
14 | assert parse_dzn("") == {}
15 |
16 |
17 | def test_dzn_int():
18 | assert parse_dzn("x = 0") == {"x": 0}
19 | assert parse_dzn("x = -10") == {"x": -10}
20 | assert parse_dzn("x = 2123") == {"x": 2123}
21 | assert parse_dzn("x = 0xFF") == {"x": 255}
22 | assert parse_dzn("x = -0x100") == {"x": -256}
23 | assert parse_dzn("x = 0x0") == {"x": 0}
24 | assert parse_dzn("x = 0o23") == {"x": 19}
25 | assert parse_dzn("x = -0o30") == {"x": -24}
26 | assert parse_dzn("x = 0o0") == {"x": 0}
27 |
28 |
29 | def test_dzn_float():
30 | assert parse_dzn("x = 0.0") == {"x": 0.0}
31 | assert parse_dzn("x = 0.125") == {"x": 0.125}
32 | assert parse_dzn("x = -0.99") == {"x": -0.99}
33 | assert parse_dzn("x = 3.8e10") == {"x": 3.8e10}
34 | assert parse_dzn("x = -1.4e7") == {"x": -1.4e7}
35 | assert parse_dzn("x = 2.45E-3") == {"x": 2.45e-3}
36 | assert parse_dzn("x = -1.33e-2") == {"x": -1.33e-2}
37 | assert parse_dzn("x = 4e12") == {"x": 4e12}
38 | assert parse_dzn("x = -3E10") == {"x": -3e10}
39 | assert parse_dzn("x = 2e-110") == {"x": 2e-110}
40 | assert parse_dzn("x = -9e-124") == {"x": -9e-124}
41 |
42 |
43 | def test_dzn_string():
44 | assert parse_dzn('x = ""') == {"x": ""}
45 | assert parse_dzn('x = "test string"') == {"x": "test string"}
46 | assert parse_dzn('x = "ކ"') == {"x": "ކ"}
47 | assert parse_dzn('x = "🐛"') == {"x": "🐛"}
48 |
49 |
50 | def test_dzn_set():
51 | # Set literals
52 | assert parse_dzn("x = {}") == {"x": set()}
53 | assert parse_dzn("x = {1}") == {"x": {1}}
54 | assert parse_dzn("x = {1,2,3}") == {"x": {1, 2, 3}}
55 | assert parse_dzn("x = {1,1,2}") == {"x": {1, 2}}
56 | assert parse_dzn("x = {1.2,2.1}") == {"x": {1.2, 2.1}}
57 |
58 | # Set Ranges
59 | # note: upper range limit is exclusive in Python
60 | assert parse_dzn("x = 1..1") == {"x": range(1, 2)}
61 | assert set(parse_dzn("x = 1..1")["x"]) == {1}
62 | assert parse_dzn("x = 1..3") == {"x": range(1, 4)}
63 | assert set(parse_dzn("x = 1..3")["x"]) == {1, 2, 3}
64 |
65 |
66 | def test_dzn_array():
67 | assert parse_dzn("x = []") == {"x": []}
68 | assert parse_dzn("x = [1]") == {"x": [1]}
69 | assert parse_dzn("x = [1,2,3,4]") == {"x": [1, 2, 3, 4]}
70 | assert parse_dzn("x = [2.1]") == {"x": [2.1]}
71 | assert parse_dzn("x = [2.1,3.2,4.2]") == {"x": [2.1, 3.2, 4.2]}
72 | assert parse_dzn('x = ["str1", "str2"]') == {"x": ["str1", "str2"]}
73 | assert parse_dzn("x = [{1,2,3}, {1,2}]") == {"x": [{1, 2, 3}, {1, 2}]}
74 |
75 |
76 | def test_dzn_array2d():
77 | assert parse_dzn("x = [||]") == {"x": []}
78 | assert parse_dzn("x = [|1|2|3|]") == {"x": [[1], [2], [3]]}
79 | assert parse_dzn("x = [|1,4|2,5|3,6|]") == {"x": [[1, 4], [2, 5], [3, 6]]}
80 | assert parse_dzn("x = [|1.1,4.4|2.2,5.5|3.3,6.6|]") == {
81 | "x": [[1.1, 4.4], [2.2, 5.5], [3.3, 6.6]]
82 | }
83 |
84 |
85 | def test_dzn_unknown():
86 | assert parse_dzn("x = array2d(index1,1..2,[4, 3, 4, 5, 3, 6]);") == {
87 | "x": UnknownExpression("array2d(index1,1..2,[4, 3, 4, 5, 3, 6])")
88 | }
89 |
90 |
91 | def test_dzn_semicolon():
92 | assert parse_dzn("x = 1;") == {"x": 1}
93 | assert parse_dzn("x = 1") == {"x": 1}
94 | assert parse_dzn("x = 1; y = 2") == {"x": 1, "y": 2}
95 | assert parse_dzn("x = 1; y = 2;") == {"x": 1, "y": 2}
96 | assert parse_dzn('x = -20; y = 2e3; z = "string"') == {
97 | "x": -20,
98 | "y": 2e3,
99 | "z": "string",
100 | }
101 |
102 |
103 | def test_dzn_trailing_comma():
104 | assert parse_dzn("x = [1,2,3,]") == {"x": [1, 2, 3]}
105 | assert parse_dzn("x = {1,2,3,}") == {"x": {1, 2, 3}}
106 | assert parse_dzn("x = [{1,},{2,},]") == {"x": [{1}, {2}]}
107 | assert parse_dzn("x = [|1,|2,|3,|]") == {"x": [[1], [2], [3]]}
108 |
--------------------------------------------------------------------------------
/tests/test_errors.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import pytest
6 | from support import InstanceTestCase
7 |
8 | import minizinc
9 | from minizinc.error import (
10 | AssertionError,
11 | EvaluationError,
12 | MiniZincError,
13 | SyntaxError,
14 | TypeError,
15 | )
16 |
17 |
18 | class AssertionTest(InstanceTestCase):
19 | code = """
20 | array [1..10] of int: a = [i | i in 1..10];
21 | constraint assert(forall (i in 1..9) (a[i] > a[i + 1]), "a not decreasing");
22 | var 1..10: x;
23 | constraint a[x] = max(a);
24 | solve satisfy;
25 | """
26 |
27 | def test_assertion_error(self):
28 | with pytest.raises(AssertionError, match="a not decreasing") as error:
29 | self.instance.solve()
30 | loc = error.value.location
31 | assert str(loc.file).endswith(".mzn")
32 | assert loc.lines == (3, 3)
33 | if minizinc.default_driver.parsed_version >= (2, 6, 0):
34 | assert loc.columns == (27, 62)
35 |
36 |
37 | class TypeErrorTest(InstanceTestCase):
38 | code = """
39 | array[1..2] of var int: i;
40 | constraint i = 1.5;
41 | """
42 |
43 | def test_type_error(self):
44 | with pytest.raises(
45 | TypeError, match="No matching operator found"
46 | ) as error:
47 | self.instance.solve()
48 | loc = error.value.location
49 | assert str(loc.file).endswith(".mzn")
50 | assert loc.lines == (3, 3)
51 | assert loc.columns == (20, 26)
52 |
53 |
54 | class SyntaxErrorTest(InstanceTestCase):
55 | code = "constrain true;"
56 |
57 | def test_syntax_error(self):
58 | with pytest.raises(
59 | SyntaxError, match="unexpected bool literal"
60 | ) as error:
61 | self.instance.solve()
62 | loc = error.value.location
63 | assert str(loc.file).endswith(".mzn")
64 | assert loc.lines == (1, 1)
65 | assert loc.columns == (11, 14)
66 |
67 |
68 | class EvaluationErrorTest(InstanceTestCase):
69 | def test_infinite_recursion(self):
70 | self.instance.add_string(
71 | """
72 | test overflow(int: x) = overflow(x + 1);
73 | int: cause_overflow = overflow(1);
74 | """
75 | )
76 |
77 | with pytest.raises(
78 | MiniZincError,
79 | match="stack overflow",
80 | ):
81 | self.instance.solve()
82 |
83 | def test_evaluation_error(self):
84 | self.instance.add_string(
85 | """
86 | array [1..3] of int: a = [1, 2, 3, 4];
87 |
88 | solve satisfy;
89 | """
90 | )
91 | with pytest.raises(EvaluationError, match="index set") as error:
92 | self.instance.solve()
93 | loc = error.value.location
94 | assert str(loc.file).endswith(".mzn")
95 | assert loc.lines == (2, 2)
96 | if minizinc.default_driver.parsed_version >= (2, 7, 1):
97 | assert loc.columns == (26, 37)
98 | else:
99 | assert loc.columns == (1, 22)
100 |
--------------------------------------------------------------------------------
/tests/test_helpers.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import warnings
6 |
7 | from support import InstanceTestCase
8 |
9 | from minizinc import Method, Solver, Status
10 | from minizinc.error import MiniZincWarning
11 | from minizinc.helpers import check_result, check_solution
12 |
13 |
14 | class CheckResults(InstanceTestCase):
15 | code = """
16 | array[1..2] of var 1..10: x;
17 | constraint x[1] + 1 = x[2];
18 | """
19 |
20 | @classmethod
21 | def setUpClass(cls):
22 | super().setUpClass()
23 | cls.other_solver = Solver.lookup("chuffed")
24 |
25 | def test_correct(self):
26 | assert self.instance.method == Method.SATISFY
27 | result = self.instance.solve()
28 | assert check_result(self.instance, result, self.other_solver)
29 |
30 | def test_incorrect(self):
31 | assert self.instance.method == Method.SATISFY
32 | with warnings.catch_warnings(record=True) as w:
33 | result = self.instance.solve()
34 | result.solution = self.instance.output_type(x=[2, 1])
35 | assert not check_result(self.instance, result, self.other_solver)
36 | assert len(w) == 1
37 | assert issubclass(w[-1].category, MiniZincWarning)
38 | assert "model inconsistency" in str(w[-1].message)
39 |
40 | def test_check_all(self):
41 | assert self.instance.method == Method.SATISFY
42 | result = self.instance.solve(all_solutions=True)
43 | assert check_result(
44 | self.instance,
45 | result,
46 | self.other_solver,
47 | range(len(result.solution)),
48 | )
49 |
50 | def test_check_specific(self):
51 | assert self.instance.method == Method.SATISFY
52 | result = self.instance.solve(nr_solutions=5)
53 | assert check_result(self.instance, result, self.other_solver, [1, 2])
54 |
55 | def test_dict(self):
56 | assert check_solution(
57 | self.instance, {"x": [5, 6]}, Status.SATISFIED, self.other_solver
58 | )
59 |
60 | def test_enum(self):
61 | self.instance.add_string("""enum Foo = {A, B};var Foo: f;""")
62 | result = self.instance.solve()
63 | assert check_result(self.instance, result, self.other_solver)
64 |
--------------------------------------------------------------------------------
/tests/test_init.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 | import minizinc
5 |
6 |
7 | def test_default_driver():
8 | assert minizinc.default_driver is not None
9 | assert minizinc.default_driver.executable.exists()
10 |
11 |
12 | def test_version():
13 | # Test normal behaviour
14 | assert "MiniZinc" in minizinc.default_driver.minizinc_version
15 |
--------------------------------------------------------------------------------
/tests/test_instance.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import warnings
6 |
7 | import pytest
8 | from support import InstanceTestCase
9 |
10 | import minizinc
11 | from minizinc.error import MiniZincWarning
12 | from minizinc.result import Status
13 |
14 |
15 | class TestAssign(InstanceTestCase):
16 | code = """
17 | include "globals.mzn";
18 | int: n;
19 | array[1..4] of var 1..5: x;
20 | constraint increasing(x);
21 | constraint alldifferent(x);
22 | constraint sum(x) = n;
23 | """
24 |
25 | def test_assign(self):
26 | self.instance["n"] = 14
27 | result = self.instance.solve(all_solutions=True)
28 | assert result.status == Status.ALL_SOLUTIONS
29 | assert len(result.solution) == 1
30 | assert result[0, "x"] == list(range(2, 5 + 1))
31 |
32 | def test_reassign(self):
33 | self.instance["n"] = 14
34 | with pytest.raises(
35 | AssertionError, match="cannot be assigned multiple values."
36 | ):
37 | self.instance["n"] = 15
38 |
39 |
40 | class TestPythonConflict(InstanceTestCase):
41 | code = """
42 | include "globals.mzn";
43 | var 1..2: return;
44 | constraint return > 1;
45 | """
46 |
47 | def test_rename(self):
48 | with pytest.warns(SyntaxWarning):
49 | result = self.instance.solve()
50 | assert result.solution.mzn_return == 2
51 |
52 |
53 | class TestBranch(InstanceTestCase):
54 | code = """
55 | include "globals.mzn";
56 | var 14..15: n;
57 | array[1..4] of var 1..5: x;
58 | constraint increasing(x);
59 | constraint sum(x) = n;
60 | """
61 |
62 | def test_add_data(self):
63 | result = self.instance.solve(all_solutions=True)
64 | assert result.status == Status.ALL_SOLUTIONS
65 | assert len(result.solution) == 12
66 | with self.instance.branch() as child:
67 | child["n"] = 15
68 | result = child.solve(all_solutions=True)
69 | assert result.status == Status.ALL_SOLUTIONS
70 | assert len(result.solution) == 5
71 | with self.instance.branch() as child:
72 | child["n"] = 14
73 | result = child.solve(all_solutions=True)
74 | assert result.status == Status.ALL_SOLUTIONS
75 | assert len(result.solution) == 7
76 |
77 | def test_extra_constraint(self):
78 | self.instance["n"] = 14
79 | result = self.instance.solve(all_solutions=True)
80 | assert result.status == Status.ALL_SOLUTIONS
81 | assert len(result.solution) == 7
82 | with self.instance.branch() as child:
83 | child.add_string("constraint all_different(x);")
84 | result = child.solve(all_solutions=True)
85 | assert result.status == Status.ALL_SOLUTIONS
86 | assert len(result.solution) == 1
87 |
88 | def test_replace_data(self):
89 | self.instance["n"] = 14
90 | result = self.instance.solve(all_solutions=True)
91 | assert result.status == Status.ALL_SOLUTIONS
92 | assert len(result.solution) == 7
93 | with self.instance.branch() as child:
94 | child["n"] = 15
95 | with warnings.catch_warnings(record=True) as w:
96 | result = child.solve(all_solutions=True)
97 | assert result.status == Status.UNSATISFIABLE
98 | assert len(result.solution) == 0
99 | if minizinc.default_driver.parsed_version >= (2, 6, 0):
100 | assert len(w) == 1
101 | assert issubclass(w[-1].category, MiniZincWarning)
102 | assert "model inconsistency" in str(w[-1].message)
103 |
--------------------------------------------------------------------------------
/tests/test_solver.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | from minizinc import Solver
6 |
7 |
8 | def test_gecode():
9 | gecode = Solver.lookup("gecode")
10 | assert gecode is not None
11 | assert gecode.id.endswith("gecode")
12 | assert gecode.executable.endswith("fzn-gecode")
13 |
14 |
15 | def test_chuffed():
16 | chuffed = Solver.lookup("chuffed")
17 | assert chuffed is not None
18 | assert chuffed.id.endswith("chuffed")
19 | assert chuffed.executable.endswith("fzn-chuffed")
20 |
21 |
22 | def test_coinbc():
23 | coinbc = Solver.lookup("coin-bc")
24 | assert coinbc is not None
25 | assert coinbc.id.endswith("coin-bc")
26 | assert coinbc.executable is None
27 |
--------------------------------------------------------------------------------
/tests/test_solving.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 |
5 | import asyncio
6 | import os
7 | import tempfile
8 | from dataclasses import fields
9 |
10 | import pytest
11 | from support import InstanceTestCase
12 |
13 | from minizinc.instance import Method
14 | from minizinc.result import Status
15 |
16 |
17 | class TestSatisfy(InstanceTestCase):
18 | code = "var 1..5: x"
19 |
20 | def test_solve(self):
21 | assert self.instance.method == Method.SATISFY
22 | result = self.instance.solve()
23 | assert result.status == Status.SATISFIED
24 | assert result["x"] in range(1, 5 + 1)
25 |
26 | def test_all_solution(self):
27 | result = self.instance.solve(all_solutions=True)
28 | assert result.status == Status.ALL_SOLUTIONS
29 | assert len(result) == 5
30 | assert sorted([sol.x for sol in result.solution]) == list(
31 | range(1, 5 + 1)
32 | )
33 |
34 | def test_nr_solutions(self):
35 | result = self.instance.solve(nr_solutions=3)
36 | assert result.status == Status.SATISFIED
37 | assert len(result) == 3
38 | for sol in result.solution:
39 | assert sol.x in range(1, 5 + 1)
40 |
41 |
42 | class TestChecker(InstanceTestCase):
43 | code = "var 1..5: x"
44 |
45 | def test_checker(self):
46 | try:
47 | checker = tempfile.NamedTemporaryFile(
48 | prefix="checker_test_", suffix=".mzc.mzn", delete=False
49 | )
50 | checker.write(b'output["SIMPLE CHECK"];\n')
51 | checker.close()
52 | self.instance.add_file(checker.name)
53 | assert self.instance.method == Method.SATISFY
54 | result = self.instance.solve()
55 | assert result.status == Status.SATISFIED
56 | assert result["x"] in range(1, 5 + 1)
57 | assert result.solution.check().strip() == "SIMPLE CHECK"
58 | finally:
59 | os.remove(checker.name)
60 |
61 | def test_no_output_checker(self):
62 | try:
63 | checker = tempfile.NamedTemporaryFile(
64 | prefix="checker_test_", suffix=".mzc.mzn", delete=False
65 | )
66 | checker.write(b"int: data :: add_to_output = 2;\n")
67 | checker.close()
68 | self.instance.add_file(checker.name)
69 | assert self.instance.method == Method.SATISFY
70 | result = self.instance.solve()
71 | assert result.status == Status.SATISFIED
72 | assert result["x"] in range(1, 5 + 1)
73 | assert result.solution.check().strip() == "data = 2;"
74 | finally:
75 | os.remove(checker.name)
76 |
77 |
78 | class TestMaximise(InstanceTestCase):
79 | code = """
80 | array[1..5] of var 1..5: x;
81 | solve ::int_search(x, input_order, indomain_min) maximize sum(x);
82 | """
83 |
84 | def test_solve(self):
85 | assert self.instance.method == Method.MAXIMIZE
86 | result = self.instance.solve()
87 | assert result.status == Status.OPTIMAL_SOLUTION
88 | assert result.objective == 25
89 |
90 | def test_intermediate(self):
91 | result = self.instance.solve(intermediate_solutions=True)
92 | assert len(result) == 21
93 | assert result.objective == 25
94 |
95 | def test_solutions_no_intermediate(self):
96 | async def run():
97 | results = []
98 | async for result in self.instance.solutions(
99 | intermediate_solutions=False
100 | ):
101 | results.append(result)
102 | return results
103 |
104 | results = asyncio.run(run())
105 | assert len(results) == 1
106 | assert results[0].solution.objective == 25
107 |
108 |
109 | class TestParameter(InstanceTestCase):
110 | code = """
111 | int: n; % The number of queens.
112 | array [1..n] of var 1..n: q;
113 |
114 | include "alldifferent.mzn";
115 |
116 | constraint alldifferent(q);
117 | constraint alldifferent(i in 1..n)(q[i] + i);
118 | constraint alldifferent(i in 1..n)(q[i] - i);
119 | """
120 |
121 | def test_2(self):
122 | self.instance["n"] = 2
123 | assert self.instance.method == Method.SATISFY
124 | result = self.instance.solve()
125 | assert result.status == Status.UNSATISFIABLE
126 |
127 | def test_4(self):
128 | self.instance["n"] = 4
129 | assert self.instance.method == Method.SATISFY
130 | result = self.instance.solve()
131 | assert result.status == Status.SATISFIED
132 | assert len(result["q"]) == 4
133 |
134 |
135 | class CheckEmpty(InstanceTestCase):
136 | code = """int: x = 5;"""
137 |
138 | def test_empty(self):
139 | assert self.instance.method == Method.SATISFY
140 | result = self.instance.solve()
141 | assert len(fields(result.solution)) == 0
142 | assert result.status == Status.SATISFIED
143 |
144 |
145 | class FromAsync(InstanceTestCase):
146 | code = """int: x ::add_to_output = 5;"""
147 |
148 | def test_async_error(self):
149 | async def bad_run():
150 | return self.instance.solve()
151 |
152 | with pytest.raises(RuntimeError) as exc_info:
153 | _ = asyncio.run(bad_run())
154 |
155 | assert "solve_async" in str(exc_info.value)
156 |
157 | def test_async_success(self):
158 | async def good_run():
159 | return await self.instance.solve_async()
160 |
161 | result = asyncio.run(good_run())
162 | assert result.status == Status.SATISFIED
163 | assert result["x"] == 5
164 |
--------------------------------------------------------------------------------
/tests/test_types.py:
--------------------------------------------------------------------------------
1 | # This Source Code Form is subject to the terms of the Mozilla Public
2 | # License, v. 2.0. If a copy of the MPL was not distributed with this
3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 | import enum
5 |
6 | import pytest
7 | from support import InstanceTestCase
8 |
9 | from minizinc import Instance, default_driver
10 | from minizinc.result import Status
11 | from minizinc.types import AnonEnum, ConstrEnum
12 |
13 |
14 | class TestEnum(InstanceTestCase):
15 | code = """
16 | enum DAY = {Mo, Tu, We, Th, Fr, Sa, Su};
17 | var DAY: d;
18 | """
19 |
20 | def test_value(self):
21 | self.instance.add_string("constraint d == Mo;")
22 | result = self.instance.solve()
23 | assert isinstance(result["d"], str)
24 | assert result["d"] == "Mo"
25 |
26 | def test_cmp_in_instance(self):
27 | self.instance.add_string("var DAY: d2;")
28 | self.instance.add_string("constraint d < d2;")
29 | result = self.instance.solve()
30 | assert isinstance(result["d"], str)
31 | assert isinstance(result["d2"], str)
32 | # TODO: assert result["d"] < result["d2"]
33 |
34 | def test_cmp_between_instances(self):
35 | append = "constraint d == Mo;"
36 | self.instance.add_string(append)
37 | result = self.instance.solve()
38 |
39 | inst = Instance(self.solver)
40 | inst.add_string(self.code + append)
41 | result2 = inst.solve()
42 | assert isinstance(result["d"], str)
43 | assert isinstance(result2["d"], str)
44 | assert result["d"] == result2["d"]
45 |
46 | inst = Instance(self.solver)
47 | inst.add_string(
48 | """
49 | enum DAY = {Mo, Tu, We, Th, Fr};
50 | var DAY: d;
51 | """
52 | + append
53 | )
54 | result2 = inst.solve()
55 | assert result["d"] == result2["d"]
56 |
57 | def test_assign(self):
58 | self.instance = Instance(self.solver)
59 | self.instance.add_string(
60 | """
61 | enum TT;
62 | var TT: t1;
63 | """
64 | )
65 | TT = enum.Enum("TT", ["one"])
66 | self.instance["TT"] = TT
67 | result = self.instance.solve()
68 |
69 | assert isinstance(result["t1"], TT)
70 | assert result["t1"] is TT.one
71 |
72 | def test_collections(self):
73 | self.instance = Instance(self.solver)
74 | self.instance.add_string(
75 | """
76 | enum TT;
77 | array[int] of var TT: arr_t;
78 | var set of TT: set_t;
79 | """
80 | )
81 | TT = enum.Enum("TT", ["one", "two", "three"])
82 | self.instance["TT"] = TT
83 | self.instance["arr_t"] = [TT(3), TT(2), TT(1)]
84 | self.instance["set_t"] = {TT(2), TT(1)}
85 | result = self.instance.solve()
86 |
87 | assert result["arr_t"] == [TT(3), TT(2), TT(1)]
88 | assert result["set_t"] == {TT(1), TT(2)}
89 |
90 | def test_intenum_collections(self):
91 | self.instance = Instance(self.solver)
92 | self.instance.add_string(
93 | """
94 | enum TT;
95 | % array[int] of var TT: arr_t;
96 | var set of TT: set_t;
97 | """
98 | )
99 | TT = enum.IntEnum("TT", ["one", "two", "three"])
100 | self.instance["TT"] = TT
101 | # TODO: self.instance["arr_t"] = [TT(3), TT(2), TT(1)]
102 | self.instance["set_t"] = {TT(2), TT(1)}
103 | result = self.instance.solve()
104 |
105 | # TODO: assert result["arr_t"] == [TT(3), TT(2), TT(1)]
106 | assert result["set_t"] == {TT(1), TT(2)}
107 |
108 | def test_constructor_enum(self):
109 | self.instance = Instance(self.solver)
110 | self.instance.add_string(
111 | """
112 | enum T = X(1..3);
113 | var T: x;
114 | constraint x > X(1) /\\ x < X(3); % TODO: Remove for MiniZinc 2.7+
115 | """
116 | )
117 | # TODO: Remove for MiniZinc 2.7+
118 | # self.instance["x"] = ConstrEnum("X", 2)
119 | result = self.instance.solve()
120 | assert isinstance(result["x"], ConstrEnum)
121 | assert result["x"] == ConstrEnum("X", 2)
122 | assert str(result["x"]) == "X(2)"
123 |
124 | def test_anon_enum(self):
125 | self.instance = Instance(self.solver)
126 | self.instance.add_string(
127 | """
128 | enum T = _(1..5);
129 | var T: x;
130 | """
131 | )
132 | self.instance["x"] = AnonEnum("T", 3)
133 | result = self.instance.solve()
134 | assert isinstance(result["x"], AnonEnum)
135 | assert result["x"].value == 3
136 | assert str(result["x"]) == "to_enum(T,3)"
137 |
138 | def test_non_ascii(self):
139 | self.instance = Instance(self.solver)
140 | self.instance.add_string(
141 | """
142 | include "strictly_increasing.mzn";
143 | enum TT;
144 | array[1..3] of var TT: x;
145 | constraint strictly_increasing(x);
146 | """
147 | )
148 | TT = enum.Enum("TT", ["this one", "🍻", "∑"])
149 | self.instance["TT"] = TT
150 | result = self.instance.solve()
151 | assert result["x"] == [TT(1), TT(2), TT(3)]
152 |
153 |
154 | class TestSets(InstanceTestCase):
155 | def test_sets(self):
156 | self.instance.add_string(
157 | """
158 | var set of 0..10: s;
159 | set of int: s1;
160 | constraint s1 = s;
161 | """
162 | )
163 |
164 | self.instance["s1"] = range(1, 4)
165 | result = self.instance.solve()
166 | assert isinstance(result["s"], set)
167 | assert result["s"] == set(range(1, 4))
168 |
169 |
170 | class TestString(InstanceTestCase):
171 | code = """
172 | array[int] of string: names;
173 | var index_set(names): x;
174 | string: name ::output_only ::add_to_output = names[fix(x)];
175 | """
176 |
177 | def test_string(self):
178 | names = ["Guido", "Peter"]
179 | self.instance["names"] = names
180 |
181 | result = self.instance.solve()
182 | assert result.solution.name in names
183 |
184 |
185 | class TestTuple(InstanceTestCase):
186 | @pytest.mark.skipif(
187 | default_driver is None or default_driver.parsed_version < (2, 7, 0),
188 | reason="requires MiniZinc 2.7 or higher",
189 | )
190 | def test_simple_tuple(self):
191 | self.instance.add_string(
192 | """
193 | var tuple(1..3, bool, 1.0..3.0): x;
194 | """
195 | )
196 | result = self.instance.solve()
197 | tup = result["x"]
198 | assert isinstance(tup, list)
199 | assert len(tup) == 3
200 | assert isinstance(tup[0], int)
201 | assert tup[0] in range(1, 4)
202 | assert isinstance(tup[1], bool)
203 | assert isinstance(tup[2], float)
204 | assert 1.0 <= tup[2] and tup[2] <= 3.0
205 |
206 | @pytest.mark.skipif(
207 | default_driver is None or default_driver.parsed_version < (2, 7, 0),
208 | reason="requires MiniZinc 2.7 or higher",
209 | )
210 | def test_rec_tuple(self):
211 | self.instance.add_string(
212 | """
213 | var tuple(1..3, bool, tuple(2..3, 4..6)): x;
214 | """
215 | )
216 | result = self.instance.solve()
217 | tup = result["x"]
218 | assert isinstance(tup, list)
219 | assert len(tup) == 3
220 | assert isinstance(tup[0], int)
221 | assert tup[0] in range(1, 4)
222 | assert isinstance(tup[1], bool)
223 | assert isinstance(tup[2], list)
224 | assert len(tup[2]) == 2
225 | assert isinstance(tup[2][0], int)
226 | assert tup[2][0] in range(2, 4)
227 | assert isinstance(tup[2][1], int)
228 | assert tup[2][1] in range(4, 7)
229 |
230 |
231 | class TestRecord(InstanceTestCase):
232 | @pytest.mark.skipif(
233 | default_driver is None or default_driver.parsed_version < (2, 7, 0),
234 | reason="requires MiniZinc 2.7 or higher",
235 | )
236 | def test_simple_record(self):
237 | self.instance.add_string(
238 | """
239 | var record(1..3: a, bool: b, 1.0..3.0: c): x;
240 | """
241 | )
242 | result = self.instance.solve()
243 | rec = result["x"]
244 | assert isinstance(rec, dict)
245 | assert len(rec) == 3
246 | assert isinstance(rec["a"], int)
247 | assert rec["a"] in range(1, 4)
248 | assert isinstance(rec["b"], bool)
249 | assert isinstance(rec["c"], float)
250 | assert 1.0 <= rec["c"] and rec["c"] <= 3.0
251 |
252 | @pytest.mark.skipif(
253 | default_driver is None or default_driver.parsed_version < (2, 7, 0),
254 | reason="requires MiniZinc 2.7 or higher",
255 | )
256 | def test_rec_record(self):
257 | self.instance.add_string(
258 | """
259 | var record(1..3: a, bool: b, record(2..3: d, 4..6: e): c): x;
260 | """
261 | )
262 | result = self.instance.solve()
263 | rec = result["x"]
264 | assert isinstance(rec, dict)
265 | assert len(rec) == 3
266 | assert isinstance(rec["a"], int)
267 | assert rec["a"] in range(1, 4)
268 | assert isinstance(rec["b"], bool)
269 | assert isinstance(rec["c"], dict)
270 | assert len(rec["c"]) == 2
271 | assert isinstance(rec["c"]["d"], int)
272 | assert rec["c"]["d"] in range(2, 4)
273 | assert isinstance(rec["c"]["e"], int)
274 | assert rec["c"]["e"] in range(4, 7)
275 |
276 |
277 | class TestNumPy(InstanceTestCase):
278 | def test_nparray_bool(self):
279 | numpy = pytest.importorskip("numpy")
280 | self.instance.add_string("array[int] of bool: x;")
281 | self.instance["x"] = numpy.array([True, False], dtype=numpy.bool_)
282 | result = self.instance.solve()
283 | assert result.status is Status.SATISFIED
284 |
285 | def test_nparray_f32(self):
286 | numpy = pytest.importorskip("numpy")
287 | self.instance.add_string("array[int] of float: x;")
288 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.float32)
289 | result = self.instance.solve()
290 | assert result.status is Status.SATISFIED
291 |
292 | def test_nparray_f64(self):
293 | numpy = pytest.importorskip("numpy")
294 | self.instance.add_string("array[int] of float: x;")
295 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.float64)
296 | result = self.instance.solve()
297 | assert result.status is Status.SATISFIED
298 |
299 | def test_nparray_int32(self):
300 | numpy = pytest.importorskip("numpy")
301 | self.instance.add_string("array[int] of int: x;")
302 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.int32)
303 | result = self.instance.solve()
304 | assert result.status is Status.SATISFIED
305 |
306 | def test_nparray_int64(self):
307 | numpy = pytest.importorskip("numpy")
308 | self.instance.add_string("array[int] of int: x;")
309 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.int64)
310 | result = self.instance.solve()
311 | assert result.status is Status.SATISFIED
312 |
313 | def test_nparray_uint32(self):
314 | numpy = pytest.importorskip("numpy")
315 | self.instance.add_string("array[int] of int: x;")
316 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.uint32)
317 | result = self.instance.solve()
318 | assert result.status is Status.SATISFIED
319 |
320 | def test_nparray_uint64(self):
321 | numpy = pytest.importorskip("numpy")
322 | self.instance.add_string("array[int] of int: x;")
323 | self.instance["x"] = numpy.array([1, 2, 3], dtype=numpy.uint64)
324 | result = self.instance.solve()
325 | assert result.status is Status.SATISFIED
326 |
327 | def test_npbool(self):
328 | numpy = pytest.importorskip("numpy")
329 | self.instance.add_string("bool: x;")
330 | self.instance["x"] = numpy.bool_(0)
331 | result = self.instance.solve()
332 | assert result.status is Status.SATISFIED
333 |
334 | def test_npf32(self):
335 | numpy = pytest.importorskip("numpy")
336 | self.instance.add_string("float: x;")
337 | self.instance["x"] = numpy.float32(0)
338 | result = self.instance.solve()
339 | assert result.status is Status.SATISFIED
340 |
341 | def test_npf64(self):
342 | numpy = pytest.importorskip("numpy")
343 | self.instance.add_string("float: x;")
344 | self.instance["x"] = numpy.float64(0)
345 | result = self.instance.solve()
346 | assert result.status is Status.SATISFIED
347 |
348 | def test_npint32(self):
349 | numpy = pytest.importorskip("numpy")
350 | self.instance.add_string("int: x;")
351 | self.instance["x"] = numpy.int32(0)
352 | result = self.instance.solve()
353 | assert result.status is Status.SATISFIED
354 |
355 | def test_npint64(self):
356 | numpy = pytest.importorskip("numpy")
357 | self.instance.add_string("int: x;")
358 | self.instance["x"] = numpy.int64(0)
359 | result = self.instance.solve()
360 | assert result.status is Status.SATISFIED
361 |
362 |
363 | class TestAnn(InstanceTestCase):
364 | def test_ann_atom(self):
365 | self.instance.add_string("ann: x :: add_to_output = promise_total;")
366 | result = self.instance.solve()
367 | assert result.status is Status.SATISFIED
368 | assert result["x"] == "promise_total"
369 |
370 | def test_ann_call(self):
371 | self.instance.add_string(
372 | 'ann: x :: add_to_output = expression_name("test");'
373 | )
374 | result = self.instance.solve()
375 | assert result.status is Status.SATISFIED
376 | assert result["x"] == 'expression_name("test")'
377 |
--------------------------------------------------------------------------------