├── .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 |

4 | 5 | Logo 6 | 7 | 8 |

MiniZinc Python

9 | 10 |

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 | image/svg+xml -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------