├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build_wheels.yml │ ├── coverage.yml │ ├── generate_docs.yml │ ├── integration-test.yml │ ├── test-release.yml │ └── update-packages-and-documentation.yml ├── .gitignore ├── .landscape.yml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CITATION.bib ├── CODE_OF_CONDUCT.md ├── INSTALL.md ├── LICENSE ├── README.md ├── RELEASE.md ├── docs ├── Makefile ├── _static │ ├── scip_structure_landscape-compressed.png │ └── skippy_logo_blue.png ├── api.rst ├── api │ ├── column.rst │ ├── constraint.rst │ ├── event.rst │ ├── model.rst │ ├── node.rst │ ├── row.rst │ └── variable.rst ├── build.rst ├── conf.py ├── contributors.rst ├── extend.rst ├── faq.rst ├── index.rst ├── install.rst ├── requirements.txt ├── similarsoftware.rst ├── tutorials │ ├── branchrule.rst │ ├── constypes.rst │ ├── cutselector.rst │ ├── eventhandler.rst │ ├── expressions.rst │ ├── heuristic.rst │ ├── index.rst │ ├── lazycons.rst │ ├── logfile.rst │ ├── matrix.rst │ ├── model.rst │ ├── nodeselector.rst │ ├── readwrite.rst │ ├── scipdex.rst │ ├── separator.rst │ └── vartypes.rst └── whyscip.rst ├── examples ├── finished │ ├── __init__.py │ ├── atsp.py │ ├── bpp.py │ ├── categorical_data.py │ ├── diet.py │ ├── eoq_en.py │ ├── even.py │ ├── flp-benders.py │ ├── flp.py │ ├── gcp.py │ ├── gcp_fixed_k.py │ ├── kmedian.py │ ├── lo_wines.py │ ├── logical.py │ ├── lotsizing_lazy.py │ ├── markowitz_soco.py │ ├── mctransp.py │ ├── mkp.py │ ├── pfs.py │ ├── piecewise.py │ ├── plot_primal_dual_evolution.py │ ├── prodmix_soco.py │ ├── rcs.py │ ├── read_tsplib.py │ ├── ssa.py │ ├── ssp.py │ ├── sudoku.py │ ├── transp.py │ ├── transp_nofn.py │ ├── tsp.py │ └── weber_soco.py ├── tutorial │ ├── even.py │ ├── logical.py │ └── puzzle.py └── unfinished │ ├── cutstock.py │ ├── diet_std.py │ ├── eld.py │ ├── eoq_soco.py │ ├── flp_nonlinear.py │ ├── flp_nonlinear_soco.py │ ├── gpp.py │ ├── kcenter.py │ ├── kcenter_binary_search.py │ ├── lotsizing.py │ ├── lotsizing_cut.py │ ├── lotsizing_echelon.py │ ├── mctransp_tuplelist.py │ ├── pareto_front.py │ ├── portfolio_soco.py │ ├── read_tsplib.py │ ├── scheduling.py │ ├── staff_sched.py │ ├── staff_sched_mo.py │ ├── tsp_flow.py │ ├── tsp_lazy.py │ ├── tsp_mo.py │ ├── tsptw.py │ ├── vrp.py │ └── vrp_lazy.py ├── generate-docs.sh ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── pyscipopt │ ├── Multidict.py │ ├── __init__.py │ ├── _version.py │ ├── benders.pxi │ ├── benderscut.pxi │ ├── branchrule.pxi │ ├── conshdlr.pxi │ ├── cutsel.pxi │ ├── event.pxi │ ├── expr.pxi │ ├── heuristic.pxi │ ├── lp.pxi │ ├── matrix.pxi │ ├── nodesel.pxi │ ├── presol.pxi │ ├── pricer.pxi │ ├── propagator.pxi │ ├── reader.pxi │ ├── recipes │ ├── README.md │ ├── __init__.py │ ├── infeasibilities.py │ ├── nonlinear.py │ ├── piecewise.py │ └── primal_dual_evolution.py │ ├── relax.pxi │ ├── scip.pxd │ ├── scip.pxi │ ├── scip.pyx │ └── sepa.pxi └── tests ├── data ├── 10teams.mps ├── readStatistics.stats └── test_locale.cip ├── helpers └── utils.py ├── test_alldiff.py ├── test_benders.py ├── test_bipartite.py ├── test_branch_mostinfeas.py ├── test_branch_probing_lp.py ├── test_cons.py ├── test_conshdlr.py ├── test_copy.py ├── test_customizedbenders.py ├── test_cutsel.py ├── test_event.py ├── test_expr.py ├── test_gomory.py ├── test_heur.py ├── test_knapsack.py ├── test_linexpr.py ├── test_logical.py ├── test_lp.py ├── test_matrix_variable.py ├── test_memory.py ├── test_model.py ├── test_nlrow.py ├── test_node.py ├── test_nodesel.py ├── test_nogil.py ├── test_nonlinear.py ├── test_numerics.py ├── test_pricer.py ├── test_quadcons.py ├── test_quickprod.py ├── test_quicksum.py ├── test_reader.py ├── test_recipe_infeasibilities.py ├── test_recipe_nonlinear.py ├── test_recipe_piecewise.py ├── test_recipe_primal_dual_evolution.py ├── test_relax.py ├── test_reopt.py ├── test_row_dual.py ├── test_short.py ├── test_solution.py ├── test_strong_branching.py ├── test_sub_sol.py ├── test_tree.py ├── test_tsp.py └── test_vars.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | plugins = Cython.Coverage 3 | source = src/pyscipopt 4 | omit = 5 | tests 6 | __init__.py 7 | scip.pyx 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **System** 23 | - OS: [e.g. iOS] 24 | - Version [e.g. 22] 25 | - SCIP version 26 | - How did you install `pyscipopt`? 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels.yml: -------------------------------------------------------------------------------- 1 | name: Build wheels 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | # scip_version: 7 | # type: string 8 | # description: SCIPOptSuite deployment version 9 | # required: true 10 | # default: "v0.4.0" 11 | upload_to_pypi: 12 | type: boolean 13 | description: Should upload 14 | required: false 15 | default: true 16 | test_pypi: 17 | type: boolean 18 | description: Use Test PyPI 19 | required: false 20 | default: true 21 | 22 | jobs: 23 | build_wheels: 24 | name: Build wheels on ${{ matrix.os }} 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | include: 29 | - os: ubuntu-22.04 30 | arch: x86_64 31 | - os: macos-14 32 | arch: arm64 33 | - os: macos-13 34 | arch: x86_64 35 | - os: windows-latest 36 | arch: AMD64 37 | 38 | 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - name: Build wheels 44 | uses: pypa/cibuildwheel@v2.21.1 45 | env: 46 | CIBW_ARCHS: ${{ matrix.arch }} 47 | CIBW_TEST_REQUIRES: pytest 48 | CIBW_TEST_COMMAND: "pytest {project}/tests" 49 | CIBW_MANYLINUX_*_IMAGE: manylinux_2_28 50 | 51 | - uses: actions/upload-artifact@v4 52 | with: 53 | name: wheels-${{ matrix.os}}-${{ matrix.arch }} 54 | path: ./wheelhouse/*.whl 55 | 56 | build_sdist: 57 | name: Build source distribution 58 | runs-on: ubuntu-22.04 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - name: Build sdist 63 | shell: bash -l {0} 64 | run: pipx run build --sdist 65 | 66 | - uses: actions/upload-artifact@v4 67 | with: 68 | name: source-distribution 69 | path: dist/*.tar.gz 70 | 71 | merge_artifacts: 72 | name: Merge Artifacts 73 | needs: [build_wheels, build_sdist] 74 | runs-on: ubuntu-22.04 75 | steps: 76 | - name: Merge Artifacts 77 | uses: actions/upload-artifact/merge@v4 78 | 79 | upload_pypi: 80 | needs: [build_wheels, build_sdist, merge_artifacts] 81 | runs-on: ubuntu-22.04 82 | if: github.event.inputs.upload_to_pypi == 'true' 83 | steps: 84 | - uses: actions/download-artifact@v4 85 | with: 86 | name: merged-artifacts 87 | path: dist 88 | 89 | - uses: pypa/gh-action-pypi-publish@release/v1 90 | if: github.event.inputs.test_pypi == 'false' 91 | with: 92 | user: __token__ 93 | password: ${{ secrets.PYPI_API_TOKEN }} 94 | verbose: true 95 | 96 | - uses: pypa/gh-action-pypi-publish@release/v1 97 | if: github.event.inputs.test_pypi == 'true' 98 | with: 99 | repository-url: https://test.pypi.org/legacy/ 100 | user: __token__ 101 | password: ${{ secrets.TESTPYPI_API_TOKEN }} 102 | verbose: true 103 | 104 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Run tests with coverage 2 | env: 3 | version: 9.2.1 4 | 5 | on: 6 | push: 7 | branches: 8 | - 'master' 9 | pull_request: 10 | types: [opened, synchronize, reopened, ready_for_review] 11 | branches: 12 | - 'master' 13 | 14 | jobs: 15 | 16 | test-coverage: 17 | if: (github.event_name != 'pull_request') || (github.event.pull_request.draft == false) 18 | runs-on: ubuntu-22.04 19 | strategy: 20 | fail-fast: true 21 | matrix: 22 | python-version: ["3.11"] 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Install dependencies (SCIPOptSuite) 27 | run: | 28 | wget --quiet --no-check-certificate https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/SCIPOptSuite-${{ env.version }}-Linux-ubuntu20.deb 29 | sudo apt-get update && sudo apt install -y ./SCIPOptSuite-${{ env.version }}-Linux-ubuntu20.deb 30 | 31 | - name: Setup python ${{ matrix.python-version }} 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: ${{ matrix.python-version }} 35 | 36 | - name: Prepare python environment 37 | run: | 38 | python -m pip install --upgrade pip 39 | python -m pip install networkx cython pytest-cov numpy 40 | 41 | - name: Install PySCIPOpt 42 | run: | 43 | export CFLAGS="-O0 -ggdb" # disable compiler optimizations for faster builds 44 | python -m pip install . 45 | 46 | - name: Run pyscipopt tests 47 | run: | 48 | sudo apt-get install tzdata locales -y && sudo locale-gen pt_PT && sudo update-locale # add pt_PT locale that is used in tests 49 | coverage run -m pytest 50 | coverage report -m 51 | coverage xml 52 | 53 | - name: Upload to codecov.io 54 | uses: codecov/codecov-action@v2 55 | with: 56 | fail_ci_if_error: false 57 | -------------------------------------------------------------------------------- /.github/workflows/generate_docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: {} 6 | 7 | jobs: 8 | generate-documentation: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - name: Generate documentation 14 | run: | 15 | sudo apt-get install doxygen graphviz 16 | bash -ex generate-docs.sh "${{ secrets.GITHUB_TOKEN }}" "gh-pages" -------------------------------------------------------------------------------- /.github/workflows/test-release.yml: -------------------------------------------------------------------------------- 1 | name: TestPyPI release 2 | 3 | env: 4 | version: 9.0.0 5 | 6 | 7 | # runs only when a release is published, not on drafts 8 | on: 9 | workflow_dispatch: 10 | 11 | 12 | jobs: 13 | deploy-packges-and-generate-documentation: 14 | runs-on: ubuntu-22.04 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install dependencies (SCIPOptSuite) 19 | run: | 20 | wget --quiet --no-check-certificate https://github.com/scipopt/scip/releases/download/$(echo "v${{env.version}}" | tr -d '.')/SCIPOptSuite-${{ env.version }}-Linux-ubuntu20.deb 21 | sudo apt-get update && sudo apt install -y ./SCIPOptSuite-${{ env.version }}-Linux-ubuntu20.deb 22 | 23 | - name: Setup python 3 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.x' 27 | 28 | - name: Prepare python environment 29 | run: | 30 | python -m pip install --upgrade pip 31 | python -m pip install wheel cython networkx pytest-cov build 32 | 33 | - name: Build package 34 | run: | 35 | python -m build --sdist --no-isolation --outdir dist/ 36 | 37 | - name: Generate documentation 38 | run: | 39 | sudo apt-get install doxygen graphviz 40 | bash -ex generate-docs.sh "${{ secrets.GITHUB_TOKEN }}" "gh-pages" 41 | 42 | - name: "Publish to test.pypi.org" 43 | uses: pypa/gh-action-pypi-publish@release/v1 44 | with: 45 | repository_url: https://test.pypi.org/legacy/ 46 | user: __token__ 47 | password: ${{ secrets.TESTPYPI_API_TOKEN }} 48 | verbose: true 49 | packages_dir: dist/ 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project specific 2 | *~ 3 | *.swp 4 | *.swo 5 | build/ 6 | dist/ 7 | include 8 | lib 9 | src/pyscipopt/scip.c 10 | __pycache__ 11 | .cache 12 | .idea 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # macOS dir files 23 | .DS_Store 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | .static_storage/ 72 | .media/ 73 | local_settings.py 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | docs/_autosummary/ 85 | 86 | # PyBuilder 87 | target/ 88 | 89 | # Jupyter Notebook 90 | .ipynb_checkpoints 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | venv3/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | 124 | # pytest 125 | .pytest_cache/ 126 | 127 | # model (for tests) 128 | model 129 | model.cip 130 | model.lp 131 | 132 | # VSCode 133 | .vscode/ 134 | .devcontainer/ 135 | -------------------------------------------------------------------------------- /.landscape.yml: -------------------------------------------------------------------------------- 1 | doc-warnings: true 2 | test-warnings: true 3 | ignore-paths: 4 | - examples/unfinished 5 | - src/pyscipopt/__init__.py 6 | python-targets: 7 | - 2 8 | - 3 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | builder: html 5 | configuration: docs/conf.py 6 | 7 | build: 8 | os: "ubuntu-22.04" 9 | tools: 10 | python: "3.11" 11 | 12 | formats: all 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @incollection{MaherMiltenbergerPedrosoRehfeldtSchwarzSerrano2016, 2 | author = {Stephen Maher and Matthias Miltenberger and Jo{\~{a}}o Pedro Pedroso and Daniel Rehfeldt and Robert Schwarz and Felipe Serrano}, 3 | title = {{PySCIPOpt}: Mathematical Programming in Python with the {SCIP} Optimization Suite}, 4 | booktitle = {Mathematical Software {\textendash} {ICMS} 2016}, 5 | publisher = {Springer International Publishing}, 6 | pages = {301--307}, 7 | year = {2016}, 8 | doi = {10.1007/978-3-319-42432-3_37}, 9 | } 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zuse Institute Berlin (ZIB) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | The following are the steps to follow to make a new PySCIPOpt release. They should mostly be done in order. 3 | - [ ] Check if [scipoptsuite-deploy](https://github.com/scipopt/scipoptsuite-deploy) needs a new release, if a new SCIP version is released for example, or new dependencies (change symmetry dependency, add support for papilo/ parallelization.. etc). And Update release links in `pyproject.toml` 4 | - [ ] Check if the table in the [documentation](https://pyscipopt.readthedocs.io/en/latest/build.html#building-from-source) needs to be updated. 5 | - [ ] Update version number according to semantic versioning [rules](https://semver.org/) in `src/pyscipopt/_version.py` and `setup.py` 6 | - [ ] Update `CHANGELOG.md`; Change the `Unreleased` to the new version number and add an empty unreleased section. 7 | - [ ] Create a release candidate on test-pypi by running the workflow “Build wheels” in Actions->build wheels, with these parameters `upload:true, test-pypi:true`  8 | - [ ] If the pipeline passes, test the released pip package on test-pypi by running and checking that it works 9 | ```bash 10 | pip install -i https://test.pypi.org/simple/ PySCIPOpt 11 | ``` 12 | - [ ] If it works, release on pypi.org with running the same workflow but with `test-pypi:false`. 13 | - [ ] Then create a tag with the new version (from the master branch) 14 | ```bash 15 | git tag vX.X.X 16 | git push origin vX.X.X 17 | ``` 18 | - [ ] Then make a github [release](https://github.com/scipopt/PySCIPOpt/releases/new) from this new tag. 19 | - [ ] Update the documentation: from readthedocs.io -> Builds -> Build version (latest and stable) 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/scip_structure_landscape-compressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/37a1ed6da9a7ebb43a643daca2ff94b3e72f82b8/docs/_static/scip_structure_landscape-compressed.png -------------------------------------------------------------------------------- /docs/_static/skippy_logo_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/37a1ed6da9a7ebb43a643daca2ff94b3e72f82b8/docs/_static/skippy_logo_blue.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | API Reference 3 | ############# 4 | 5 | This page provides an auto-generated summary of PySCIPOpt's API. 6 | 7 | .. automodule:: pyscipopt 8 | 9 | SCIP Model 10 | ========== 11 | 12 | This is the main class of PySCIPOpt. Most functionality is accessible through functions 13 | of this class. All functions that require the SCIP object belong to this class. 14 | 15 | .. toctree:: 16 | :maxdepth: 1 17 | 18 | api/model 19 | 20 | SCIP Constraint 21 | =============== 22 | 23 | This class wraps a SCIP constraint object. It contains functions that can retrieve basic information 24 | that is entirely contained within the constraint object. 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | api/constraint 30 | 31 | SCIP Variable 32 | ============= 33 | 34 | This class wraps a SCIP variable object. It contains functions that can retrieve basic information 35 | that is entirely contained within the variable object. 36 | 37 | .. toctree:: 38 | :maxdepth: 1 39 | 40 | api/variable 41 | 42 | SCIP Row 43 | ======== 44 | 45 | This class wraps a SCIP row object. It contains functions that can retrieve basic information 46 | that is entirely contained within the row object. 47 | 48 | .. toctree:: 49 | :maxdepth: 1 50 | 51 | api/row 52 | 53 | SCIP Column 54 | =========== 55 | 56 | This class wraps a SCIP column object. It contains functions that can retrieve basic information 57 | that is entirely contained within the column object. 58 | 59 | .. toctree:: 60 | :maxdepth: 1 61 | 62 | api/column 63 | 64 | SCIP Node 65 | ========= 66 | 67 | This class wraps a SCIP node object. It contains functions that can retrieve basic information 68 | that is entirely contained within the node object. 69 | 70 | .. toctree:: 71 | :maxdepth: 1 72 | 73 | api/node 74 | 75 | SCIP Event 76 | ========== 77 | 78 | This class wraps a SCIP event object. It contains functions that can retrieve basic information 79 | that is entirely contained within the event object. 80 | 81 | .. toctree:: 82 | :maxdepth: 1 83 | 84 | api/event 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/api/column.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Column API 3 | ########## 4 | 5 | .. autoclass:: pyscipopt.scip.Column 6 | :members: -------------------------------------------------------------------------------- /docs/api/constraint.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | Constraint API 3 | ############## 4 | 5 | .. autoclass:: pyscipopt.Constraint 6 | :members: -------------------------------------------------------------------------------- /docs/api/event.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Event API 3 | ########## 4 | 5 | .. autoclass:: pyscipopt.scip.Event 6 | :members: -------------------------------------------------------------------------------- /docs/api/model.rst: -------------------------------------------------------------------------------- 1 | ######### 2 | Model API 3 | ######### 4 | 5 | .. autoclass:: pyscipopt.Model 6 | :members: -------------------------------------------------------------------------------- /docs/api/node.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Node API 3 | ######## 4 | 5 | .. autoclass:: pyscipopt.scip.Node 6 | :members: -------------------------------------------------------------------------------- /docs/api/row.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Row API 3 | ####### 4 | 5 | .. autoclass:: pyscipopt.scip.Row 6 | :members: -------------------------------------------------------------------------------- /docs/api/variable.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Variable API 3 | ############ 4 | 5 | .. autoclass:: pyscipopt.Variable 6 | :members: -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import pyscipopt 16 | 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | sys.path.insert(0, os.path.abspath("../src/")) 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "PySCIPOpt" 24 | copyright = "2024, Zuse Institute Berlin" 25 | author = "Zuse Institute Berlin " 26 | html_logo = "_static/skippy_logo_blue.png" 27 | html_favicon = '_static/skippy_logo_blue.png' 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.napoleon", 38 | "sphinx.ext.autosummary", 39 | "sphinx.ext.autosectionlabel", 40 | "sphinxcontrib.bibtex", 41 | "sphinxcontrib.jquery", 42 | "sphinx.ext.mathjax", 43 | "sphinx.ext.intersphinx", 44 | "sphinx.ext.extlinks", 45 | ] 46 | 47 | # You can define documentation here that will be repeated often 48 | # e.g. XXX = """WARNING: This method will only work if your model has status=='optimal'""" 49 | # rst_epilog = f""" .. |XXX| replace:: {XXX}""" 50 | # Then in the documentation of a method you can put |XXX| 51 | 52 | autosectionlabel_prefix_document = True 53 | 54 | # Add any paths that contain templates here, relative to this directory. 55 | templates_path = ["_templates"] 56 | 57 | # List of patterns, relative to source directory, that match files and 58 | # directories to ignore when looking for source files. 59 | # This pattern also affects html_static_path and html_extra_path. 60 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 61 | 62 | # The intersphinx mapping dictionary is used to automatically reference 63 | # python object types to their respective documentation. 64 | # As PySCIPOpt has few dependencies this is not done. 65 | 66 | intersphinx_mapping = {} 67 | 68 | extlinks = { 69 | "pypi": ("https://pypi.org/project/%s/", "%s"), 70 | } 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = "sphinx_book_theme" 79 | 80 | # Add any paths that contain custom static files (such as style sheets) here, 81 | # relative to this directory. They are copied after the builtin static files, 82 | # so a file named "default.css" will overwrite the builtin "default.css". 83 | html_static_path = ["_static"] 84 | 85 | autosummary_generate = False 86 | napoleon_numpy_docstring = True 87 | napoleon_google_docstring = False 88 | 89 | pygments_style = "sphinx" 90 | 91 | bibtex_bibfiles = ["ref.bib"] 92 | -------------------------------------------------------------------------------- /docs/contributors.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Contributors 3 | ############ 4 | 5 | .. contents:: Contents 6 | 7 | List of SCIP Authors 8 | ==================== 9 | 10 | A list of current and past SCIP developers can be found `here `__. 11 | 12 | 13 | List of PySCIPOpt Contributors 14 | ============================== 15 | 16 | A list of all contributors to PySCIPOpt can be found 17 | `here `__. 18 | 19 | 20 | How to Cite PySCIPOpt / SCIP 21 | ============================ 22 | 23 | When using PySCIPOpt as part of your research, please cite the following paper: 24 | 25 | .. code-block:: RST 26 | 27 | @incollection{MaherMiltenbergerPedrosoRehfeldtSchwarzSerrano2016, 28 | author = {Stephen Maher and Matthias Miltenberger and Jo{\~{a}}o Pedro Pedroso and Daniel Rehfeldt and Robert Schwarz and Felipe Serrano}, 29 | title = {{PySCIPOpt}: Mathematical Programming in Python with the {SCIP} Optimization Suite}, 30 | booktitle = {Mathematical Software {\textendash} {ICMS} 2016}, 31 | publisher = {Springer International Publishing}, 32 | pages = {301--307}, 33 | year = {2016}, 34 | doi = {10.1007/978-3-319-42432-3_37}, 35 | } 36 | 37 | Additionally, please cite the corresponding SCIP Optimization Suite report, which can be found 38 | `here `__. 39 | 40 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | ############################ 2 | Frequently Asked Questions 3 | ############################ 4 | 5 | In this page we will provide some answers to commonly asked questions by users of PySCIPOpt. 6 | 7 | How do I set parameter values? 8 | ============================== 9 | 10 | See the appropriate section on :doc:`this page `. 11 | 12 | How do I know the order that the different plug-ins will be called? 13 | =================================================================== 14 | 15 | Each rule for a given plug-in is called in order of their priority. 16 | To ensure that your custom rule is called first it must have a higher 17 | priority than all other rules of that plug-in. 18 | 19 | Problems with dual values? 20 | ========================== 21 | 22 | See the appropriate section on :doc:`this page `. Short answer: SCIP cannot 23 | guarantee accurate precise dual values without certain parameter settings. 24 | 25 | Constraints with both LHS and RHS (ranged constraints) 26 | ====================================================== 27 | 28 | A ranged constraint takes the form: 29 | 30 | .. code-block:: python 31 | 32 | lhs <= expression <= rhs 33 | 34 | While these are supported, the Python syntax for chained comparisons can't be hijacked with operator overloading. 35 | Instead, parenthesis must be used when adding your own ranged constraints, e.g., 36 | 37 | .. code-block:: python 38 | 39 | lhs <= (expression <= rhs) 40 | 41 | Alternatively, you may call ``scip.chgRhs(cons, newrhs)`` or ``scip.chgLhs(cons, newlhs)`` after the single-sided 42 | constraint has been created. 43 | 44 | .. note:: Be careful of constant expressions being rearranged by Python when handling ranged consraints. 45 | 46 | My model crashes when I make changes to it after optimizing 47 | =========================================================== 48 | 49 | When dealing with an already optimized model, and you want to make changes, e.g., add a new 50 | constraint or change the objective, please use the following command: 51 | 52 | .. code-block:: python 53 | 54 | scip.freeTransform() 55 | 56 | Without calling this function the Model can only be queried in its post optimized state. 57 | This is because the transformed problem and all the previous solving information 58 | is not automatically deleted, and thus stops a new optimization call. 59 | 60 | Why can I not add a non-linear objective? 61 | ========================================= 62 | 63 | SCIP does not support non-linear objectives. However, an equivalent optimization 64 | problem can easily be constructed by introducing a single new variable and a constraint. 65 | Please see :doc:`this page ` for a guide. -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ######################### 2 | PySCIPOpt Documentation 3 | ######################### 4 | 5 | 6 | PySCIPOpt is the Python interface to `SCIP (Solving Constraint Integer Programs) `_. 7 | 8 | 9 | ******** 10 | Contents 11 | ******** 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :titlesonly: 16 | 17 | install 18 | build 19 | tutorials/index 20 | whyscip 21 | extend 22 | similarsoftware 23 | faq 24 | contributors 25 | api 26 | 27 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ################## 2 | Installation Guide 3 | ################## 4 | 5 | This page will detail all methods for installing PySCIPOpt via package managers, 6 | which come with their own versions of SCIP. For building PySCIPOpt against your 7 | own custom version of SCIP, or for building PySCIPOpt from source, visit :doc:`this page `. 8 | 9 | .. contents:: Contents 10 | 11 | 12 | PyPI (pip) 13 | ============ 14 | 15 | Pre-built binary wheels are uploaded to PyPI (Python Package Index) for each release. 16 | Supported platforms are Linux (x86_64), Windows (x86_64) and MacOS (x86_64, Apple Silicon). 17 | 18 | To install PySCIPOpt simply run the command: 19 | 20 | .. code-block:: bash 21 | 22 | pip install pyscipopt 23 | 24 | To avoid interfering with system packages, it's best to use a `virtual environment `. 25 | 26 | .. warning:: 27 | 28 | Using a virtual environment is **mandatory** in some newer Python configurations 29 | to avoid permission and package conflicts. 30 | 31 | .. code-block:: bash 32 | python3 -m venv venv 33 | source venv/bin/activate # On Windows use: venv\Scripts\activate 34 | pip install pyscipopt 35 | 36 | Remember to activate the environment (``source venv/bin/activate``) in each terminal session where you use PySCIPOpt. 37 | 38 | .. note:: For Linux users: PySCIPOpt versions newer than 5.1.1 installed via PyPI now require glibc 2.28+ 39 | 40 | For our build infrastructure we use `manylinux `_. 41 | As CentOS 7 is no longer supported, we have migrated from ``manylinux2014`` to ``manylinux_2_28``. 42 | 43 | TLDR: Older linux distributions may not work for newer versions of PySCIPOpt installed via pip. 44 | 45 | .. note:: For Mac users: PySCIPOpt versions newer than 5.1.1 installed via PyPI now only support 46 | MACOSX 13+ for users running x86_64 architecture, and MACOSX 14+ for users running newer Apple silicon. 47 | 48 | .. note:: For versions older than 4.4.0 installed via PyPI SCIP is not automatically installed. 49 | This means that SCIP must be installed yourself. If it is not installed globally, 50 | then the ``SCIPOPTDIR`` environment flag must be set, see :doc:`this page ` for more details. 51 | 52 | .. note:: Some Mac configurations require adding the library installation path to `DYLD_LIBRARY_PATH` when 53 | using a locally installed version of SCIP. 54 | 55 | Conda 56 | ===== 57 | 58 | It is also possible to use the Conda package manager to install PySCIPOpt. 59 | Conda will install SCIP automatically, hence everything can be installed in a single command: 60 | 61 | .. code-block:: bash 62 | 63 | conda install --channel conda-forge pyscipopt 64 | 65 | .. note:: Do not use the Conda base environment to install PySCIPOpt. 66 | 67 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-jquery 3 | sphinxcontrib-bibtex 4 | sphinx-book-theme 5 | pyscipopt -------------------------------------------------------------------------------- /docs/tutorials/cutselector.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Cut Selector 3 | ############ 4 | 5 | For the following let us assume that a Model object is available, which is created as follows: 6 | 7 | .. code-block:: python 8 | 9 | from pyscipopt import Model, SCIP_RESULT 10 | from pyscipopt.scip import Cutsel 11 | 12 | scip = Model() 13 | 14 | .. contents:: Contents 15 | 16 | What is a Cut Selector? 17 | ======================== 18 | 19 | A cutting plane (cut) selector is an algorithm that selects which cuts to add to the 20 | optimization problem. It is given a set of candidate cuts, and from the set must decide which 21 | subset to add. 22 | 23 | Cut Selector Structure 24 | ======================= 25 | 26 | A cut selector in PySCIPOpt takes the following structure: 27 | 28 | .. code-block:: python 29 | 30 | class DummyCutsel(Cutsel): 31 | 32 | def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): 33 | """ 34 | :param cuts: the cuts which we want to select from. Is a list of scip Rows 35 | :param forcedcuts: the cuts which we must add. Is a list of scip Rows 36 | :param root: boolean indicating whether we are at the root node 37 | :param maxnselectedcuts: int which is the maximum amount of cuts that can be selected 38 | :return: sorted cuts and forcedcuts 39 | """ 40 | 41 | return {'cuts': sorted_cuts, 'nselectedcuts': n, 42 | 'result': SCIP_RESULT.SUCCESS} 43 | 44 | The class ``DummyCutsel`` inherits the necessary ``Cutsel`` class, and then programs 45 | the necessary function ``cutselselect``. The docstrings of the ``cutselselect`` explain 46 | the input to the function. It is then up to the user to create some new ordering ``cuts``, 47 | which we have represented by ``sorted_cuts``. The returned value of ``nselectedcuts`` results in the first 48 | ``nselectedcuts`` of the ``sorted_cuts`` being added to the optimization problem. The 49 | ``SCIP_RESULT`` is there to indicate whether the algorithm was successful. See the 50 | appropriate documentation for more potential result codes. 51 | 52 | To include a cut selector one would need to do something like the following code: 53 | 54 | .. code-block:: python 55 | 56 | cutsel = DummyCutsel() 57 | scip.includeCutsel(cutsel, 'name', 'description', 5000000) 58 | 59 | The final argument of the ``includeCutsel`` function in the example above was the 60 | priority. If the priority is higher than all other cut selectors then it will be called 61 | first. In the case of some failure or non-success return code, then the second-highest 62 | priority cut selector is called and so on. 63 | 64 | Example Cut Selector 65 | ====================== 66 | 67 | In this example we will program a cut selector that selects the 10 most 68 | efficacious cuts. Efficacy is the standard measure for cut quality and can be calculated 69 | via SCIP directly. 70 | 71 | .. code-block:: python 72 | 73 | class MaxEfficacyCutsel(Cutsel): 74 | 75 | def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): 76 | """ 77 | Selects the 10 cuts with largest efficacy. 78 | """ 79 | 80 | scip = self.model 81 | 82 | scores = [0] * len(cuts) 83 | for i in range(len(scores)): 84 | scores[i] = scip.getCutEfficacy(cuts[i]) 85 | 86 | rankings = sorted(range(len(cuts)), key=lambda x: scores[x], reverse=True) 87 | 88 | sorted_cuts = [cuts[rank] for rank in rankings] 89 | 90 | assert len(sorted_cuts) == len(cuts) 91 | 92 | return {'cuts': sorted_cuts, 'nselectedcuts': min(maxnselectedcuts, len(cuts), 10), 93 | 'result': SCIP_RESULT.SUCCESS} 94 | 95 | 96 | Things to Keep in Mind 97 | ======================= 98 | 99 | Here are some things to keep in mind when programming your own custom cut selector. 100 | 101 | - Do not change any of the actual cut information! 102 | - Do not reorder the ``forcedcuts``. They are provided as reference points to inform 103 | the selection process. They should not be edited or reordered. 104 | - Only reorder ``cuts``. Do not add any new cuts. 105 | -------------------------------------------------------------------------------- /docs/tutorials/eventhandler.rst: -------------------------------------------------------------------------------- 1 | ############### 2 | Event Handlers 3 | ############### 4 | 5 | For the following let us assume that a Model object is available, which is created as follows: 6 | 7 | .. code-block:: python 8 | 9 | from pyscipopt import Model 10 | 11 | scip = Model() 12 | 13 | .. contents:: Contents 14 | 15 | SCIP Events 16 | =========== 17 | SCIP provides a number of events that can be used to interact with the solver. These events would describe a change in the model state before or during the solving process; For example a variable/constraint were added or a new best solution is found. 18 | The enum :code:`pyscipopt.SCIP_EVENTTYPE` provides a list of all available events. 19 | 20 | 21 | What's an Event Handler? 22 | ======================== 23 | Event handlers are used to react to events that occur during the solving process. 24 | They are registered with the solver and are called whenever an event occurs. 25 | The event handler can then react to the event by performing some action. 26 | For example, an event handler can be used to update the incumbent solution whenever a new best solution is found. 27 | 28 | 29 | Adding Event Handlers with Callbacks 30 | ==================================== 31 | 32 | The easiest way to create an event handler is providing a callback function to the model using the :code:`Model.attachEventHandlerCallback` method. 33 | The following is an example the prints the value of the objective function whenever a new best solution is found: 34 | 35 | .. code-block:: python 36 | 37 | from pyscipopt import Model, SCIP_EVENTTYPE 38 | 39 | def print_obj_value(model, event): 40 | print("New best solution found with objective value: {}".format(model.getObjVal())) 41 | 42 | m = Model() 43 | m.attachEventHandlerCallback(print_obj_value, [SCIP_EVENTTYPE.BESTSOLFOUND]) 44 | m.optimize() 45 | 46 | 47 | The callback function should have the following signature: :code:`def callback(model, event)`. 48 | The first argument is the model object and the second argument is the event that occurred. 49 | 50 | 51 | Adding Event Handlers with Classes 52 | ================================== 53 | 54 | If you need to store additional data in the event handler, you can create a custom event handler class that inherits from :code:`pyscipopt.Eventhdlr`, 55 | and then include it in the model using the :code:`Model.includeEventHandler` method. The following is an example that stores the number of best solutions found: 56 | 57 | .. code-block:: python 58 | 59 | from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE 60 | 61 | 62 | class BestSolCounter(Eventhdlr): 63 | def __init__(self, model): 64 | Eventhdlr.__init__(model) 65 | self.count = 0 66 | 67 | def eventinit(self): 68 | self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) 69 | 70 | def eventexit(self): 71 | self.model.dropEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) 72 | 73 | def eventexec(self, event): 74 | self.count += 1 75 | print("!!!![@BestSolCounter] New best solution found. Total best solutions found: {}".format(self.count)) 76 | 77 | 78 | m = Model() 79 | best_sol_counter = BestSolCounter(m) 80 | m.includeEventhdlr(best_sol_counter, "best_sol_event_handler", "Event handler that counts the number of best solutions found") 81 | m.optimize() 82 | assert best_sol_counter.count == 1 83 | 84 | -------------------------------------------------------------------------------- /docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | ###################### 2 | User Guide (Tutorials) 3 | ###################### 4 | 5 | This section contains official tutorials (examples) for PySCIPOpt. Please keep in mind 6 | that PySCIPOpt's primary purpose is as a wrapper for SCIP. Therefore, for sometimes 7 | more detailed information see `this page `_. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | :caption: Contents: 12 | 13 | model 14 | vartypes 15 | constypes 16 | expressions 17 | matrix 18 | readwrite 19 | logfile 20 | branchrule 21 | cutselector 22 | separator 23 | heuristic 24 | nodeselector 25 | lazycons 26 | eventhandler 27 | scipdex -------------------------------------------------------------------------------- /docs/tutorials/nodeselector.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | Node Selector 3 | ############# 4 | 5 | For the following let us assume that a Model object is available, which is created as follows: 6 | 7 | .. code-block:: python 8 | 9 | from pyscipopt import Model 10 | from pyscipopt.scip import Nodesel 11 | 12 | scip = Model() 13 | 14 | .. contents:: Contents 15 | 16 | What is a Node Selector? 17 | ======================== 18 | 19 | In the branch-and-bound tree an important question that must be answered is which node should currently 20 | be processed. That is, given a branch-and-bound tree in an intermediate state, select a leaf node of the tree 21 | that will be processed next (most likely branched on). In SCIP this problem has its own plug-in, 22 | and thus custom algorithms can easily be included into the solving process! 23 | 24 | Example Node Selector 25 | ===================== 26 | 27 | In this example we are going to implement a depth first search node selection strategy. 28 | There are two functions that we need to code ourselves when adding such a rule from python. 29 | The first is the strategy on which node to select from all the current leaves, and the other 30 | is a comparison function that decides which node is preferred from two candidates. 31 | 32 | .. code-block:: python 33 | 34 | # Depth First Search Node Selector 35 | class DFS(Nodesel): 36 | 37 | def __init__(self, scip, *args, **kwargs): 38 | super().__init__(*args, **kwargs) 39 | self.scip = scip 40 | 41 | def nodeselect(self): 42 | """Decide which of the leaves from the branching tree to process next""" 43 | selnode = self.scip.getPrioChild() 44 | if selnode is None: 45 | selnode = self.scip.getPrioSibling() 46 | if selnode is None: 47 | selnode = self.scip.getBestLeaf() 48 | 49 | return {"selnode": selnode} 50 | 51 | def nodecomp(self, node1, node2): 52 | """ 53 | compare two leaves of the current branching tree 54 | 55 | It should return the following values: 56 | 57 | value < 0, if node 1 comes before (is better than) node 2 58 | value = 0, if both nodes are equally good 59 | value > 0, if node 1 comes after (is worse than) node 2. 60 | """ 61 | depth_1 = node1.getDepth() 62 | depth_2 = node2.getDepth() 63 | if depth_1 > depth_2: 64 | return -1 65 | elif depth_1 < depth_2: 66 | return 1 67 | else: 68 | lb_1 = node1.getLowerbound() 69 | lb_2 = node2.getLowerbound() 70 | if lb_1 < lb_2: 71 | return -1 72 | elif lb_1 > lb_2: 73 | return 1 74 | else: 75 | return 0 76 | 77 | .. note:: In general when implementing a node selection rule you will commonly use either ``getPrioChild`` 78 | or ``getBestChild``. The first returns the child of the current node with 79 | the largest node selection priority, as assigned by the branching rule. The second 80 | returns the best child of the current node with respect to the node selector's ordering relation as defined 81 | in ``nodecomp``. 82 | 83 | To include the node selector in your SCIP Model one would use the following code: 84 | 85 | .. code-block:: python 86 | 87 | dfs_node_sel = DFS(scip) 88 | scip.includeNodesel(dfs_node_sel, "DFS", "Depth First Search Nodesel.", 1000000, 1000000) 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/tutorials/readwrite.rst: -------------------------------------------------------------------------------- 1 | ##################### 2 | Read and Write Files 3 | ##################### 4 | 5 | While building your own optimization problem is fun, at some point you often need to share it, or 6 | read an optimization problem written by someone else. For this you're going to need to 7 | write the optimization problem to some file or read it from some file. 8 | 9 | .. contents:: Contents 10 | 11 | Model File Formats 12 | ===================== 13 | 14 | SCIP has extensive support for a wide variety of file formats. The table below outlines 15 | what formats those are and the model types they're associated with. 16 | 17 | .. list-table:: Supported File Formats 18 | :widths: 25 25 19 | :align: center 20 | :header-rows: 1 21 | 22 | * - Extension 23 | - Model Type 24 | * - CIP 25 | - SCIP's constraint integer programming format 26 | * - CNF 27 | - DIMACS CNF (conjunctive normal form) format used for example for SAT problems 28 | * - DIFF 29 | - reading a new objective function for mixed-integer programs 30 | * - FZN 31 | - FlatZinc is a low-level solver input language that is the target language for MiniZinc 32 | * - GMS 33 | - mixed-integer nonlinear programs (GAMS) [reading requires compilation with GAMS=true and a working GAMS system] 34 | * - LP 35 | - mixed-integer (quadratically constrained quadratic) programs (CPLEX) 36 | * - MPS 37 | - mixed-integer (quadratically constrained quadratic) programs 38 | * - OPB 39 | - pseudo-Boolean optimization instances 40 | * - OSiL 41 | - mixed-integer nonlinear programs 42 | * - PIP 43 | - mixed-integer polynomial programming problems 44 | * - SOL 45 | - solutions; XML-format (read-only) or raw SCIP format 46 | * - WBO 47 | - weighted pseudo-Boolean optimization instances 48 | * - ZPL 49 | - ZIMPL models, i.e., mixed-integer linear and nonlinear programming problems [read only] 50 | 51 | 52 | .. note:: In general we recommend sharing files using the ``.mps`` extension when possible. 53 | 54 | For a more human-readable format for equivalent problems we recommend the ``.lp`` extension. 55 | 56 | For general non-linearities that are to be shared with others we recommend the ``.osil`` extension. 57 | 58 | For general constraint types that will only be used by other SCIP users we recommend the ``.cip`` extension. 59 | 60 | .. note:: Some of these file formats may only have a reader programmed and not a writer. Additionally, 61 | some of these readers may require external plug-ins that are not shipped by default via PyPI. 62 | 63 | Write a Model 64 | ================ 65 | 66 | To write a SCIP Model to a file one simply needs to run the command: 67 | 68 | .. code-block:: python 69 | 70 | from pyscipopt import Model 71 | scip = Model() 72 | scip.writeProblem(filename="example_file.mps", trans=False, genericnames=False) 73 | 74 | .. note:: Both ``trans`` and ``genericnames`` are there as their default values. The ``trans`` 75 | option is available if you want to print the transformed problem (post presolve) instead 76 | of the model originally created. The ``genericnames`` option is there if you want to overwrite 77 | the variable and constraint names provided. 78 | 79 | Read a Model 80 | =============== 81 | 82 | To read in a file to a SCIP model one simply needs to run the command: 83 | 84 | .. code-block:: python 85 | 86 | from pyscipopt import Model 87 | scip = Model() 88 | scip.readProblem(filename="example_file.mps") 89 | 90 | This will read in the file and you will now have a SCIP model that matches the file. 91 | Variables and constraints can be queried, with their names matching those in the file. 92 | -------------------------------------------------------------------------------- /docs/tutorials/scipdex.rst: -------------------------------------------------------------------------------- 1 | ################### 2 | Hands On Tutorials 3 | ################### 4 | 5 | For more tutorials with PySCIPOpt, especially interactive ones, please see 6 | `SCIPDex `_. SCIPDex is a collection of 7 | interactive exercises to get you started (and more) with PySCIPOpt -------------------------------------------------------------------------------- /examples/finished/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/37a1ed6da9a7ebb43a643daca2ff94b3e72f82b8/examples/finished/__init__.py -------------------------------------------------------------------------------- /examples/finished/categorical_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how one can optimize a model with categorical data by converting it into integers. 3 | 4 | There are three employees (Alice, Bob, Charlie) and three shifts. Each shift is assigned an integer: 5 | 6 | Morning - 0 7 | Afternoon - 1 8 | Night - 2 9 | 10 | The employees have availabilities (e.g. Alice can only work in the Morning and Afternoon), and different 11 | salary demands. These constraints, and an additional one stipulating that every shift must be covered, 12 | allows us to model a MIP with the objective of minimizing the money spent on salary. 13 | """ 14 | 15 | from pyscipopt import Model 16 | 17 | # Define categorical data 18 | shift_to_int = {"Morning": 0, "Afternoon": 1, "Night": 2} 19 | employees = ["Alice", "Bob", "Charlie"] 20 | 21 | # Employee availability 22 | availability = { 23 | "Alice": ["Morning", "Afternoon"], 24 | "Bob": ["Afternoon", "Night"], 25 | "Charlie": ["Morning", "Night"] 26 | } 27 | 28 | # Transform availability into integer values 29 | availability_int = {} 30 | for emp, available_shifts in availability.items(): 31 | availability_int[emp] = [shift_to_int[shift] for shift in available_shifts] 32 | 33 | 34 | # Employees have different salary demands 35 | cost = { 36 | "Alice": [2,4,1], 37 | "Bob": [3,2,7], 38 | "Charlie": [3,3,3] 39 | } 40 | 41 | # Create the model 42 | model = Model("Shift Assignment") 43 | 44 | # x[e, s] = 1 if employee e is assigned to shift s 45 | x = {} 46 | for e in employees: 47 | for s in shift_to_int.values(): 48 | x[e, s] = model.addVar(vtype="B", name=f"x({e},{s})") 49 | 50 | # Each shift must be assigned to exactly one employee 51 | for s in shift_to_int.values(): 52 | model.addCons(sum(x[e, s] for e in employees) == 1) 53 | 54 | # Each employee must be assigned to exactly one shift 55 | for e in employees: 56 | model.addCons(sum(x[e, s] for s in shift_to_int.values()) == 1) 57 | 58 | # Employees can only work shifts they are available for 59 | for e in employees: 60 | for s in shift_to_int.values(): 61 | if s not in availability_int[e]: 62 | model.addCons(x[e, s] == 0) 63 | 64 | # Minimize shift assignment cost 65 | model.setObjective( 66 | sum(cost[e][s]*x[e, s] for e in employees for s in shift_to_int.values()), "minimize" 67 | ) 68 | 69 | # Solve the problem 70 | model.optimize() 71 | 72 | # Display the results 73 | print("\nOptimal Shift Assignment:") 74 | for e in employees: 75 | for s, s_id in shift_to_int.items(): 76 | if model.getVal(x[e, s_id]) > 0.5: 77 | print("%s is assigned to %s" % (e, s)) 78 | -------------------------------------------------------------------------------- /examples/finished/eoq_en.py: -------------------------------------------------------------------------------- 1 | ##@file eoq_en.py 2 | # @brief piecewise linear model to the multi-item economic ordering quantity problem. 3 | """ 4 | Approach: use a convex combination formulation. 5 | 6 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 7 | """ 8 | from pyscipopt import Model, quicksum, multidict 9 | 10 | 11 | def eoq(I, F, h, d, w, W, a0, aK, K): 12 | """eoq -- multi-item capacitated economic ordering quantity model 13 | Parameters: 14 | - I: set of items 15 | - F[i]: ordering cost for item i 16 | - h[i]: holding cost for item i 17 | - d[i]: demand for item i 18 | - w[i]: unit weight for item i 19 | - W: capacity (limit on order quantity) 20 | - a0: lower bound on the cycle time (x axis) 21 | - aK: upper bound on the cycle time (x axis) 22 | - K: number of linear pieces to use in the approximation 23 | Returns a model, ready to be solved. 24 | """ 25 | 26 | # construct points for piecewise-linear relation, store in a,b 27 | a, b = {}, {} 28 | delta = float(aK - a0) / K 29 | for i in I: 30 | for k in range(K): 31 | T = a0 + delta * k 32 | a[i, k] = T # abscissa: cycle time 33 | b[i, k] = F[i] / T + h[i] * d[i] * T / 2. # ordinate: (convex) cost for this cycle time 34 | 35 | model = Model("multi-item, capacitated EOQ") 36 | 37 | x, c, w_ = {}, {}, {} 38 | for i in I: 39 | x[i] = model.addVar(vtype="C", name="x(%s)" % i) # cycle time for item i 40 | c[i] = model.addVar(vtype="C", name="c(%s)" % i) # total cost for item i 41 | for k in range(K): 42 | w_[i, k] = model.addVar(ub=1, vtype="C", name="w(%s,%s)" % (i, k)) # todo ?? 43 | 44 | for i in I: 45 | model.addCons(quicksum(w_[i, k] for k in range(K)) == 1) 46 | model.addCons(quicksum(a[i, k] * w_[i, k] for k in range(K)) == x[i]) 47 | model.addCons(quicksum(b[i, k] * w_[i, k] for k in range(K)) == c[i]) 48 | 49 | model.addCons(quicksum(w[i] * d[i] * x[i] for i in I) <= W) 50 | 51 | model.setObjective(quicksum(c[i] for i in I), "minimize") 52 | 53 | model.data = x, w 54 | return model 55 | 56 | 57 | if __name__ == "__main__": 58 | # multiple item EOQ 59 | I, F, h, d, w = multidict( 60 | {1: [300, 10, 10, 20], 61 | 2: [300, 10, 30, 40], 62 | 3: [300, 10, 50, 10]} 63 | ) 64 | W = 2000 65 | K = 1000 66 | a0, aK = 0.1, 10 67 | model = eoq(I, F, h, d, w, W, a0, aK, K) 68 | model.optimize() 69 | 70 | x, w = model.data 71 | EPS = 1.e-6 72 | for v in x: 73 | if model.getVal(x[v]) >= EPS: 74 | print(x[v].name, "=", model.getVal(x[v])) 75 | 76 | print("Optimal value:", model.getObjVal()) 77 | -------------------------------------------------------------------------------- /examples/finished/even.py: -------------------------------------------------------------------------------- 1 | ##@file finished/even.py 2 | # @brief model to decide whether argument is even or odd 3 | 4 | 5 | ################################################################################ 6 | # 7 | # EVEN OR ODD? 8 | # 9 | # If a positional argument is given: 10 | # prints if the argument is even/odd/neither 11 | # else: 12 | # prints if a value is even/odd/neither per each value in a example list 13 | # 14 | # This example is made for newcomers and motivated by: 15 | # - modulus is unsupported for pyscipopt.scip.Variable and int 16 | # - variables are non-integer by default 17 | # Based on this: 18 | # https://github.com/SCIP-Interfaces/PySCIPOpt/issues/172#issuecomment-394644046 19 | # 20 | ################################################################################ 21 | 22 | from pyscipopt import Model 23 | 24 | verbose = False 25 | sdic = {0: "even", 1: "odd"} 26 | 27 | 28 | def parity(number): 29 | try: 30 | assert number == int(round(number)) 31 | m = Model() 32 | m.hideOutput() 33 | 34 | ### variables are non-negative by default since 0 is the default lb. 35 | ### To allow for negative values, give None as lower bound 36 | ### (None means -infinity as lower bound and +infinity as upper bound) 37 | x = m.addVar("x", vtype="I", lb=None, ub=None) # ub=None is default 38 | n = m.addVar("n", vtype="I", lb=None) 39 | s = m.addVar("s", vtype="B") 40 | 41 | ### CAVEAT: if number is negative, x's lb must be None 42 | ### if x is set by default as non-negative and number is negative: 43 | ### there is no feasible solution (trivial) but the program 44 | ### does not highlight which constraints conflict. 45 | m.addCons(x == number) 46 | 47 | m.addCons(s == x - 2 * n) 48 | m.setObjective(s) 49 | m.optimize() 50 | 51 | assert m.getStatus() == "optimal" 52 | if verbose: 53 | for v in m.getVars(): 54 | print("%s %d" % (v, m.getVal(v))) 55 | print("%d%%2 == %d?" % (m.getVal(x), m.getVal(s))) 56 | print(m.getVal(s) == m.getVal(x) % 2) 57 | 58 | xval = m.getVal(x) 59 | sval = m.getVal(s) 60 | sstr = sdic[sval] 61 | print("%d is %s" % (xval, sstr)) 62 | except (AssertionError, TypeError): 63 | print("%s is neither even nor odd!" % number.__repr__()) 64 | 65 | 66 | if __name__ == "__main__": 67 | import sys 68 | from ast import literal_eval as leval 69 | 70 | example_values = [0, 1, 1.5, "hallo welt", 20, 25, -101, -15., -10, -int(2 ** 31), int(2 ** 31 - 1), 71 | int(2 ** 63) - 1] 72 | try: 73 | try: 74 | n = leval(sys.argv[1]) 75 | except ValueError: 76 | n = sys.argv[1] 77 | parity(n) 78 | except IndexError: 79 | for n in example_values: 80 | parity(n) 81 | -------------------------------------------------------------------------------- /examples/finished/flp.py: -------------------------------------------------------------------------------- 1 | ##@file flp.py 2 | # @brief model for solving the capacitated facility location problem 3 | """ 4 | minimize the total (weighted) travel cost from n customers 5 | to some facilities with fixed costs and capacities. 6 | 7 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 8 | """ 9 | from pyscipopt import Model, quicksum, multidict 10 | 11 | 12 | def flp(I, J, d, M, f, c): 13 | """flp -- model for the capacitated facility location problem 14 | Parameters: 15 | - I: set of customers 16 | - J: set of facilities 17 | - d[i]: demand for customer i 18 | - M[j]: capacity of facility j 19 | - f[j]: fixed cost for using a facility in point j 20 | - c[i,j]: unit cost of servicing demand point i from facility j 21 | Returns a model, ready to be solved. 22 | """ 23 | 24 | model = Model("flp") 25 | 26 | x, y = {}, {} 27 | for j in J: 28 | y[j] = model.addVar(vtype="B", name="y(%s)" % j) 29 | for i in I: 30 | x[i, j] = model.addVar(vtype="C", name="x(%s,%s)" % (i, j)) 31 | 32 | for i in I: 33 | model.addCons(quicksum(x[i, j] for j in J) == d[i], "Demand(%s)" % i) 34 | 35 | for j in M: 36 | model.addCons(quicksum(x[i, j] for i in I) <= M[j] * y[j], "Capacity(%s)" % i) 37 | 38 | for (i, j) in x: 39 | model.addCons(x[i, j] <= d[i] * y[j], "Strong(%s,%s)" % (i, j)) 40 | 41 | model.setObjective( 42 | quicksum(f[j] * y[j] for j in J) + 43 | quicksum(c[i, j] * x[i, j] for i in I for j in J), 44 | "minimize") 45 | model.data = x, y 46 | 47 | return model 48 | 49 | 50 | def make_data(): 51 | """creates example data set""" 52 | I, d = multidict({1: 80, 2: 270, 3: 250, 4: 160, 5: 180}) # demand 53 | J, M, f = multidict({1: [500, 1000], 2: [500, 1000], 3: [500, 1000]}) # capacity, fixed costs 54 | c = {(1, 1): 4, (1, 2): 6, (1, 3): 9, # transportation costs 55 | (2, 1): 5, (2, 2): 4, (2, 3): 7, 56 | (3, 1): 6, (3, 2): 3, (3, 3): 4, 57 | (4, 1): 8, (4, 2): 5, (4, 3): 3, 58 | (5, 1): 10, (5, 2): 8, (5, 3): 4, 59 | } 60 | return I, J, d, M, f, c 61 | 62 | 63 | if __name__ == "__main__": 64 | I, J, d, M, f, c = make_data() 65 | model = flp(I, J, d, M, f, c) 66 | model.optimize() 67 | 68 | EPS = 1.e-6 69 | x, y = model.data 70 | edges = [(i, j) for (i, j) in x if model.getVal(x[i, j]) > EPS] 71 | facilities = [j for j in y if model.getVal(y[j]) > EPS] 72 | 73 | print("Optimal value:", model.getObjVal()) 74 | print("Facilities at nodes:", facilities) 75 | print("Edges:", edges) 76 | 77 | try: # plot the result using networkx and matplotlib 78 | import networkx as NX 79 | import matplotlib.pyplot as P 80 | 81 | P.clf() 82 | G = NX.Graph() 83 | 84 | other = [j for j in y if j not in facilities] 85 | customers = ["c%s" % i for i in d] 86 | G.add_nodes_from(facilities) 87 | G.add_nodes_from(other) 88 | G.add_nodes_from(customers) 89 | for (i, j) in edges: 90 | G.add_edge("c%s" % i, j) 91 | 92 | position = NX.drawing.layout.spring_layout(G) 93 | NX.draw(G, position, node_color="y", nodelist=facilities) 94 | NX.draw(G, position, node_color="g", nodelist=other) 95 | NX.draw(G, position, node_color="b", nodelist=customers) 96 | P.show() 97 | except ImportError: 98 | print("install 'networkx' and 'matplotlib' for plotting") 99 | -------------------------------------------------------------------------------- /examples/finished/gcp_fixed_k.py: -------------------------------------------------------------------------------- 1 | ##@file gcp_fixed_k.py 2 | # @brief solve the graph coloring problem with fixed-k model 3 | """ 4 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 5 | """ 6 | 7 | from pyscipopt import Model, quicksum 8 | 9 | 10 | def gcp_fixed_k(V, E, K): 11 | """gcp_fixed_k -- model for minimizing number of bad edges in coloring a graph 12 | Parameters: 13 | - V: set/list of nodes in the graph 14 | - E: set/list of edges in the graph 15 | - K: number of colors to be used 16 | Returns a model, ready to be solved. 17 | """ 18 | model = Model("gcp - fixed k") 19 | 20 | x, z = {}, {} 21 | for i in V: 22 | for k in range(K): 23 | x[i, k] = model.addVar(vtype="B", name="x(%s,%s)" % (i, k)) 24 | for (i, j) in E: 25 | z[i, j] = model.addVar(vtype="B", name="z(%s,%s)" % (i, j)) 26 | 27 | for i in V: 28 | model.addCons(quicksum(x[i, k] for k in range(K)) == 1, "AssignColor(%s)" % i) 29 | 30 | for (i, j) in E: 31 | for k in range(K): 32 | model.addCons(x[i, k] + x[j, k] <= 1 + z[i, j], "BadEdge(%s,%s,%s)" % (i, j, k)) 33 | 34 | model.setObjective(quicksum(z[i, j] for (i, j) in E), "minimize") 35 | 36 | model.data = x, z 37 | return model 38 | 39 | 40 | def solve_gcp(V, E): 41 | """solve_gcp -- solve the graph coloring problem with bisection and fixed-k model 42 | Parameters: 43 | - V: set/list of nodes in the graph 44 | - E: set/list of edges in the graph 45 | Returns tuple with number of colors used, and dictionary mapping colors to vertices 46 | """ 47 | LB = 0 48 | UB = len(V) 49 | color = {} 50 | while UB - LB > 1: 51 | K = int((UB + LB) / 2) 52 | gcp = gcp_fixed_k(V, E, K) 53 | # gcp.Params.OutputFlag = 0 # silent mode 54 | # gcp.Params.Cutoff = .1 55 | gcp.setObjlimit(0.1) 56 | gcp.optimize() 57 | status = gcp.getStatus() 58 | if status == "optimal": 59 | x, z = gcp.data 60 | for i in V: 61 | for k in range(K): 62 | if gcp.getVal(x[i, k]) > 0.5: 63 | color[i] = k 64 | break 65 | # else: 66 | # raise "undefined color for", i 67 | UB = K 68 | else: 69 | LB = K 70 | 71 | return UB, color 72 | 73 | 74 | import random 75 | 76 | 77 | def make_data(n, prob): 78 | """make_data: prepare data for a random graph 79 | Parameters: 80 | - n: number of vertices 81 | - prob: probability of existence of an edge, for each pair of vertices 82 | Returns a tuple with a list of vertices and a list edges. 83 | """ 84 | V = range(1, n + 1) 85 | E = [(i, j) for i in V for j in V if i < j and random.random() < prob] 86 | return V, E 87 | 88 | 89 | if __name__ == "__main__": 90 | random.seed(1) 91 | V, E = make_data(75, .25) 92 | 93 | K, color = solve_gcp(V, E) 94 | print("minimum number of colors:", K) 95 | print("solution:", color) 96 | -------------------------------------------------------------------------------- /examples/finished/kmedian.py: -------------------------------------------------------------------------------- 1 | ##@file kmedian.py 2 | # @brief model for solving the k-median problem. 3 | """ 4 | minimize the total (weighted) travel cost for servicing 5 | a set of customers from k facilities. 6 | 7 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 8 | """ 9 | import math 10 | import random 11 | 12 | from pyscipopt import Model, quicksum 13 | 14 | 15 | def kmedian(I, J, c, k): 16 | """kmedian -- minimize total cost of servicing customers from k facilities 17 | Parameters: 18 | - I: set of customers 19 | - J: set of potential facilities 20 | - c[i,j]: cost of servicing customer i from facility j 21 | - k: number of facilities to be used 22 | Returns a model, ready to be solved. 23 | """ 24 | 25 | model = Model("k-median") 26 | x, y = {}, {} 27 | 28 | for j in J: 29 | y[j] = model.addVar(vtype="B", name="y(%s)" % j) 30 | for i in I: 31 | x[i, j] = model.addVar(vtype="B", name="x(%s,%s)" % (i, j)) 32 | 33 | for i in I: 34 | model.addCons(quicksum(x[i, j] for j in J) == 1, "Assign(%s)" % i) 35 | for j in J: 36 | model.addCons(x[i, j] <= y[j], "Strong(%s,%s)" % (i, j)) 37 | model.addCons(quicksum(y[j] for j in J) == k, "Facilities") 38 | 39 | model.setObjective(quicksum(c[i, j] * x[i, j] for i in I for j in J), "minimize") 40 | model.data = x, y 41 | 42 | return model 43 | 44 | 45 | def distance(x1, y1, x2, y2): 46 | """return distance of two points""" 47 | return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) 48 | 49 | 50 | def make_data(n, m, same=True): 51 | """creates example data set""" 52 | if same == True: 53 | I = range(n) 54 | J = range(m) 55 | x = [random.random() for i in range(max(m, n))] # positions of the points in the plane 56 | y = [random.random() for i in range(max(m, n))] 57 | else: 58 | I = range(n) 59 | J = range(n, n + m) 60 | x = [random.random() for i in range(n + m)] # positions of the points in the plane 61 | y = [random.random() for i in range(n + m)] 62 | c = {} 63 | for i in I: 64 | for j in J: 65 | c[i, j] = distance(x[i], y[i], x[j], y[j]) 66 | 67 | return I, J, c, x, y 68 | 69 | 70 | if __name__ == "__main__": 71 | 72 | random.seed(67) 73 | n = 200 74 | m = n 75 | I, J, c, x_pos, y_pos = make_data(n, m, same=True) 76 | k = 20 77 | model = kmedian(I, J, c, k) 78 | # model.Params.Threads = 1 79 | model.optimize() 80 | EPS = 1.e-6 81 | x, y = model.data 82 | edges = [(i, j) for (i, j) in x if model.getVal(x[i, j]) > EPS] 83 | facilities = [j for j in y if model.getVal(y[j]) > EPS] 84 | 85 | print("Optimal value:", model.getObjVal()) 86 | print("Selected facilities:", facilities) 87 | print("Edges:", edges) 88 | print("max c:", max([c[i, j] for (i, j) in edges])) 89 | 90 | try: # plot the result using networkx and matplotlib 91 | import networkx as NX 92 | import matplotlib.pyplot as P 93 | 94 | P.clf() 95 | G = NX.Graph() 96 | 97 | facilities = set(j for j in J if model.getVal(y[j]) > EPS) 98 | other = set(j for j in J if j not in facilities) 99 | client = set(i for i in I if i not in facilities and i not in other) 100 | G.add_nodes_from(facilities) 101 | G.add_nodes_from(client) 102 | G.add_nodes_from(other) 103 | for (i, j) in edges: 104 | G.add_edge(i, j) 105 | 106 | position = {} 107 | for i in range(len(x_pos)): 108 | position[i] = (x_pos[i], y_pos[i]) 109 | 110 | NX.draw(G, position, with_labels=False, node_color="w", nodelist=facilities) 111 | NX.draw(G, position, with_labels=False, node_color="c", nodelist=other, node_size=50) 112 | NX.draw(G, position, with_labels=False, node_color="g", nodelist=client, node_size=50) 113 | P.show() 114 | except ImportError: 115 | print("install 'networkx' and 'matplotlib' for plotting") 116 | -------------------------------------------------------------------------------- /examples/finished/lo_wines.py: -------------------------------------------------------------------------------- 1 | ##@file lo_wines.py 2 | # @brief Simple SCIP example of linear programming. 3 | """ 4 | It solves the same instance as lo_wines_simple.py: 5 | 6 | maximize 15x + 18y + 30z 7 | subject to 2x + y + z <= 60 8 | x + 2y + z <= 60 9 | z <= 30 10 | x,y,z >= 0 11 | Variables correspond to the production of three types of wine blends, 12 | made from pure-grape wines. 13 | Constraints correspond to the inventory of pure-grape wines. 14 | 15 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 16 | """ 17 | from pyscipopt import Model, quicksum, SCIP_PARAMSETTING 18 | 19 | # Initialize model 20 | model = Model("Wine blending") 21 | model.setPresolve(SCIP_PARAMSETTING.OFF) 22 | 23 | Inventory = {"Alfrocheiro": 60, "Baga": 60, "Castelao": 30} 24 | Grapes = Inventory.keys() 25 | 26 | Profit = {"Dry": 15, "Medium": 18, "Sweet": 30} 27 | Blends = Profit.keys() 28 | 29 | Use = { 30 | ("Alfrocheiro", "Dry"): 2, 31 | ("Alfrocheiro", "Medium"): 1, 32 | ("Alfrocheiro", "Sweet"): 1, 33 | ("Baga", "Dry"): 1, 34 | ("Baga", "Medium"): 2, 35 | ("Baga", "Sweet"): 1, 36 | ("Castelao", "Dry"): 0, 37 | ("Castelao", "Medium"): 0, 38 | ("Castelao", "Sweet"): 1 39 | } 40 | 41 | # Create variables 42 | x = {} 43 | for j in Blends: 44 | x[j] = model.addVar(vtype="C", name="x(%s)" % j) 45 | 46 | # Create constraints 47 | c = {} 48 | for i in Grapes: 49 | c[i] = model.addCons(quicksum(Use[i, j] * x[j] for j in Blends) <= Inventory[i], name="Use(%s)" % i) 50 | 51 | # Objective 52 | model.setObjective(quicksum(Profit[j] * x[j] for j in Blends), "maximize") 53 | 54 | model.optimize() 55 | 56 | if model.getStatus() == "optimal": 57 | print("Optimal value:", model.getObjVal()) 58 | 59 | for j in x: 60 | print(x[j].name, "=", model.getVal(x[j]), " (red. cost: ", model.getVarRedcost(x[j]), ")") 61 | for i in c: 62 | try: 63 | dual = model.getDualsolLinear(c[i]) 64 | except: 65 | dual = None 66 | print("dual of", c[i].name, ":", dual) 67 | else: 68 | print("Problem could not be solved to optimality") 69 | -------------------------------------------------------------------------------- /examples/finished/logical.py: -------------------------------------------------------------------------------- 1 | ##@file finished/logical.py 2 | # @brief Tutorial example on how to use AND/OR/XOR constraints 3 | 4 | from pyscipopt import Model 5 | from pyscipopt import quicksum 6 | 7 | """ 8 | 9 | AND/OR/XOR CONSTRAINTS 10 | 11 | Tutorial example on how to use AND/OR/XOR constraints. 12 | 13 | N.B.: standard SCIP XOR constraint works differently from AND/OR by design. 14 | The constraint is set with a boolean rhs instead of an integer resultant. 15 | cf. http://listserv.zib.de/pipermail/scip/2018-May/003392.html 16 | A workaround to get the resultant as variable is here proposed. 17 | 18 | """ 19 | 20 | 21 | def printFunc(name, m): 22 | """prints results""" 23 | print("* %s *" % name) 24 | objSet = bool(m.getObjective().terms.keys()) 25 | print("* Is objective set? %s" % objSet) 26 | if objSet: 27 | print("* Sense: %s" % m.getObjectiveSense()) 28 | for v in m.getVars(): 29 | if v.name != "n": 30 | print("%s: %d" % (v, round(m.getVal(v)))) 31 | print("\n") 32 | 33 | 34 | # AND 35 | model = Model() 36 | model.hideOutput() 37 | x = model.addVar("x", "B") 38 | y = model.addVar("y", "B") 39 | z = model.addVar("z", "B") 40 | r = model.addVar("r", "B") 41 | model.addConsAnd([x, y, z], r) 42 | model.addCons(x == 1) 43 | model.setObjective(r, sense="minimize") 44 | model.optimize() 45 | printFunc("AND", model) 46 | 47 | # OR 48 | model = Model() 49 | model.hideOutput() 50 | x = model.addVar("x", "B") 51 | y = model.addVar("y", "B") 52 | z = model.addVar("z", "B") 53 | r = model.addVar("r", "B") 54 | model.addConsOr([x, y, z], r) 55 | model.addCons(x == 0) 56 | model.setObjective(r, sense="maximize") 57 | model.optimize() 58 | printFunc("OR", model) 59 | 60 | # XOR (r as boolean, standard) 61 | model = Model() 62 | model.hideOutput() 63 | x = model.addVar("x", "B") 64 | y = model.addVar("y", "B") 65 | z = model.addVar("z", "B") 66 | r = True 67 | model.addConsXor([x, y, z], r) 68 | model.addCons(x == 1) 69 | model.optimize() 70 | printFunc("Standard XOR (as boolean)", model) 71 | 72 | # XOR (r as variable, custom) 73 | model = Model() 74 | model.hideOutput() 75 | x = model.addVar("x", "B") 76 | y = model.addVar("y", "B") 77 | z = model.addVar("z", "B") 78 | r = model.addVar("r", "B") 79 | n = model.addVar("n", "I") # auxiliary 80 | model.addCons(r + quicksum([x, y, z]) == 2 * n) 81 | model.addCons(x == 0) 82 | model.setObjective(r, sense="maximize") 83 | model.optimize() 84 | printFunc("Custom XOR (as variable)", model) 85 | -------------------------------------------------------------------------------- /examples/finished/markowitz_soco.py: -------------------------------------------------------------------------------- 1 | ##@file markowitz_soco.py 2 | # @brief simple markowitz model for portfolio optimization. 3 | """ 4 | Approach: use second-order cone optimization. 5 | 6 | Copyright (c) by Joao Pedro PEDROSO, Masahiro MURAMATSU and Mikio KUBO, 2012 7 | """ 8 | from pyscipopt import Model, quicksum, multidict 9 | 10 | 11 | def markowitz(I, sigma, r, alpha): 12 | """markowitz -- simple markowitz model for portfolio optimization. 13 | Parameters: 14 | - I: set of items 15 | - sigma[i]: standard deviation of item i 16 | - r[i]: revenue of item i 17 | - alpha: acceptance threshold 18 | Returns a model, ready to be solved. 19 | """ 20 | model = Model("markowitz") 21 | 22 | x = {} 23 | for i in I: 24 | x[i] = model.addVar(vtype="C", name="x(%s)" % i) # quantity of i to buy 25 | 26 | model.addCons(quicksum(r[i] * x[i] for i in I) >= alpha) 27 | model.addCons(quicksum(x[i] for i in I) == 1) 28 | 29 | # set nonlinear objective: SCIP only allow for linear objectives hence the following 30 | obj = model.addVar(vtype="C", name="objective", lb=None, ub=None) # auxiliary variable to represent objective 31 | model.addCons(quicksum(sigma[i] ** 2 * x[i] * x[i] for i in I) <= obj) 32 | model.setObjective(obj, "minimize") 33 | 34 | model.data = x 35 | return model 36 | 37 | 38 | if __name__ == "__main__": 39 | # portfolio 40 | 41 | I, sigma, r = multidict( 42 | {1: [0.07, 1.01], 43 | 2: [0.09, 1.05], 44 | 3: [0.1, 1.08], 45 | 4: [0.2, 1.10], 46 | 5: [0.3, 1.20]} 47 | ) 48 | alpha = 1.05 49 | 50 | model = markowitz(I, sigma, r, alpha) 51 | model.optimize() 52 | 53 | x = model.data 54 | EPS = 1.e-6 55 | print("%5s\t%8s" % ("i", "x[i]")) 56 | for i in I: 57 | print("%5s\t%8g" % (i, model.getVal(x[i]))) 58 | print("sum:", sum(model.getVal(x[i]) for i in I)) 59 | print 60 | print("Optimal value:", model.getObjVal()) 61 | -------------------------------------------------------------------------------- /examples/finished/mkp.py: -------------------------------------------------------------------------------- 1 | ##@file mkp.py 2 | # @brief model for the multi-constrained knapsack problem 3 | """ 4 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 5 | """ 6 | from pyscipopt import Model, quicksum, multidict 7 | 8 | 9 | def mkp(I, J, v, a, b): 10 | """mkp -- model for solving the multi-constrained knapsack 11 | Parameters: 12 | - I: set of dimensions 13 | - J: set of items 14 | - v[j]: value of item j 15 | - a[i,j]: weight of item j on dimension i 16 | - b[i]: capacity of knapsack on dimension i 17 | Returns a model, ready to be solved. 18 | """ 19 | model = Model("mkp") 20 | 21 | # Create Variables 22 | x = {} 23 | for j in J: 24 | x[j] = model.addVar(vtype="B", name="x(%s)" % j) 25 | 26 | # Create constraints 27 | for i in I: 28 | model.addCons(quicksum(a[i, j] * x[j] for j in J) <= b[i], "Capacity(%s)" % i) 29 | 30 | # Objective 31 | model.setObjective(quicksum(v[j] * x[j] for j in J), "maximize") 32 | model.data = x 33 | 34 | return model 35 | 36 | 37 | def example(): 38 | """creates example data set""" 39 | J, v = multidict({1: 16, 2: 19, 3: 23, 4: 28}) 40 | a = {(1, 1): 2, (1, 2): 3, (1, 3): 4, (1, 4): 5, 41 | (2, 1): 3000, (2, 2): 3500, (2, 3): 5100, (2, 4): 7200, 42 | } 43 | I, b = multidict({1: 7, 2: 10000}) 44 | return I, J, v, a, b 45 | 46 | 47 | if __name__ == "__main__": 48 | I, J, v, a, b = example() 49 | model = mkp(I, J, v, a, b) 50 | x = model.data 51 | model.optimize() 52 | 53 | print("Optimal value:", model.getObjVal()) 54 | 55 | EPS = 1.e-6 56 | 57 | for i in x: 58 | v = x[i] 59 | if model.getVal(v) > EPS: 60 | print(v.name, "=", model.getVal(v)) 61 | -------------------------------------------------------------------------------- /examples/finished/pfs.py: -------------------------------------------------------------------------------- 1 | ##@file pfs.py 2 | # @brief model for the permutation flow shop problem 3 | """ 4 | Use a position index formulation for modeling the permutation flow 5 | shop problem, with the objective of minimizing the makespan (maximum 6 | completion time). 7 | 8 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 9 | """ 10 | import random 11 | 12 | from pyscipopt import Model, quicksum 13 | 14 | 15 | def permutation_flow_shop(n, m, p): 16 | """gpp -- model for the graph partitioning problem 17 | Parameters: 18 | - n: number of jobs 19 | - m: number of machines 20 | - p[i,j]: processing time of job i on machine j 21 | Returns a model, ready to be solved. 22 | """ 23 | model = Model("permutation flow shop") 24 | 25 | x, s, f = {}, {}, {} 26 | for j in range(1, n + 1): 27 | for k in range(1, n + 1): 28 | x[j, k] = model.addVar(vtype="B", name="x(%s,%s)" % (j, k)) 29 | 30 | for i in range(1, m + 1): 31 | for k in range(1, n + 1): 32 | s[i, k] = model.addVar(vtype="C", name="start(%s,%s)" % (i, k)) 33 | f[i, k] = model.addVar(vtype="C", name="finish(%s,%s)" % (i, k)) 34 | 35 | for j in range(1, n + 1): 36 | model.addCons(quicksum(x[j, k] for k in range(1, n + 1)) == 1, "Assign1(%s)" % (j)) 37 | model.addCons(quicksum(x[k, j] for k in range(1, n + 1)) == 1, "Assign2(%s)" % (j)) 38 | 39 | for i in range(1, m + 1): 40 | for k in range(1, n + 1): 41 | if k != n: 42 | model.addCons(f[i, k] <= s[i, k + 1], "FinishStart(%s,%s)" % (i, k)) 43 | if i != m: 44 | model.addCons(f[i, k] <= s[i + 1, k], "Machine(%s,%s)" % (i, k)) 45 | 46 | model.addCons(s[i, k] + quicksum(p[i, j] * x[j, k] for j in range(1, n + 1)) <= f[i, k], 47 | "StartFinish(%s,%s)" % (i, k)) 48 | 49 | model.setObjective(f[m, n], "minimize") 50 | 51 | model.data = x, s, f 52 | return model 53 | 54 | 55 | def make_data(n, m): 56 | """make_data: prepare matrix of m times n random processing times""" 57 | p = {} 58 | for i in range(1, m + 1): 59 | for j in range(1, n + 1): 60 | p[i, j] = random.randint(1, 10) 61 | return p 62 | 63 | 64 | def example(): 65 | """creates example data set""" 66 | proc = [[2, 3, 1], [4, 2, 3], [1, 4, 1]] 67 | p = {} 68 | for i in range(3): 69 | for j in range(3): 70 | p[i + 1, j + 1] = proc[j][i] 71 | return p 72 | 73 | 74 | if __name__ == "__main__": 75 | random.seed(1) 76 | n = 15 77 | m = 10 78 | p = make_data(n, m) 79 | 80 | # n = 3 81 | # m = 3 82 | # p = example() 83 | print("processing times (%s jobs, %s machines):" % (n, m)) 84 | for i in range(1, m + 1): 85 | for j in range(1, n + 1): 86 | print(p[i, j], ) 87 | print 88 | 89 | model = permutation_flow_shop(n, m, p) 90 | # model.write("permflow.lp") 91 | model.optimize() 92 | x, s, f = model.data 93 | print("Optimal value:", model.getObjVal()) 94 | 95 | ### for (j,k) in sorted(x): 96 | ### if x[j,k].X > 0.5: 97 | ### print(x[j,k].VarName,x[j,k].X 98 | ### 99 | ### for i in sorted(s): 100 | ### print(s[i].VarName,s[i].X 101 | ### 102 | ### for i in sorted(f): 103 | ### print(f[i].VarName,f[i].X 104 | 105 | # x[j,k] = 1 if j is the k-th job; extract job sequence: 106 | seq = [j for (k, j) in sorted([(k, j) for (j, k) in x if model.getVal(x[j, k]) > 0.5])] 107 | print("optimal job permutation:", seq) 108 | -------------------------------------------------------------------------------- /examples/finished/plot_primal_dual_evolution.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example show how to retrieve the primal and dual solutions during the optimization process 3 | and plot them as a function of time. The model is about gas transportation and can be found in 4 | PySCIPOpt/tests/helpers/utils.py 5 | 6 | It makes use of the attach_primal_dual_evolution_eventhdlr recipe. 7 | 8 | Requires matplotlib, and may require PyQt6 to show the plot. 9 | """ 10 | 11 | from pyscipopt import Model 12 | 13 | def plot_primal_dual_evolution(model: Model): 14 | try: 15 | from matplotlib import pyplot as plt 16 | except ImportError: 17 | raise ImportError("matplotlib is required to plot the solution. Try running `pip install matplotlib` in the command line.\ 18 | You may also need to install PyQt6 to show the plot.") 19 | 20 | assert model.data["primal_log"], "Could not find any feasible solutions" 21 | time_primal, val_primal = map(list,zip(*model.data["primal_log"])) 22 | time_dual, val_dual = map(list,zip(*model.data["dual_log"])) 23 | 24 | 25 | if time_primal[-1] < time_dual[-1]: 26 | time_primal.append(time_dual[-1]) 27 | val_primal.append(val_primal[-1]) 28 | 29 | if time_primal[-1] > time_dual[-1]: 30 | time_dual.append(time_primal[-1]) 31 | val_dual.append(val_dual[-1]) 32 | 33 | plt.plot(time_primal, val_primal, label="Primal bound") 34 | plt.plot(time_dual, val_dual, label="Dual bound") 35 | 36 | plt.legend(loc="best") 37 | plt.show() 38 | 39 | if __name__=="__main__": 40 | from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr 41 | import os 42 | import sys 43 | 44 | # just a way to import files from different folders, not important 45 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tests/helpers'))) 46 | 47 | from utils import gastrans_model 48 | 49 | model = gastrans_model() 50 | model.data = {} 51 | attach_primal_dual_evolution_eventhdlr(model) 52 | 53 | model.optimize() 54 | plot_primal_dual_evolution(model) 55 | -------------------------------------------------------------------------------- /examples/finished/prodmix_soco.py: -------------------------------------------------------------------------------- 1 | ##@file prodmix_soco.py 2 | # @brief product mix model using soco. 3 | """ 4 | Copyright (c) by Joao Pedro PEDROSO, Masahiro MURAMATSU and Mikio KUBO, 2012 5 | """ 6 | from pyscipopt import Model, quicksum, multidict 7 | 8 | 9 | def prodmix(I, K, a, p, epsilon, LB): 10 | """prodmix: robust production planning using soco 11 | Parameters: 12 | I - set of materials 13 | K - set of components 14 | a[i][k] - coef. matrix 15 | p[i] - price of material i 16 | LB[k] - amount needed for k 17 | Returns a model, ready to be solved. 18 | """ 19 | 20 | model = Model("robust product mix") 21 | 22 | x, rhs = {}, {} 23 | for i in I: 24 | x[i] = model.addVar(vtype="C", name="x(%s)" % i) 25 | for k in K: 26 | rhs[k] = model.addVar(vtype="C", name="rhs(%s)" % k) 27 | 28 | model.addCons(quicksum(x[i] for i in I) == 1) 29 | for k in K: 30 | model.addCons(rhs[k] == -LB[k] + quicksum(a[i, k] * x[i] for i in I)) 31 | model.addCons(quicksum(epsilon * epsilon * x[i] * x[i] for i in I) <= rhs[k] * rhs[k]) 32 | 33 | model.setObjective(quicksum(p[i] * x[i] for i in I), "minimize") 34 | 35 | model.data = x, rhs 36 | return model 37 | 38 | 39 | def make_data(): 40 | """creates example data set""" 41 | a = {(1, 1): .25, (1, 2): .15, (1, 3): .2, 42 | (2, 1): .3, (2, 2): .3, (2, 3): .1, 43 | (3, 1): .15, (3, 2): .65, (3, 3): .05, 44 | (4, 1): .1, (4, 2): .05, (4, 3): .8 45 | } 46 | epsilon = 0.01 47 | I, p = multidict({1: 5, 2: 6, 3: 8, 4: 20}) 48 | K, LB = multidict({1: .2, 2: .3, 3: .2}) 49 | return I, K, a, p, epsilon, LB 50 | 51 | 52 | if __name__ == "__main__": 53 | I, K, a, p, epsilon, LB = make_data() 54 | model = prodmix(I, K, a, p, epsilon, LB) 55 | model.optimize() 56 | print("Objective value:", model.getObjVal()) 57 | x, rhs = model.data 58 | for i in I: 59 | print(i, ": ", model.getVal(x[i])) 60 | -------------------------------------------------------------------------------- /examples/finished/ssa.py: -------------------------------------------------------------------------------- 1 | ##@file ssa.py 2 | # @brief multi-stage (serial) safety stock allocation model 3 | """ 4 | Approach: use SOS2 constraints for modeling non-linear functions. 5 | 6 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 7 | """ 8 | import math 9 | import random 10 | 11 | from pyscipopt import Model, quicksum 12 | 13 | from piecewise import convex_comb_sos 14 | 15 | 16 | def ssa(n, h, K, f, T): 17 | """ssa -- multi-stage (serial) safety stock allocation model 18 | Parameters: 19 | - n: number of stages 20 | - h[i]: inventory cost on stage i 21 | - K: number of linear segments 22 | - f: (non-linear) cost function 23 | - T[i]: production lead time on stage i 24 | Returns the model with the piecewise linear relation on added variables x, f, and z. 25 | """ 26 | 27 | model = Model("safety stock allocation") 28 | 29 | # calculate endpoints for linear segments 30 | a, b = {}, {} 31 | for i in range(1, n + 1): 32 | a[i] = [k for k in range(K)] 33 | b[i] = [f(i, k) for k in range(K)] 34 | 35 | # x: net replenishment time for stage i 36 | # y: corresponding cost 37 | # s: piecewise linear segment of variable x 38 | x, y, s = {}, {}, {} 39 | L = {} # service time of stage i 40 | for i in range(1, n + 1): 41 | x[i], y[i], s[i] = convex_comb_sos(model, a[i], b[i]) 42 | if i == 1: 43 | L[i] = model.addVar(ub=0, vtype="C", name="L[%s]" % i) 44 | else: 45 | L[i] = model.addVar(vtype="C", name="L[%s]" % i) 46 | L[n + 1] = model.addVar(ub=0, vtype="C", name="L[%s]" % (n + 1)) 47 | 48 | for i in range(1, n + 1): 49 | # net replenishment time for each stage i 50 | model.addCons(x[i] + L[i] == T[i] + L[i + 1]) 51 | 52 | model.setObjective(quicksum(h[i] * y[i] for i in range(1, n + 1)), "minimize") 53 | 54 | model.data = x, s, L 55 | return model 56 | 57 | 58 | def make_data(): 59 | """creates example data set""" 60 | n = 30 # number of stages 61 | z = 1.65 # for 95% service level 62 | sigma = 100 # demand's standard deviation 63 | h = {} # inventory cost 64 | T = {} # production lead time 65 | h[n] = 1 66 | for i in range(n - 1, 0, -1): 67 | h[i] = h[i + 1] + random.randint(30, 50) 68 | K = 0 # number of segments (=sum of processing times) 69 | for i in range(1, n + 1): 70 | T[i] = random.randint(3, 5) # production lead time at stage i 71 | K += T[i] 72 | return z, sigma, h, T, K, n 73 | 74 | 75 | if __name__ == "__main__": 76 | random.seed(1) 77 | 78 | z, sigma, h, T, K, n = make_data() 79 | 80 | 81 | def f(i, k): 82 | return sigma * z * math.sqrt(k) 83 | 84 | 85 | model = ssa(n, h, K, f, T) 86 | model.optimize() 87 | 88 | # model.write("ssa.lp") 89 | x, s, L = model.data 90 | for i in range(1, n + 1): 91 | for k in range(K): 92 | if model.getVal(s[i][k]) >= 0.001: 93 | print(s[i][k].name, model.getVal(s[i][k])) 94 | print 95 | print("%10s%10s%10s%10s" % ("Period", "x", "L", "T")) 96 | for i in range(1, n + 1): 97 | print("%10s%10s%10s%10s" % (i, model.getVal(x[i]), model.getVal(L[i]), T[i])) 98 | 99 | print("Objective:", model.getObjVal()) 100 | -------------------------------------------------------------------------------- /examples/finished/ssp.py: -------------------------------------------------------------------------------- 1 | ##@file ssp.py 2 | # @brief model for the stable set problem 3 | """ 4 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 5 | """ 6 | 7 | from pyscipopt import Model, quicksum 8 | 9 | 10 | def ssp(V, E): 11 | """ssp -- model for the stable set problem 12 | Parameters: 13 | - V: set/list of nodes in the graph 14 | - E: set/list of edges in the graph 15 | Returns a model, ready to be solved. 16 | """ 17 | model = Model("ssp") 18 | 19 | x = {} 20 | for i in V: 21 | x[i] = model.addVar(vtype="B", name="x(%s)" % i) 22 | 23 | for (i, j) in E: 24 | model.addCons(x[i] + x[j] <= 1, "Edge(%s,%s)" % (i, j)) 25 | 26 | model.setObjective(quicksum(x[i] for i in V), "maximize") 27 | 28 | model.data = x 29 | return model 30 | 31 | 32 | import random 33 | 34 | 35 | def make_data(n, prob): 36 | """make_data: prepare data for a random graph 37 | Parameters: 38 | - n: number of vertices 39 | - prob: probability of existence of an edge, for each pair of vertices 40 | Returns a tuple with a list of vertices and a list edges. 41 | """ 42 | V = range(1, n + 1) 43 | E = [(i, j) for i in V for j in V if i < j and random.random() < prob] 44 | return V, E 45 | 46 | 47 | if __name__ == "__main__": 48 | random.seed(1) 49 | V, E = make_data(100, .5) 50 | 51 | model = ssp(V, E) 52 | model.optimize() 53 | print("Optimal value:", model.getObjVal()) 54 | 55 | x = model.data 56 | print("Maximum stable set:") 57 | print([i for i in V if model.getVal(x[i]) > 0.5]) 58 | -------------------------------------------------------------------------------- /examples/finished/sudoku.py: -------------------------------------------------------------------------------- 1 | ##@file sudoku.py 2 | # @brief Simple example of modeling a Sudoku as a binary program 3 | 4 | from pyscipopt import Model, quicksum 5 | 6 | # initial Sudoku values 7 | init = [5, 3, 0, 0, 7, 0, 0, 0, 0, 8 | 6, 0, 0, 1, 9, 5, 0, 0, 0, 9 | 0, 9, 8, 0, 0, 0, 0, 6, 0, 10 | 8, 0, 0, 0, 6, 0, 0, 0, 3, 11 | 4, 0, 0, 8, 0, 3, 0, 0, 1, 12 | 7, 0, 0, 0, 2, 0, 0, 0, 6, 13 | 0, 6, 0, 0, 0, 0, 2, 8, 0, 14 | 0, 0, 0, 4, 1, 9, 0, 0, 5, 15 | 0, 0, 0, 0, 8, 0, 0, 7, 9] 16 | 17 | m = Model() 18 | 19 | # create a binary variable for every field and value 20 | x = {} 21 | for i in range(9): 22 | for j in range(9): 23 | for k in range(9): 24 | name = str(i) + ',' + str(j) + ',' + str(k) 25 | x[i, j, k] = m.addVar(name, vtype='B') 26 | 27 | # fill in initial values 28 | for i in range(9): 29 | for j in range(9): 30 | if init[j + 9 * i] != 0: 31 | m.addCons(x[i, j, init[j + 9 * i] - 1] == 1) 32 | 33 | # only one digit in every field 34 | for i in range(9): 35 | for j in range(9): 36 | m.addCons(quicksum(x[i, j, k] for k in range(9)) == 1) 37 | 38 | # set up row and column constraints 39 | for ind in range(9): 40 | for k in range(9): 41 | m.addCons(quicksum(x[ind, j, k] for j in range(9)) == 1) 42 | m.addCons(quicksum(x[i, ind, k] for i in range(9)) == 1) 43 | 44 | # set up square constraints 45 | for row in range(3): 46 | for col in range(3): 47 | for k in range(9): 48 | m.addCons(quicksum(x[i + 3 * row, j + 3 * col, k] for i in range(3) for j in range(3)) == 1) 49 | 50 | m.hideOutput() 51 | m.optimize() 52 | 53 | if m.getStatus() != 'optimal': 54 | print('Sudoku is not feasible!') 55 | else: 56 | print('\nSudoku solution:\n') 57 | sol = {} 58 | for i in range(9): 59 | out = '' 60 | for j in range(9): 61 | for k in range(9): 62 | if m.getVal(x[i, j, k]) == 1: 63 | sol[i, j] = k + 1 64 | out += str(sol[i, j]) + ' ' 65 | print(out) 66 | -------------------------------------------------------------------------------- /examples/finished/transp.py: -------------------------------------------------------------------------------- 1 | ##@file transp.py 2 | # @brief a model for the transportation problem 3 | """ 4 | Model for solving a transportation problem: 5 | minimize the total transportation cost for satisfying demand at 6 | customers, from capacitated facilities. 7 | 8 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 9 | """ 10 | from pyscipopt import Model, quicksum, multidict 11 | 12 | 13 | def transp(I, J, c, d, M): 14 | """transp -- model for solving the transportation problem 15 | Parameters: 16 | I - set of customers 17 | J - set of facilities 18 | c[i,j] - unit transportation cost on arc (i,j) 19 | d[i] - demand at node i 20 | M[j] - capacity 21 | Returns a model, ready to be solved. 22 | """ 23 | 24 | model = Model("transportation") 25 | 26 | # Create variables 27 | x = {} 28 | 29 | for i in I: 30 | for j in J: 31 | x[i, j] = model.addVar(vtype="C", name="x(%s,%s)" % (i, j)) 32 | 33 | # Demand constraints 34 | for i in I: 35 | model.addCons(quicksum(x[i, j] for j in J if (i, j) in x) == d[i], name="Demand(%s)" % i) 36 | 37 | # Capacity constraints 38 | for j in J: 39 | model.addCons(quicksum(x[i, j] for i in I if (i, j) in x) <= M[j], name="Capacity(%s)" % j) 40 | 41 | # Objective 42 | model.setObjective(quicksum(c[i, j] * x[i, j] for (i, j) in x), "minimize") 43 | 44 | model.optimize() 45 | 46 | model.data = x 47 | return model 48 | 49 | 50 | def make_inst1(): 51 | """creates example data set 1""" 52 | I, d = multidict({1: 80, 2: 270, 3: 250, 4: 160, 5: 180}) # demand 53 | J, M = multidict({1: 500, 2: 500, 3: 500}) # capacity 54 | c = {(1, 1): 4, (1, 2): 6, (1, 3): 9, # cost 55 | (2, 1): 5, (2, 2): 4, (2, 3): 7, 56 | (3, 1): 6, (3, 2): 3, (3, 3): 4, 57 | (4, 1): 8, (4, 2): 5, (4, 3): 3, 58 | (5, 1): 10, (5, 2): 8, (5, 3): 4, 59 | } 60 | return I, J, c, d, M 61 | 62 | 63 | def make_inst2(): 64 | """creates example data set 2""" 65 | I, d = multidict({1: 45, 2: 20, 3: 30, 4: 30}) # demand 66 | J, M = multidict({1: 35, 2: 50, 3: 40}) # capacity 67 | c = {(1, 1): 8, (1, 2): 9, (1, 3): 14, # {(customer,factory) : cost} 68 | (2, 1): 6, (2, 2): 12, (2, 3): 9, 69 | (3, 1): 10, (3, 2): 13, (3, 3): 16, 70 | (4, 1): 9, (4, 2): 7, (4, 3): 5, 71 | } 72 | return I, J, c, d, M 73 | 74 | 75 | if __name__ == "__main__": 76 | I, J, c, d, M = make_inst1(); 77 | # I,J,c,d,M = make_inst2(); 78 | model = transp(I, J, c, d, M) 79 | model.optimize() 80 | 81 | print("Optimal value:", model.getObjVal()) 82 | 83 | EPS = 1.e-6 84 | x = model.data 85 | 86 | for (i, j) in x: 87 | if model.getVal(x[i, j]) > EPS: 88 | print("sending quantity %10s from factory %3s to customer %3s" % (model.getVal(x[i, j]), j, i)) 89 | -------------------------------------------------------------------------------- /examples/finished/transp_nofn.py: -------------------------------------------------------------------------------- 1 | ##@file transp_nofn.py 2 | # @brief a model for the transportation problem 3 | """ 4 | Model for solving a transportation problem: 5 | minimize the total transportation cost for satisfying demand at 6 | customers, from capacitated facilities. 7 | 8 | Data: 9 | I - set of customers 10 | J - set of facilities 11 | c[i,j] - unit transportation cost on arc (i,j) 12 | d[i] - demand at node i 13 | M[j] - capacity 14 | 15 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 16 | """ 17 | 18 | from pyscipopt import Model, quicksum 19 | 20 | d = {1: 80, 2: 270, 3: 250, 4: 160, 5: 180} # demand 21 | I = d.keys() 22 | 23 | M = {1: 500, 2: 500, 3: 500} # capacity 24 | J = M.keys() 25 | 26 | c = {(1, 1): 4, (1, 2): 6, (1, 3): 9, # cost 27 | (2, 1): 5, (2, 2): 4, (2, 3): 7, 28 | (3, 1): 6, (3, 2): 3, (3, 3): 4, 29 | (4, 1): 8, (4, 2): 5, (4, 3): 3, 30 | (5, 1): 10, (5, 2): 8, (5, 3): 4, 31 | } 32 | 33 | model = Model("transportation") 34 | 35 | # Create variables 36 | x = {} 37 | 38 | for i in I: 39 | for j in J: 40 | x[i, j] = model.addVar(vtype="C", name="x(%s,%s)" % (i, j)) 41 | 42 | # Demand constraints 43 | for i in I: 44 | model.addCons(sum(x[i, j] for j in J if (i, j) in x) == d[i], name="Demand(%s)" % i) 45 | 46 | # Capacity constraints 47 | for j in J: 48 | model.addCons(sum(x[i, j] for i in I if (i, j) in x) <= M[j], name="Capacity(%s)" % j) 49 | 50 | # Objective 51 | model.setObjective(quicksum(c[i, j] * x[i, j] for (i, j) in x), "minimize") 52 | 53 | model.optimize() 54 | 55 | print("Optimal value:", model.getObjVal()) 56 | 57 | EPS = 1.e-6 58 | 59 | for (i, j) in x: 60 | if model.getVal(x[i, j]) > EPS: 61 | print("sending quantity %10s from factory %3s to customer %3s" % (model.getVal(x[i, j]), j, i)) 62 | -------------------------------------------------------------------------------- /examples/tutorial/even.py: -------------------------------------------------------------------------------- 1 | ##@file tutorial/even.py 2 | # @brief Tutorial example to check whether values are even or odd 3 | """ 4 | Public Domain, WTFNMFPL Public Licence 5 | """ 6 | from pprint import pformat as pfmt 7 | 8 | from pyscipopt import Model 9 | 10 | example_values = [ 11 | 0, 12 | 1, 13 | 1.5, 14 | "helloworld", 15 | 20, 16 | 25, 17 | -101, 18 | -15., 19 | -10, 20 | -2 ** 31, 21 | -int(2 ** 31), 22 | "2**31-1", 23 | int(2 ** 31 - 1), 24 | int(2 ** 63) - 1 25 | ] 26 | 27 | verbose = False 28 | # verbose = True # uncomment for additional info on variables! 29 | sdic = {0: "even", 1: "odd"} # remainder to 2 30 | 31 | 32 | def parity(number): 33 | """ 34 | Prints if a value is even/odd/neither per each value in a example list 35 | 36 | This example is made for newcomers and motivated by: 37 | - modulus is unsupported for pyscipopt.scip.Variable and int 38 | - variables are non-integer by default 39 | Based on this: #172#issuecomment-394644046 40 | 41 | Args: 42 | number: value which parity is checked 43 | 44 | Returns: 45 | sval: 1 if number is odd, 0 if number is even, -1 if neither 46 | """ 47 | sval = -1 48 | if verbose: 49 | print(80 * "*") 50 | try: 51 | assert number == int(round(number)) 52 | m = Model() 53 | m.hideOutput() 54 | 55 | # x and n are integer, s is binary 56 | # Irrespective to their type, variables are non-negative by default 57 | # since 0 is the default lb. To allow for negative values, give None 58 | # as lower bound. 59 | # (None means -infinity as lower bound and +infinity as upper bound) 60 | x = m.addVar("x", vtype="I", lb=None, ub=None) # ub=None is default 61 | n = m.addVar("n", vtype="I", lb=None) 62 | s = m.addVar("s", vtype="B") 63 | # CAVEAT: if number is negative, x's lower bound must be None 64 | # if x is set by default as non-negative and number is negative: 65 | # there is no feasible solution (trivial) but the program 66 | # does not highlight which constraints conflict. 67 | 68 | m.addCons(x == number) 69 | 70 | # minimize the difference between the number and twice a natural number 71 | m.addCons(s == x - 2 * n) 72 | m.setObjective(s) 73 | m.optimize() 74 | 75 | assert m.getStatus() == "optimal" 76 | boolmod = m.getVal(s) == m.getVal(x) % 2 77 | if verbose: 78 | for v in m.getVars(): 79 | print("%*s: %d" % (fmtlen, v, m.getVal(v))) 80 | print("%*d%%2 == %d?" % (fmtlen, m.getVal(x), m.getVal(s))) 81 | print("%*s" % (fmtlen, boolmod)) 82 | 83 | xval = m.getVal(x) 84 | sval = m.getVal(s) 85 | sstr = sdic[sval] 86 | print("%*d is %s" % (fmtlen, xval, sstr)) 87 | except (AssertionError, TypeError): 88 | print("%*s is neither even nor odd!" % (fmtlen, number.__repr__())) 89 | finally: 90 | if verbose: 91 | print(80 * "*") 92 | print("") 93 | return sval 94 | 95 | 96 | if __name__ == "__main__": 97 | """ 98 | If positional arguments are given: 99 | the parity check is performed on each of them 100 | Else: 101 | the parity check is performed on each of the default example values 102 | """ 103 | import sys 104 | from ast import literal_eval as leval 105 | 106 | try: 107 | # check parity for each positional arguments 108 | sys.argv[1] 109 | values = sys.argv[1:] 110 | except IndexError: 111 | # check parity for each default example value 112 | values = example_values 113 | # format lenght, cosmetics 114 | fmtlen = max([len(fmt) for fmt in pfmt(values, width=1).split('\n')]) 115 | for value in values: 116 | try: 117 | n = leval(value) 118 | except (ValueError, SyntaxError): # for numbers or str w/ spaces 119 | n = value 120 | parity(n) 121 | -------------------------------------------------------------------------------- /examples/tutorial/logical.py: -------------------------------------------------------------------------------- 1 | ##@file tutorial/logical.py 2 | # @brief Tutorial example on how to use AND/OR/XOR constraints. 3 | """ 4 | N.B.: standard SCIP XOR constraint works differently from AND/OR by design. 5 | The constraint is set with a boolean rhs instead of an integer resultant. 6 | cf. http://listserv.zib.de/pipermail/scip/2018-May/003392.html 7 | A workaround to get the resultant as variable is here proposed. 8 | 9 | Public Domain, WTFNMFPL Public Licence 10 | """ 11 | from pyscipopt import Model 12 | from pyscipopt import quicksum 13 | 14 | 15 | def _init(): 16 | model = Model() 17 | model.hideOutput() 18 | x = model.addVar("x", "B") 19 | y = model.addVar("y", "B") 20 | z = model.addVar("z", "B") 21 | return model, x, y, z 22 | 23 | 24 | def _optimize(name, m): 25 | m.optimize() 26 | print("* %s constraint *" % name) 27 | objSet = bool(m.getObjective().terms.keys()) 28 | print("* Is objective set? %s" % objSet) 29 | if objSet: 30 | print("* Sense: %s" % m.getObjectiveSense()) 31 | status = m.getStatus() 32 | print("* Model status: %s" % status) 33 | if status == 'optimal': 34 | for v in m.getVars(): 35 | if v.name != "n": 36 | print("%s: %d" % (v, round(m.getVal(v)))) 37 | else: 38 | print("* No variable is printed if model status is not optimal") 39 | print("") 40 | 41 | 42 | def and_constraint(v=1, sense="minimize"): 43 | """ AND constraint """ 44 | assert v in [0, 1], "v must be 0 or 1 instead of %s" % v.__repr__() 45 | model, x, y, z = _init() 46 | r = model.addVar("r", "B") 47 | model.addConsAnd([x, y, z], r) 48 | model.addCons(x == v) 49 | model.setObjective(r, sense=sense) 50 | _optimize("AND", model) 51 | 52 | 53 | def or_constraint(v=0, sense="maximize"): 54 | """ OR constraint""" 55 | assert v in [0, 1], "v must be 0 or 1 instead of %s" % v.__repr__() 56 | model, x, y, z = _init() 57 | r = model.addVar("r", "B") 58 | model.addConsOr([x, y, z], r) 59 | model.addCons(x == v) 60 | model.setObjective(r, sense=sense) 61 | _optimize("OR", model) 62 | 63 | 64 | def xors_constraint(v=1): 65 | """ XOR (r as boolean) standard constraint""" 66 | assert v in [0, 1], "v must be 0 or 1 instead of %s" % v.__repr__() 67 | model, x, y, z = _init() 68 | r = True 69 | model.addConsXor([x, y, z], r) 70 | model.addCons(x == v) 71 | _optimize("Standard XOR (as boolean)", model) 72 | 73 | 74 | def xorc_constraint(v=0, sense="maximize"): 75 | """ XOR (r as variable) custom constraint""" 76 | assert v in [0, 1], "v must be 0 or 1 instead of %s" % v.__repr__() 77 | model, x, y, z = _init() 78 | r = model.addVar("r", "B") 79 | n = model.addVar("n", "I") # auxiliary 80 | model.addCons(r + quicksum([x, y, z]) == 2 * n) 81 | model.addCons(x == v) 82 | model.setObjective(r, sense=sense) 83 | _optimize("Custom XOR (as variable)", model) 84 | 85 | 86 | if __name__ == "__main__": 87 | and_constraint() 88 | or_constraint() 89 | xors_constraint() 90 | xorc_constraint() 91 | -------------------------------------------------------------------------------- /examples/tutorial/puzzle.py: -------------------------------------------------------------------------------- 1 | ##@file tutorial/puzzle.py 2 | # @brief solve a simple puzzle using SCIP 3 | """ 4 | On a beach there are octopuses, turtles and cranes. 5 | The total number of legs of all animals is 80, while the number of heads is 32. 6 | What are the minimum numbers of turtles and octopuses, respectively? 7 | 8 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 9 | """ 10 | from pyscipopt import Model 11 | 12 | model = Model("puzzle") 13 | x = model.addVar(vtype="I", name="octopusses") 14 | y = model.addVar(vtype="I", name="turtles") 15 | z = model.addVar(vtype="I", name="cranes") 16 | 17 | # Set up constraint for number of heads 18 | model.addCons(x + y + z == 32, name="Heads") 19 | 20 | # Set up constraint for number of legs 21 | model.addCons(8 * x + 4 * y + 2 * z == 80, name="Legs") 22 | 23 | # Set objective function 24 | model.setObjective(x + y, "minimize") 25 | 26 | model.hideOutput() 27 | model.optimize() 28 | 29 | # solution = model.getBestSol() 30 | 31 | print("Optimal value:", model.getObjVal()) 32 | print((x.name, y.name, z.name), " = ", (model.getVal(x), model.getVal(y), model.getVal(z))) 33 | -------------------------------------------------------------------------------- /examples/unfinished/eoq_soco.py: -------------------------------------------------------------------------------- 1 | """ 2 | eoq_soco.py: model to the multi-item economic ordering quantity problem. 3 | 4 | Approach: use second-order cone optimization. 5 | 6 | Copyright (c) by Joao Pedro PEDROSO, Masahiro MURAMATSU and Mikio KUBO, 2012 7 | """ 8 | from pyscipopt import Model, quicksum, multidict 9 | 10 | def eoq_soco(I,F,h,d,w,W): 11 | """eoq_soco -- multi-item capacitated economic ordering quantity model using soco 12 | Parameters: 13 | - I: set of items 14 | - F[i]: ordering cost for item i 15 | - h[i]: holding cost for item i 16 | - d[i]: demand for item i 17 | - w[i]: unit weight for item i 18 | - W: capacity (limit on order quantity) 19 | Returns a model, ready to be solved. 20 | """ 21 | model = Model("EOQ model using SOCO") 22 | 23 | T,c = {},{} 24 | for i in I: 25 | T[i] = model.addVar(vtype="C", name="T(%s)"%i) # cycle time for item i 26 | c[i] = model.addVar(vtype="C", name="c(%s)"%i) # total cost for item i 27 | 28 | for i in I: 29 | model.addCons(F[i] <= c[i]*T[i]) 30 | 31 | model.addCons(quicksum(w[i]*d[i]*T[i] for i in I) <= W) 32 | 33 | model.setObjective(quicksum(c[i] + h[i]*d[i]*T[i]*0.5 for i in I), "minimize") 34 | 35 | model.data = T,c 36 | return model 37 | 38 | 39 | 40 | if __name__ == "__main__": 41 | # multiple item EOQ 42 | I,F,h,d,w = multidict( 43 | {1:[300,10,10,20], 44 | 2:[300,10,30,40], 45 | 3:[300,10,50,10]} 46 | ) 47 | W = 2000 48 | model = eoq_soco(I,F,h,d,w,W) 49 | model.optimize() 50 | 51 | T,c = model.data 52 | EPS = 1.e-6 53 | print("%5s\t%8s\t%8s" % ("i","T[i]","c[i]")) 54 | for i in I: 55 | print("%5s\t%8g\t%8g" % (i,model.getVal(T[i]),model.getVal(c[i]))) 56 | print 57 | print("Optimal value:", model.getObjVal()) 58 | -------------------------------------------------------------------------------- /examples/unfinished/kcenter.py: -------------------------------------------------------------------------------- 1 | """ 2 | kcenter.py: model for solving the k-center problem. 3 | 4 | select k facility positions such that the maximum distance 5 | of each vertex in the graph to a facility is minimum 6 | 7 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 8 | """ 9 | from pyscipopt import Model, quicksum 10 | 11 | 12 | def kcenter(I,J,c,k): 13 | """kcenter -- minimize the maximum travel cost from customers to k facilities. 14 | Parameters: 15 | - I: set of customers 16 | - J: set of potential facilities 17 | - c[i,j]: cost of servicing customer i from facility j 18 | - k: number of facilities to be used 19 | Returns a model, ready to be solved. 20 | """ 21 | 22 | model = Model("k-center") 23 | z = model.addVar(vtype="C", name="z") 24 | x,y = {},{} 25 | 26 | for j in J: 27 | y[j] = model.addVar(vtype="B", name="y(%s)"%j) 28 | for i in I: 29 | x[i,j] = model.addVar(vtype="B", name="x(%s,%s)"%(i,j)) 30 | 31 | 32 | for i in I: 33 | model.addCons(quicksum(x[i,j] for j in J) == 1, "Assign(%s)"%i) 34 | 35 | for j in J: 36 | model.addCons(x[i,j] <= y[j], "Strong(%s,%s)"%(i,j)) 37 | model.addCons(c[i,j]*x[i,j] <= z, "Max_x(%s,%s)"%(i,j)) 38 | 39 | model.addCons(quicksum(y[j] for j in J) == k, "Facilities") 40 | 41 | model.setObjective(z, "minimize") 42 | model.data = x,y 43 | 44 | return model 45 | 46 | 47 | import math 48 | import random 49 | def distance(x1,y1,x2,y2): 50 | return math.sqrt((x2-x1)**2 + (y2-y1)**2) 51 | 52 | 53 | def make_data(n,m,same=True): 54 | if same == True: 55 | I = range(n) 56 | J = range(m) 57 | x = [random.random() for i in range(max(m,n))] # positions of the points in the plane 58 | y = [random.random() for i in range(max(m,n))] 59 | else: 60 | I = range(n) 61 | J = range(n,n+m) 62 | x = [random.random() for i in range(n+m)] # positions of the points in the plane 63 | y = [random.random() for i in range(n+m)] 64 | c = {} 65 | for i in I: 66 | for j in J: 67 | c[i,j] = distance(x[i],y[i],x[j],y[j]) 68 | 69 | return I,J,c,x,y 70 | 71 | 72 | if __name__ == "__main__": 73 | random.seed(67) 74 | n = 100 75 | m = n 76 | I,J,c,x_pos,y_pos = make_data(n,m,same=True) 77 | k = 10 78 | model = kcenter(I,J,c,k) 79 | model.optimize() 80 | EPS = 1.e-6 81 | x,y = model.data 82 | edges = [(i,j) for (i,j) in x if model.getVal(x[i,j]) > EPS] 83 | facilities = [j for j in y if model.getVal(y[j]) > EPS] 84 | 85 | print("Optimal value:", model.getObjVal()) 86 | print("Selected facilities:", facilities) 87 | print("Edges:", edges) 88 | print("max c:", max([c[i,j] for (i,j) in edges])) 89 | 90 | try: # plot the result using networkx and matplotlib 91 | import networkx as NX 92 | import matplotlib.pyplot as P 93 | P.clf() 94 | G = NX.Graph() 95 | 96 | facilities = set(j for j in J if model.getVal(y[j]) > EPS) 97 | other = set(j for j in J if j not in facilities) 98 | client = set(i for i in I if i not in facilities and i not in other) 99 | G.add_nodes_from(facilities) 100 | G.add_nodes_from(client) 101 | G.add_nodes_from(other) 102 | for (i,j) in edges: 103 | G.add_edge(i,j) 104 | 105 | position = {} 106 | for i in range(len(x_pos)): 107 | position[i] = (x_pos[i],y_pos[i]) 108 | 109 | NX.draw(G,position,with_labels=False,node_color="w",nodelist=facilities) 110 | NX.draw(G,position,with_labels=False,node_color="c",nodelist=other,node_size=50) 111 | NX.draw(G,position,with_labels=False,node_color="g",nodelist=client,node_size=50) 112 | P.show() 113 | except ImportError: 114 | print("install 'networkx' and 'matplotlib' for plotting") 115 | -------------------------------------------------------------------------------- /examples/unfinished/pareto_front.py: -------------------------------------------------------------------------------- 1 | """ 2 | pareto_front.py: tools for building a pareto front in multi-objective optimization 3 | 4 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 5 | """ 6 | 7 | def dominates(a,b): 8 | dominating = False 9 | for i in range(len(a)): 10 | if a[i] > b[i]: 11 | return False 12 | if a[i] < b[i]: 13 | dominating = True 14 | return dominating 15 | 16 | 17 | def is_dominated(a,front): 18 | for b in front: 19 | if dominates(b,a): 20 | return True 21 | return False 22 | 23 | def pareto_front(cand): 24 | front = set([]) 25 | for i in cand: 26 | add_i = True 27 | for j in list(front): 28 | if dominates(i,j): 29 | front.remove(j) 30 | if dominates(j,i): 31 | add_i = False 32 | if add_i: 33 | front.add(i) 34 | front = list(front) 35 | front.sort() 36 | return front 37 | 38 | 39 | if __name__ == "__main__": 40 | import random 41 | # random.seed(1) 42 | cand = [(random.random()**.25,random.random()**.25) for i in range(100)] 43 | import matplotlib.pyplot as plt 44 | for (x,y) in cand: 45 | plt.plot(x,y,"bo") 46 | 47 | front = pareto_front(cand) 48 | plt.plot([x for (x,y) in front], [y for (x,y) in front]) 49 | plt.show() 50 | -------------------------------------------------------------------------------- /examples/unfinished/portfolio_soco.py: -------------------------------------------------------------------------------- 1 | """ 2 | portfolio_soco.py: modified markowitz model for portfolio optimization. 3 | 4 | Approach: use second-order cone optimization. 5 | 6 | Copyright (c) by Joao Pedro PEDROSO, Masahiro MURAMATSU and Mikio KUBO, 2012 7 | """ 8 | import math 9 | 10 | from pyscipopt import Model, quicksum, multidict 11 | 12 | 13 | def phi_inv(p): 14 | """phi_inv: inverse of gaussian (normal) CDF 15 | Source: 16 | Handbook of Mathematical Functions 17 | Dover Books on Mathematics 18 | Milton Abramowitz and Irene A. Stegun (Editors) 19 | Formula 26.2.23. 20 | """ 21 | if p < 0.5: 22 | t = math.sqrt(-2.0*math.log(p)) 23 | return ((0.010328*t + 0.802853)*t + 2.515517)/(((0.001308*t + 0.189269)*t + 1.432788)*t + 1.0) - t 24 | else: 25 | t = math.sqrt(-2.0*math.log(1.0-p)) 26 | return t - ((0.010328*t + 0.802853)*t + 2.515517)/(((0.001308*t + 0.189269)*t + 1.432788)*t + 1.0) 27 | 28 | 29 | def p_portfolio(I,sigma,r,alpha,beta): 30 | """p_portfolio -- modified markowitz model for portfolio optimization. 31 | Parameters: 32 | - I: set of items 33 | - sigma[i]: standard deviation of item i 34 | - r[i]: revenue of item i 35 | - alpha: acceptance threshold 36 | - beta: desired confidence level 37 | Returns a model, ready to be solved. 38 | """ 39 | 40 | model = Model("p_portfolio") 41 | 42 | x = {} 43 | for i in I: 44 | x[i] = model.addVar(vtype="C", name="x(%s)"%i) # quantity of i to buy 45 | rho = model.addVar(vtype="C", name="rho") 46 | rhoaux = model.addVar(vtype="C", name="rhoaux") 47 | 48 | model.addCons(rho == quicksum(r[i]*x[i] for i in I)) 49 | model.addCons(quicksum(x[i] for i in I) == 1) 50 | 51 | model.addCons(rhoaux == (alpha - rho)*(1/phi_inv(beta))) #todo 52 | model.addCons(quicksum(sigma[i]**2 * x[i] * x[i] for i in I) <= rhoaux * rhoaux) 53 | 54 | model.setObjective(rho, "maximize") 55 | 56 | model.data = x 57 | return model 58 | 59 | 60 | 61 | if __name__ == "__main__": 62 | # portfolio 63 | I,sigma,r = multidict( 64 | {1:[0.07,1.01], 65 | 2:[0.09,1.05], 66 | 3:[0.1,1.08], 67 | 4:[0.2,1.10], 68 | 5:[0.3,1.20]} 69 | ) 70 | alpha = 0.95 71 | # beta = 0.1 72 | 73 | for beta in [0.1, 0.05, 0.02, 0.01]: 74 | print("\n\n\nbeta:",beta,"phi inv:",phi_inv(beta)) 75 | model = p_portfolio(I,sigma,r,alpha,beta) 76 | model.optimize() 77 | 78 | x = model.data 79 | EPS = 1.e-6 80 | print("Investment:") 81 | print("%5s\t%8s" % ("i","x[i]")) 82 | for i in I: 83 | print("%5s\t%8g" % (i,model.getVal(x[i]))) 84 | 85 | print("Objective:",model.getObjVal()) 86 | -------------------------------------------------------------------------------- /examples/unfinished/vrp.py: -------------------------------------------------------------------------------- 1 | """ 2 | vrp.py: solve the vehicle routing problem. 3 | 4 | approach: 5 | - start with assignment model 6 | - add cuts until all components of the graph are connected 7 | 8 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 9 | """ 10 | import math 11 | import random 12 | 13 | import networkx 14 | from pyscipopt import Model, quicksum 15 | 16 | 17 | def solve_vrp(V, c, m, q, Q): 18 | """solve_vrp -- solve the vehicle routing problem. 19 | - start with assignment model (depot has a special status) 20 | - add cuts until all components of the graph are connected 21 | Parameters: 22 | - V: set/list of nodes in the graph 23 | - c[i,j]: cost for traversing edge (i,j) 24 | - m: number of vehicles available 25 | - q[i]: demand for customer i 26 | - Q: vehicle capacity 27 | Returns the optimum objective value and the list of edges used. 28 | """ 29 | 30 | def addcut(cut_edges): 31 | """addcut: add constraint to eliminate infeasible solutions 32 | Parameters: 33 | - cut_edges: list of edges in the current solution, except connections to depot 34 | Returns True if a cut was added, False otherwise 35 | """ 36 | G = networkx.Graph() 37 | G.add_edges_from(cut_edges) 38 | Components = networkx.connected_components(G) 39 | cut = False 40 | for S in Components: 41 | S_card = len(S) 42 | q_sum = sum(q[i] for i in S) 43 | NS = int(math.ceil(float(q_sum) / Q)) 44 | S_edges = [(i, j) for i in S for j in S if i < j and (i, j) in cut_edges] 45 | if S_card >= 3 and (len(S_edges) >= S_card or NS > 1): 46 | print("Adding a cut") 47 | add = model.addCons(quicksum(x[i, j] for i in S for j in S if j > i) <= S_card - NS) 48 | cut = True 49 | return cut 50 | 51 | model = Model("vrp") 52 | 53 | x = {} 54 | for i in V: 55 | for j in V: 56 | if j > i and i == V[0]: # depot 57 | x[i, j] = model.addVar(ub=2, vtype="I", name="x(%s,%s)" % (i, j)) 58 | elif j > i: 59 | x[i, j] = model.addVar(ub=1, vtype="I", name="x(%s,%s)" % (i, j)) 60 | 61 | model.addCons(quicksum(x[V[0], j] for j in V[1:]) == 2 * m, "DegreeDepot") 62 | for i in V[1:]: 63 | model.addCons(quicksum(x[j, i] for j in V if j < i) + 64 | quicksum(x[i, j] for j in V if j > i) == 2, "Degree(%s)" % i) 65 | 66 | model.setObjective(quicksum(c[i, j] * x[i, j] for i in V for j in V if j > i), "minimize") 67 | 68 | model.hideOutput() 69 | 70 | EPS = 1.e-6 71 | while True: 72 | model.optimize() 73 | edges = [] 74 | for (i, j) in x: 75 | if model.getVal(x[i, j]) > EPS: 76 | if i != V[0] and j != V[0]: 77 | edges.append((i, j)) 78 | model.freeTransform() 79 | if addcut(edges) == False: 80 | break 81 | 82 | return model.getObjVal(), edges 83 | 84 | 85 | def distance(x1, y1, x2, y2): 86 | """distance: euclidean distance between (x1,y1) and (x2,y2)""" 87 | return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) 88 | 89 | 90 | def make_data(n): 91 | """make_data: compute matrix distance based on euclidean distance""" 92 | V = range(1, n + 1) 93 | x = dict([(i, random.random()) for i in V]) 94 | y = dict([(i, random.random()) for i in V]) 95 | c, q = {}, {} 96 | Q = 100 97 | for i in V: 98 | q[i] = random.randint(10, 20) 99 | for j in V: 100 | if j > i: 101 | c[i, j] = distance(x[i], y[i], x[j], y[j]) 102 | return V, c, q, Q 103 | 104 | 105 | if __name__ == "__main__": 106 | n = 5 107 | m = 3 108 | seed = 1 109 | random.seed(seed) 110 | V, c, q, Q = make_data(n) 111 | z, edges = solve_vrp(V, c, m, q, Q) 112 | print("Optimal solution:", z) 113 | print("Edges in the solution:") 114 | print(sorted(edges)) 115 | -------------------------------------------------------------------------------- /generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | GITHUB_TOKEN=$1 4 | if [ $GITHUB_TOKEN == "" ]; then 5 | echo "Fatal: Missing access token, exiting." 6 | exit 1 7 | fi 8 | 9 | # for live documentation branch set DOCBRANCH=gh-pages 10 | DOCBRANCH=$2 11 | if [ $2 == "" ]; then 12 | echo "Fatal: Missing branch, exiting." 13 | exit 1 14 | fi 15 | 16 | # get repo info 17 | REPO_SLUG=$GITHUB_REPOSITORY 18 | BUILD_COMMIT=$GITHUB_SHA 19 | BUILD_NUMBER=$GITHUB_RUN_ID 20 | 21 | GH_REPO_ORG=`echo $REPO_SLUG | cut -d "/" -f 1` 22 | GH_REPO_NAME=`echo $REPO_SLUG | cut -d "/" -f 2` 23 | GH_REPO_REF="github.com/$GH_REPO_ORG/$GH_REPO_NAME.git" 24 | 25 | # clone the docu branch 26 | echo "cloning branch ${DOCBRANCH} from repo git@github.com:${GH_REPO_ORG}/${GH_REPO_NAME}" 27 | git clone -b ${DOCBRANCH} https://${GH_REPO_ORG}:${GITHUB_TOKEN}@github.com/${GH_REPO_ORG}/${GH_REPO_NAME} code_docs 28 | 29 | #get SCIP TAGFILE 30 | echo "Downloading SCIP tagfile to create links to SCIP docu" 31 | wget -q -O docs/scip.tag https://scip.zib.de/doc/scip.tag 32 | 33 | #get version number for doxygen 34 | export VERSION_NUMBER=$(grep "__version__" src/pyscipopt/_version.py | cut -d ' ' -f 3 | tr --delete \') 35 | 36 | # generate html documentation in docs/html 37 | echo "Generating documentation" 38 | doxygen docs/doxy 39 | 40 | # fix broken links to SCIP online documentation 41 | # If you set `HTML_FILE_EXTENSION = .php` in doc/doxy you don't need the following sed commands 42 | sed -i "s@\.php\.html@.php@g" docs/html/*.* docs/html/search/*.* 43 | sed -i -E "s@(scip.zib.de.*)\.html@\1.php@g" docs/html/*.* docs/html/search/*.* 44 | 45 | # change into the clone of the previously cloned repo 46 | cd code_docs 47 | 48 | ##### Configure git. 49 | # Set the push default to simple i.e. push only the current branch. 50 | git config --global push.default simple 51 | # Pretend to be an user called SCIP CI Bot 52 | git config user.name "SCIP CI Bot" 53 | git config user.email "timo-admin@zib.de" 54 | 55 | git remote set-url --push origin https://${GH_REPO_ORG}:${GITHUB_TOKEN}@github.com/${GH_REPO_ORG}/${GH_REPO_NAME} 56 | 57 | # go back to first commit 58 | git reset --hard `git rev-list --max-parents=0 --abbrev-commit HEAD` 59 | 60 | # copy new docu files to gh-pages 61 | mkdir -p docs/html 62 | mv ../docs/html/* docs/html/ 63 | git add --all 64 | git commit -m "Deploy docs to GitHub Pages, GitHub Actions build: ${BUILD_NUMBER}" -m "Commit: ${BUILD_COMMIT}" 65 | 66 | # Force push to the remote gh-pages branch. 67 | git push --force https://${GH_REPO_ORG}:${GITHUB_TOKEN}@github.com/${GH_REPO_ORG}/${GH_REPO_NAME} 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools', 'cython >=0.21'] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "PySCIPOpt" 7 | description = "Python interface and modeling environment for SCIP" 8 | authors = [ 9 | {name = "Zuse Institute Berlin", email = "scip@zib.de"}, 10 | ] 11 | dependencies = ['numpy >=1.16.0'] 12 | requires-python = ">=3.8" 13 | readme = "README.md" 14 | license = {text = "MIT"} 15 | classifiers = [ 16 | "Development Status :: 4 - Beta", 17 | "Intended Audience :: Education", 18 | "Intended Audience :: Science/Research", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Cython", 21 | "Programming Language :: Python :: 3", 22 | "Topic :: Scientific/Engineering :: Mathematics", 23 | ] 24 | dynamic = ["version"] 25 | 26 | [project.urls] 27 | Homepage = "https://github.com/SCIP-Interfaces/PySCIPOpt" 28 | 29 | [tool.pytest.ini_options] 30 | norecursedirs = ["check"] 31 | testpaths = ["tests"] 32 | 33 | [tool.setuptools] 34 | include-package-data = false 35 | 36 | [tool.setuptools.dynamic] 37 | version = {attr = "pyscipopt._version.__version__"} 38 | 39 | [tool.cibuildwheel] 40 | skip="pp*" # currently doesn't work with PyPy 41 | manylinux-x86_64-image = "manylinux_2_28" 42 | 43 | 44 | [tool.cibuildwheel.linux] 45 | skip="pp* cp36* cp37* *musllinux*" 46 | before-all = [ 47 | "(apt-get update && apt-get install --yes wget) || yum install -y wget zlib libgfortran || brew install wget", 48 | "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.7.0/libscip-linux.zip -O scip.zip", 49 | "unzip scip.zip", 50 | "mv scip_install scip" 51 | ] 52 | environment = { SCIPOPTDIR="$(pwd)/scip", LD_LIBRARY_PATH="$(pwd)/scip/lib:$LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} 53 | 54 | 55 | [tool.cibuildwheel.macos] 56 | skip="pp* cp36* cp37*" 57 | before-all = ''' 58 | #!/bin/bash 59 | brew install wget zlib gcc 60 | if [[ $CIBW_ARCHS == *"arm"* ]]; then 61 | wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.7.0/libscip-macos-arm.zip -O scip.zip 62 | export MACOSX_DEPLOYMENT_TARGET=14.0 63 | else 64 | wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.7.0/libscip-macos-intel.zip -O scip.zip 65 | export MACOSX_DEPLOYMENT_TARGET=13.0 66 | fi 67 | unzip scip.zip 68 | mv scip_install src/scip 69 | ''' 70 | environment = {SCIPOPTDIR="$(pwd)/src/scip", LD_LIBRARY_PATH="$(pwd)/src/scip/lib:LD_LIBRARY_PATH", DYLD_LIBRARY_PATH="$(pwd)/src/scip/lib:$DYLD_LIBRARY_PATH", PATH="$(pwd)/src/scip/bin:$PATH", PKG_CONFIG_PATH="$(pwd)/src/scip/lib/pkgconfig:$PKG_CONFIG_PATH", RELEASE="true"} 71 | repair-wheel-command = ''' 72 | bash -c ' 73 | if [[ $CIBW_ARCHS == *"arm"* ]]; then 74 | export MACOSX_DEPLOYMENT_TARGET=14.0 75 | delocate-listdeps {wheel} 76 | delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} 77 | else 78 | export MACOSX_DEPLOYMENT_TARGET=13.0 79 | delocate-listdeps {wheel} 80 | delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel} 81 | fi 82 | ' 83 | ''' 84 | 85 | 86 | [tool.cibuildwheel.windows] 87 | skip="pp* cp36* cp37*" 88 | before-all = [ 89 | "choco install 7zip wget", 90 | "wget https://github.com/scipopt/scipoptsuite-deploy/releases/download/v0.7.0/libscip-windows.zip -O scip.zip", 91 | "\"C:\\Program Files\\7-Zip\\7z.exe\" x \"scip.zip\" -o\"scip-test\"", 92 | "mv .\\scip-test\\scip_install .\\test", 93 | "mv .\\test .\\scip" 94 | ] 95 | before-build = "pip install delvewheel" 96 | environment = { SCIPOPTDIR='D:\\a\\PySCIPOpt\\PySCIPOpt\\scip', RELEASE="true" } 97 | repair-wheel-command = "delvewheel repair --add-path c:/bin;c:/lib;c:/bin/src;c:/lib/src;D:/a/PySCIPOpt/PySCIPOpt/scip/;D:/a/PySCIPOpt/PySCIPOpt/scip/lib/;D:/a/PySCIPOpt/PySCIPOpt/scip/bin/ -w {dest_dir} {wheel}" 98 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pyscipopt/Multidict.py: -------------------------------------------------------------------------------- 1 | ##@file Multidict.py 2 | #@brief Implementation of Multidictionaries 3 | def multidict(D): 4 | '''creates a multidictionary''' 5 | keys = list(D.keys()) 6 | if len(keys) == 0: 7 | return [[]] 8 | try: 9 | N = len(D[keys[0]]) 10 | islist = True 11 | except: 12 | N = 1 13 | islist = False 14 | dlist = [dict() for d in range(N)] 15 | for k in keys: 16 | if islist: 17 | for i in range(N): 18 | dlist[i][k] = D[k][i] 19 | else: 20 | dlist[0][k] = D[k] 21 | return [keys]+dlist 22 | -------------------------------------------------------------------------------- /src/pyscipopt/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__ 2 | 3 | # required for Python 3.8 on Windows 4 | import os 5 | if hasattr(os, 'add_dll_directory'): 6 | if os.getenv('SCIPOPTDIR'): 7 | os.add_dll_directory(os.path.join(os.getenv('SCIPOPTDIR').strip('"'), 'bin')) 8 | 9 | # export user-relevant objects: 10 | from pyscipopt.Multidict import multidict 11 | from pyscipopt.scip import Model 12 | from pyscipopt.scip import Variable 13 | from pyscipopt.scip import MatrixVariable 14 | from pyscipopt.scip import Constraint 15 | from pyscipopt.scip import MatrixConstraint 16 | from pyscipopt.scip import Benders 17 | from pyscipopt.scip import Benderscut 18 | from pyscipopt.scip import Branchrule 19 | from pyscipopt.scip import Nodesel 20 | from pyscipopt.scip import Conshdlr 21 | from pyscipopt.scip import Eventhdlr 22 | from pyscipopt.scip import Heur 23 | from pyscipopt.scip import Presol 24 | from pyscipopt.scip import Pricer 25 | from pyscipopt.scip import Prop 26 | from pyscipopt.scip import Reader 27 | from pyscipopt.scip import Sepa 28 | from pyscipopt.scip import LP 29 | from pyscipopt.scip import PY_SCIP_LPPARAM as SCIP_LPPARAM 30 | from pyscipopt.scip import readStatistics 31 | from pyscipopt.scip import Expr 32 | from pyscipopt.scip import MatrixExpr 33 | from pyscipopt.scip import MatrixExprCons 34 | from pyscipopt.scip import ExprCons 35 | from pyscipopt.scip import quicksum 36 | from pyscipopt.scip import quickprod 37 | from pyscipopt.scip import exp 38 | from pyscipopt.scip import log 39 | from pyscipopt.scip import sqrt 40 | from pyscipopt.scip import sin 41 | from pyscipopt.scip import cos 42 | from pyscipopt.scip import PY_SCIP_RESULT as SCIP_RESULT 43 | from pyscipopt.scip import PY_SCIP_PARAMSETTING as SCIP_PARAMSETTING 44 | from pyscipopt.scip import PY_SCIP_PARAMEMPHASIS as SCIP_PARAMEMPHASIS 45 | from pyscipopt.scip import PY_SCIP_STATUS as SCIP_STATUS 46 | from pyscipopt.scip import PY_SCIP_STAGE as SCIP_STAGE 47 | from pyscipopt.scip import PY_SCIP_PROPTIMING as SCIP_PROPTIMING 48 | from pyscipopt.scip import PY_SCIP_PRESOLTIMING as SCIP_PRESOLTIMING 49 | from pyscipopt.scip import PY_SCIP_HEURTIMING as SCIP_HEURTIMING 50 | from pyscipopt.scip import PY_SCIP_EVENTTYPE as SCIP_EVENTTYPE 51 | from pyscipopt.scip import PY_SCIP_LOCKTYPE as SCIP_LOCKTYPE 52 | from pyscipopt.scip import PY_SCIP_LPSOLSTAT as SCIP_LPSOLSTAT 53 | from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR 54 | from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE 55 | from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE 56 | from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN 57 | -------------------------------------------------------------------------------- /src/pyscipopt/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = '5.5.0' 2 | -------------------------------------------------------------------------------- /src/pyscipopt/benderscut.pxi: -------------------------------------------------------------------------------- 1 | ##@file benderscut.pxi 2 | #@brief Base class of the Benderscut Plugin 3 | cdef class Benderscut: 4 | cdef public Model model 5 | cdef public Benders benders 6 | cdef public str name 7 | 8 | def benderscutfree(self): 9 | pass 10 | 11 | def benderscutinit(self): 12 | pass 13 | 14 | def benderscutexit(self): 15 | pass 16 | 17 | def benderscutinitsol(self): 18 | pass 19 | 20 | def benderscutexitsol(self): 21 | pass 22 | 23 | def benderscutexec(self, solution, probnumber, enfotype): 24 | print("python error in benderscutexec: this method needs to be implemented") 25 | return {} 26 | 27 | cdef SCIP_RETCODE PyBenderscutCopy (SCIP* scip, SCIP_BENDERS* benders, SCIP_BENDERSCUT* benderscut) noexcept with gil: 28 | return SCIP_OKAY 29 | 30 | cdef SCIP_RETCODE PyBenderscutFree (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 31 | cdef SCIP_BENDERSCUTDATA* benderscutdata 32 | benderscutdata = SCIPbenderscutGetData(benderscut) 33 | PyBenderscut = benderscutdata 34 | PyBenderscut.benderscutfree() 35 | Py_DECREF(PyBenderscut) 36 | return SCIP_OKAY 37 | 38 | cdef SCIP_RETCODE PyBenderscutInit (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 39 | cdef SCIP_BENDERSCUTDATA* benderscutdata 40 | benderscutdata = SCIPbenderscutGetData(benderscut) 41 | PyBenderscut = benderscutdata 42 | PyBenderscut.benderscutinit() 43 | return SCIP_OKAY 44 | 45 | cdef SCIP_RETCODE PyBenderscutExit (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 46 | cdef SCIP_BENDERSCUTDATA* benderscutdata 47 | benderscutdata = SCIPbenderscutGetData(benderscut) 48 | PyBenderscut = benderscutdata 49 | PyBenderscut.benderscutexit() 50 | return SCIP_OKAY 51 | 52 | cdef SCIP_RETCODE PyBenderscutInitsol (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 53 | cdef SCIP_BENDERSCUTDATA* benderscutdata 54 | benderscutdata = SCIPbenderscutGetData(benderscut) 55 | PyBenderscut = benderscutdata 56 | PyBenderscut.benderscutinitsol() 57 | return SCIP_OKAY 58 | 59 | cdef SCIP_RETCODE PyBenderscutExitsol (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 60 | cdef SCIP_BENDERSCUTDATA* benderscutdata 61 | benderscutdata = SCIPbenderscutGetData(benderscut) 62 | PyBenderscut = benderscutdata 63 | PyBenderscut.benderscutexitsol() 64 | return SCIP_OKAY 65 | 66 | cdef SCIP_RETCODE PyBenderscutExec (SCIP* scip, SCIP_BENDERS* benders, SCIP_BENDERSCUT* benderscut, SCIP_SOL* sol, int probnumber, SCIP_BENDERSENFOTYPE type, SCIP_RESULT* result) noexcept with gil: 67 | cdef SCIP_BENDERSCUTDATA* benderscutdata 68 | benderscutdata = SCIPbenderscutGetData(benderscut) 69 | PyBenderscut = benderscutdata 70 | if sol == NULL: 71 | solution = None 72 | else: 73 | solution = Solution.create(scip, sol) 74 | enfotype = type 75 | result_dict = PyBenderscut.benderscutexec(solution, probnumber, enfotype) 76 | result[0] = result_dict.get("result", result[0]) 77 | return SCIP_OKAY 78 | -------------------------------------------------------------------------------- /src/pyscipopt/cutsel.pxi: -------------------------------------------------------------------------------- 1 | ##@file cutsel.pxi 2 | #@brief Base class of the Cutsel Plugin 3 | cdef class Cutsel: 4 | cdef public Model model 5 | 6 | def cutselfree(self): 7 | '''frees memory of cut selector''' 8 | pass 9 | 10 | def cutselinit(self): 11 | ''' executed after the problem is transformed. use this call to initialize cut selector data.''' 12 | pass 13 | 14 | def cutselexit(self): 15 | '''executed before the transformed problem is freed''' 16 | pass 17 | 18 | def cutselinitsol(self): 19 | '''executed when the presolving is finished and the branch-and-bound process is about to begin''' 20 | pass 21 | 22 | def cutselexitsol(self): 23 | '''executed before the branch-and-bound process is freed''' 24 | pass 25 | 26 | def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): 27 | '''first method called in each iteration in the main solving loop. ''' 28 | # this method needs to be implemented by the user 29 | return {} 30 | 31 | 32 | cdef SCIP_RETCODE PyCutselCopy (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 33 | return SCIP_OKAY 34 | 35 | cdef SCIP_RETCODE PyCutselFree (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 36 | cdef SCIP_CUTSELDATA* cutseldata 37 | cutseldata = SCIPcutselGetData(cutsel) 38 | PyCutsel = cutseldata 39 | PyCutsel.cutselfree() 40 | Py_DECREF(PyCutsel) 41 | return SCIP_OKAY 42 | 43 | cdef SCIP_RETCODE PyCutselInit (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 44 | cdef SCIP_CUTSELDATA* cutseldata 45 | cutseldata = SCIPcutselGetData(cutsel) 46 | PyCutsel = cutseldata 47 | PyCutsel.cutselinit() 48 | return SCIP_OKAY 49 | 50 | 51 | cdef SCIP_RETCODE PyCutselExit (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 52 | cdef SCIP_CUTSELDATA* cutseldata 53 | cutseldata = SCIPcutselGetData(cutsel) 54 | PyCutsel = cutseldata 55 | PyCutsel.cutselexit() 56 | return SCIP_OKAY 57 | 58 | cdef SCIP_RETCODE PyCutselInitsol (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 59 | cdef SCIP_CUTSELDATA* cutseldata 60 | cutseldata = SCIPcutselGetData(cutsel) 61 | PyCutsel = cutseldata 62 | PyCutsel.cutselinitsol() 63 | return SCIP_OKAY 64 | 65 | cdef SCIP_RETCODE PyCutselExitsol (SCIP* scip, SCIP_CUTSEL* cutsel) noexcept with gil: 66 | cdef SCIP_CUTSELDATA* cutseldata 67 | cutseldata = SCIPcutselGetData(cutsel) 68 | PyCutsel = cutseldata 69 | PyCutsel.cutselexitsol() 70 | return SCIP_OKAY 71 | 72 | cdef SCIP_RETCODE PyCutselSelect (SCIP* scip, SCIP_CUTSEL* cutsel, SCIP_ROW** cuts, int ncuts, 73 | SCIP_ROW** forcedcuts, int nforcedcuts, SCIP_Bool root, int maxnselectedcuts, 74 | int* nselectedcuts, SCIP_RESULT* result) noexcept with gil: 75 | cdef SCIP_CUTSELDATA* cutseldata = SCIPcutselGetData(cutsel) 76 | cdef SCIP_ROW* scip_row 77 | cdef int i 78 | 79 | PyCutsel = cutseldata 80 | 81 | # translate cuts to python 82 | pycuts = [Row.create(cuts[i]) for i in range(ncuts)] 83 | pyforcedcuts = [Row.create(forcedcuts[i]) for i in range(nforcedcuts)] 84 | result_dict = PyCutsel.cutselselect(pycuts, pyforcedcuts, root, maxnselectedcuts) 85 | 86 | # Retrieve the sorted cuts. Note that these do not need to be returned explicitly in result_dict. 87 | # Pycuts could have been sorted in place in cutselselect() 88 | pycuts = result_dict.get('cuts', pycuts) 89 | 90 | assert len(pycuts) == ncuts 91 | assert len(pyforcedcuts) == nforcedcuts 92 | 93 | #sort cuts 94 | for i,cut in enumerate(pycuts): 95 | cuts[i] = ((cut).scip_row) 96 | 97 | nselectedcuts[0] = result_dict.get('nselectedcuts', 0) 98 | result[0] = result_dict.get('result', result[0]) 99 | 100 | return SCIP_OKAY 101 | -------------------------------------------------------------------------------- /src/pyscipopt/event.pxi: -------------------------------------------------------------------------------- 1 | ##@file event.pxi 2 | #@brief Base class of the Event Handler Plugin 3 | cdef class Eventhdlr: 4 | cdef public Model model 5 | cdef public str name 6 | 7 | def eventcopy(self): 8 | '''sets copy callback for all events of this event handler ''' 9 | pass 10 | 11 | def eventfree(self): 12 | '''calls destructor and frees memory of event handler ''' 13 | pass 14 | 15 | def eventinit(self): 16 | '''initializes event handler''' 17 | pass 18 | 19 | def eventexit(self): 20 | '''calls exit method of event handler''' 21 | pass 22 | 23 | def eventinitsol(self): 24 | '''informs event handler that the branch and bound process is being started ''' 25 | pass 26 | 27 | def eventexitsol(self): 28 | '''informs event handler that the branch and bound process data is being freed ''' 29 | pass 30 | 31 | def eventdelete(self): 32 | '''sets callback to free specific event data''' 33 | pass 34 | 35 | def eventexec(self, event): 36 | '''calls execution method of event handler ''' 37 | print("python error in eventexec: this method needs to be implemented") 38 | return {} 39 | 40 | 41 | # local helper functions for the interface 42 | cdef Eventhdlr getPyEventhdlr(SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 43 | cdef SCIP_EVENTHDLRDATA* eventhdlrdata 44 | eventhdlrdata = SCIPeventhdlrGetData(eventhdlr) 45 | return eventhdlrdata 46 | 47 | cdef SCIP_RETCODE PyEventCopy (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 48 | PyEventhdlr = getPyEventhdlr(eventhdlr) 49 | PyEventhdlr.eventcopy() 50 | return SCIP_OKAY 51 | 52 | cdef SCIP_RETCODE PyEventFree (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 53 | PyEventhdlr = getPyEventhdlr(eventhdlr) 54 | PyEventhdlr.eventfree() 55 | Py_DECREF(PyEventhdlr) 56 | return SCIP_OKAY 57 | 58 | cdef SCIP_RETCODE PyEventInit (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 59 | PyEventhdlr = getPyEventhdlr(eventhdlr) 60 | PyEventhdlr.eventinit() 61 | return SCIP_OKAY 62 | 63 | cdef SCIP_RETCODE PyEventExit (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 64 | PyEventhdlr = getPyEventhdlr(eventhdlr) 65 | PyEventhdlr.eventexit() 66 | return SCIP_OKAY 67 | 68 | cdef SCIP_RETCODE PyEventInitsol (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 69 | PyEventhdlr = getPyEventhdlr(eventhdlr) 70 | PyEventhdlr.eventinitsol() 71 | return SCIP_OKAY 72 | 73 | cdef SCIP_RETCODE PyEventExitsol (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 74 | PyEventhdlr = getPyEventhdlr(eventhdlr) 75 | PyEventhdlr.eventexitsol() 76 | return SCIP_OKAY 77 | 78 | cdef SCIP_RETCODE PyEventDelete (SCIP* scip, SCIP_EVENTHDLR* eventhdlr, SCIP_EVENTDATA** eventdata) noexcept with gil: 79 | PyEventhdlr = getPyEventhdlr(eventhdlr) 80 | PyEventhdlr.eventdelete() 81 | return SCIP_OKAY 82 | 83 | cdef SCIP_RETCODE PyEventExec (SCIP* scip, SCIP_EVENTHDLR* eventhdlr, SCIP_EVENT* event, SCIP_EVENTDATA* eventdata) noexcept with gil: 84 | PyEventhdlr = getPyEventhdlr(eventhdlr) 85 | PyEvent = Event() 86 | PyEvent.event = event 87 | PyEventhdlr.eventexec(PyEvent) 88 | return SCIP_OKAY 89 | -------------------------------------------------------------------------------- /src/pyscipopt/heuristic.pxi: -------------------------------------------------------------------------------- 1 | ##@file heuristic.pxi 2 | #@brief Base class of the Heuristics Plugin 3 | cdef class Heur: 4 | cdef public Model model 5 | cdef public str name 6 | 7 | def heurfree(self): 8 | '''calls destructor and frees memory of primal heuristic''' 9 | pass 10 | 11 | def heurinit(self): 12 | '''initializes primal heuristic''' 13 | pass 14 | 15 | def heurexit(self): 16 | '''calls exit method of primal heuristic''' 17 | pass 18 | 19 | def heurinitsol(self): 20 | '''informs primal heuristic that the branch and bound process is being started''' 21 | pass 22 | 23 | def heurexitsol(self): 24 | '''informs primal heuristic that the branch and bound process data is being freed''' 25 | pass 26 | 27 | def heurexec(self, heurtiming, nodeinfeasible): 28 | '''should the heuristic the executed at the given depth, frequency, timing,...''' 29 | print("python error in heurexec: this method needs to be implemented") 30 | return {} 31 | 32 | 33 | 34 | cdef SCIP_RETCODE PyHeurCopy (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 35 | return SCIP_OKAY 36 | 37 | cdef SCIP_RETCODE PyHeurFree (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 38 | cdef SCIP_HEURDATA* heurdata 39 | heurdata = SCIPheurGetData(heur) 40 | PyHeur = heurdata 41 | PyHeur.heurfree() 42 | Py_DECREF(PyHeur) 43 | return SCIP_OKAY 44 | 45 | cdef SCIP_RETCODE PyHeurInit (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 46 | cdef SCIP_HEURDATA* heurdata 47 | heurdata = SCIPheurGetData(heur) 48 | PyHeur = heurdata 49 | PyHeur.heurinit() 50 | return SCIP_OKAY 51 | 52 | cdef SCIP_RETCODE PyHeurExit (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 53 | cdef SCIP_HEURDATA* heurdata 54 | heurdata = SCIPheurGetData(heur) 55 | PyHeur = heurdata 56 | PyHeur.heurexit() 57 | return SCIP_OKAY 58 | 59 | cdef SCIP_RETCODE PyHeurInitsol (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 60 | cdef SCIP_HEURDATA* heurdata 61 | heurdata = SCIPheurGetData(heur) 62 | PyHeur = heurdata 63 | PyHeur.heurinitsol() 64 | return SCIP_OKAY 65 | 66 | cdef SCIP_RETCODE PyHeurExitsol (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 67 | cdef SCIP_HEURDATA* heurdata 68 | heurdata = SCIPheurGetData(heur) 69 | PyHeur = heurdata 70 | PyHeur.heurexitsol() 71 | return SCIP_OKAY 72 | 73 | cdef SCIP_RETCODE PyHeurExec (SCIP* scip, SCIP_HEUR* heur, SCIP_HEURTIMING heurtiming, SCIP_Bool nodeinfeasible, SCIP_RESULT* result) noexcept with gil: 74 | cdef SCIP_HEURDATA* heurdata 75 | heurdata = SCIPheurGetData(heur) 76 | PyHeur = heurdata 77 | result_dict = PyHeur.heurexec(heurtiming, nodeinfeasible) 78 | result[0] = result_dict.get("result", result[0]) 79 | return SCIP_OKAY 80 | -------------------------------------------------------------------------------- /src/pyscipopt/nodesel.pxi: -------------------------------------------------------------------------------- 1 | ##@file nodesel.pxi 2 | #@brief Base class of the Nodesel Plugin 3 | cdef class Nodesel: 4 | cdef public Model model 5 | 6 | def nodefree(self): 7 | '''frees memory of node selector''' 8 | pass 9 | 10 | def nodeinit(self): 11 | ''' executed after the problem is transformed. use this call to initialize node selector data.''' 12 | pass 13 | 14 | def nodeexit(self): 15 | '''executed before the transformed problem is freed''' 16 | pass 17 | 18 | def nodeinitsol(self): 19 | '''executed when the presolving is finished and the branch-and-bound process is about to begin''' 20 | pass 21 | 22 | def nodeexitsol(self): 23 | '''executed before the branch-and-bound process is freed''' 24 | pass 25 | 26 | def nodeselect(self): 27 | '''first method called in each iteration in the main solving loop. ''' 28 | # this method needs to be implemented by the user 29 | return {} 30 | 31 | def nodecomp(self, node1, node2): 32 | ''' 33 | compare two leaves of the current branching tree 34 | 35 | It should return the following values: 36 | 37 | value < 0, if node 1 comes before (is better than) node 2 38 | value = 0, if both nodes are equally good 39 | value > 0, if node 1 comes after (is worse than) node 2. 40 | ''' 41 | # this method needs to be implemented by the user 42 | return 0 43 | 44 | 45 | cdef SCIP_RETCODE PyNodeselCopy (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 46 | return SCIP_OKAY 47 | 48 | cdef SCIP_RETCODE PyNodeselFree (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 49 | cdef SCIP_NODESELDATA* nodeseldata 50 | nodeseldata = SCIPnodeselGetData(nodesel) 51 | PyNodesel = nodeseldata 52 | PyNodesel.nodefree() 53 | Py_DECREF(PyNodesel) 54 | return SCIP_OKAY 55 | 56 | cdef SCIP_RETCODE PyNodeselInit (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 57 | cdef SCIP_NODESELDATA* nodeseldata 58 | nodeseldata = SCIPnodeselGetData(nodesel) 59 | PyNodesel = nodeseldata 60 | PyNodesel.nodeinit() 61 | return SCIP_OKAY 62 | 63 | 64 | cdef SCIP_RETCODE PyNodeselExit (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 65 | cdef SCIP_NODESELDATA* nodeseldata 66 | nodeseldata = SCIPnodeselGetData(nodesel) 67 | PyNodesel = nodeseldata 68 | PyNodesel.nodeexit() 69 | return SCIP_OKAY 70 | 71 | cdef SCIP_RETCODE PyNodeselInitsol (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 72 | cdef SCIP_NODESELDATA* nodeseldata 73 | nodeseldata = SCIPnodeselGetData(nodesel) 74 | PyNodesel = nodeseldata 75 | PyNodesel.nodeinitsol() 76 | return SCIP_OKAY 77 | 78 | cdef SCIP_RETCODE PyNodeselExitsol (SCIP* scip, SCIP_NODESEL* nodesel) noexcept with gil: 79 | cdef SCIP_NODESELDATA* nodeseldata 80 | nodeseldata = SCIPnodeselGetData(nodesel) 81 | PyNodesel = nodeseldata 82 | PyNodesel.nodeexitsol() 83 | return SCIP_OKAY 84 | 85 | cdef SCIP_RETCODE PyNodeselSelect (SCIP* scip, SCIP_NODESEL* nodesel, SCIP_NODE** selnode) noexcept with gil: 86 | cdef SCIP_NODESELDATA* nodeseldata 87 | nodeseldata = SCIPnodeselGetData(nodesel) 88 | PyNodesel = nodeseldata 89 | result_dict = PyNodesel.nodeselect() 90 | selected_node = (result_dict.get("selnode", None)) 91 | selnode[0] = selected_node.scip_node 92 | return SCIP_OKAY 93 | 94 | cdef int PyNodeselComp (SCIP* scip, SCIP_NODESEL* nodesel, SCIP_NODE* node1, SCIP_NODE* node2) noexcept with gil: 95 | cdef SCIP_NODESELDATA* nodeseldata 96 | nodeseldata = SCIPnodeselGetData(nodesel) 97 | PyNodesel = nodeseldata 98 | n1 = Node.create(node1) 99 | n2 = Node.create(node2) 100 | result = PyNodesel.nodecomp(n1, n2) # 101 | return result 102 | -------------------------------------------------------------------------------- /src/pyscipopt/presol.pxi: -------------------------------------------------------------------------------- 1 | ##@file presol.pxi 2 | #@brief Base class of the Presolver Plugin 3 | cdef class Presol: 4 | cdef public Model model 5 | 6 | def presolfree(self): 7 | '''frees memory of presolver''' 8 | pass 9 | 10 | def presolinit(self): 11 | '''initializes presolver''' 12 | pass 13 | 14 | def presolexit(self): 15 | '''deinitializes presolver''' 16 | pass 17 | 18 | def presolinitpre(self): 19 | '''informs presolver that the presolving process is being started''' 20 | pass 21 | 22 | def presolexitpre(self): 23 | '''informs presolver that the presolving process is finished''' 24 | pass 25 | 26 | def presolexec(self, nrounds, presoltiming): 27 | '''executes presolver''' 28 | print("python error in presolexec: this method needs to be implemented") 29 | return {} 30 | 31 | 32 | 33 | cdef SCIP_RETCODE PyPresolCopy (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 34 | return SCIP_OKAY 35 | 36 | cdef SCIP_RETCODE PyPresolFree (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 37 | cdef SCIP_PRESOLDATA* presoldata 38 | presoldata = SCIPpresolGetData(presol) 39 | PyPresol = presoldata 40 | PyPresol.presolfree() 41 | Py_DECREF(PyPresol) 42 | return SCIP_OKAY 43 | 44 | cdef SCIP_RETCODE PyPresolInit (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 45 | cdef SCIP_PRESOLDATA* presoldata 46 | presoldata = SCIPpresolGetData(presol) 47 | PyPresol = presoldata 48 | PyPresol.presolinit() 49 | return SCIP_OKAY 50 | 51 | cdef SCIP_RETCODE PyPresolExit (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 52 | cdef SCIP_PRESOLDATA* presoldata 53 | presoldata = SCIPpresolGetData(presol) 54 | PyPresol = presoldata 55 | PyPresol.presolexit() 56 | return SCIP_OKAY 57 | 58 | 59 | cdef SCIP_RETCODE PyPresolInitpre (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 60 | cdef SCIP_PRESOLDATA* presoldata 61 | presoldata = SCIPpresolGetData(presol) 62 | PyPresol = presoldata 63 | PyPresol.presolinitpre() 64 | return SCIP_OKAY 65 | 66 | cdef SCIP_RETCODE PyPresolExitpre (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 67 | cdef SCIP_PRESOLDATA* presoldata 68 | presoldata = SCIPpresolGetData(presol) 69 | PyPresol = presoldata 70 | PyPresol.presolexitpre() 71 | return SCIP_OKAY 72 | 73 | cdef SCIP_RETCODE PyPresolExec (SCIP* scip, SCIP_PRESOL* presol, int nrounds, SCIP_PRESOLTIMING presoltiming, 74 | int nnewfixedvars, int nnewaggrvars, int nnewchgvartypes, int nnewchgbds, int nnewholes, 75 | int nnewdelconss, int nnewaddconss, int nnewupgdconss, int nnewchgcoefs, int nnewchgsides, 76 | int* nfixedvars, int* naggrvars, int* nchgvartypes, int* nchgbds, int* naddholes, 77 | int* ndelconss, int* naddconss, int* nupgdconss, int* nchgcoefs, int* nchgsides, SCIP_RESULT* result) noexcept with gil: 78 | cdef SCIP_PRESOLDATA* presoldata 79 | presoldata = SCIPpresolGetData(presol) 80 | PyPresol = presoldata 81 | result_dict = PyPresol.presolexec(nrounds, presoltiming) 82 | result[0] = result_dict.get("result", result[0]) 83 | nfixedvars[0] += result_dict.get("nnewfixedvars", 0) 84 | naggrvars[0] += result_dict.get("nnewaggrvars", 0) 85 | nchgvartypes[0] += result_dict.get("nnewchgvartypes", 0) 86 | nchgbds[0] += result_dict.get("nnewchgbds", 0) 87 | naddholes[0] += result_dict.get("nnewaddholes", 0) 88 | ndelconss[0] += result_dict.get("nnewdelconss", 0) 89 | naddconss[0] += result_dict.get("nnewaddconss", 0) 90 | nupgdconss[0] += result_dict.get("nnewupgdconss", 0) 91 | nchgcoefs[0] += result_dict.get("nnewchgcoefs", 0) 92 | nchgsides[0] += result_dict.get("nnewchgsides", 0) 93 | return SCIP_OKAY 94 | -------------------------------------------------------------------------------- /src/pyscipopt/pricer.pxi: -------------------------------------------------------------------------------- 1 | ##@file pricer.pxi 2 | #@brief Base class of the Pricers Plugin 3 | cdef class Pricer: 4 | cdef public Model model 5 | cdef SCIP_PRICER* scip_pricer 6 | 7 | def pricerfree(self): 8 | '''calls destructor and frees memory of variable pricer ''' 9 | pass 10 | 11 | def pricerinit(self): 12 | '''initializes variable pricer''' 13 | pass 14 | 15 | def pricerexit(self): 16 | '''calls exit method of variable pricer''' 17 | pass 18 | 19 | def pricerinitsol(self): 20 | '''informs variable pricer that the branch and bound process is being started ''' 21 | pass 22 | 23 | def pricerexitsol(self): 24 | '''informs variable pricer that the branch and bound process data is being freed''' 25 | pass 26 | 27 | def pricerredcost(self): 28 | '''calls reduced cost pricing method of variable pricer''' 29 | raise NotImplementedError("pricerredcost() is a fundamental callback and should be implemented in the derived class") 30 | 31 | def pricerfarkas(self): 32 | '''calls Farkas pricing method of variable pricer''' 33 | raise NotImplementedError("pricerfarkas() is a fundamental callback and should be implemented in the derived class") 34 | 35 | 36 | 37 | cdef SCIP_RETCODE PyPricerCopy (SCIP* scip, SCIP_PRICER* pricer, SCIP_Bool* valid) noexcept with gil: 38 | return SCIP_OKAY 39 | 40 | cdef SCIP_RETCODE PyPricerFree (SCIP* scip, SCIP_PRICER* pricer) noexcept with gil: 41 | cdef SCIP_PRICERDATA* pricerdata 42 | pricerdata = SCIPpricerGetData(pricer) 43 | PyPricer = pricerdata 44 | PyPricer.pricerfree() 45 | Py_DECREF(PyPricer) 46 | return SCIP_OKAY 47 | 48 | cdef SCIP_RETCODE PyPricerInit (SCIP* scip, SCIP_PRICER* pricer) noexcept with gil: 49 | cdef SCIP_PRICERDATA* pricerdata 50 | pricerdata = SCIPpricerGetData(pricer) 51 | PyPricer = pricerdata 52 | PyPricer.pricerinit() 53 | return SCIP_OKAY 54 | 55 | cdef SCIP_RETCODE PyPricerExit (SCIP* scip, SCIP_PRICER* pricer) noexcept with gil: 56 | cdef SCIP_PRICERDATA* pricerdata 57 | pricerdata = SCIPpricerGetData(pricer) 58 | PyPricer = pricerdata 59 | PyPricer.pricerexit() 60 | return SCIP_OKAY 61 | 62 | cdef SCIP_RETCODE PyPricerInitsol (SCIP* scip, SCIP_PRICER* pricer) noexcept with gil: 63 | cdef SCIP_PRICERDATA* pricerdata 64 | pricerdata = SCIPpricerGetData(pricer) 65 | PyPricer = pricerdata 66 | PyPricer.pricerinitsol() 67 | return SCIP_OKAY 68 | 69 | cdef SCIP_RETCODE PyPricerExitsol (SCIP* scip, SCIP_PRICER* pricer) noexcept with gil: 70 | cdef SCIP_PRICERDATA* pricerdata 71 | pricerdata = SCIPpricerGetData(pricer) 72 | PyPricer = pricerdata 73 | PyPricer.pricerexitsol() 74 | return SCIP_OKAY 75 | 76 | cdef SCIP_RETCODE PyPricerRedcost (SCIP* scip, SCIP_PRICER* pricer, SCIP_Real* lowerbound, SCIP_Bool* stopearly, SCIP_RESULT* result) noexcept with gil: 77 | cdef SCIP_PRICERDATA* pricerdata 78 | pricerdata = SCIPpricerGetData(pricer) 79 | PyPricer = pricerdata 80 | result_dict = PyPricer.pricerredcost() 81 | result[0] = result_dict.get("result", result[0]) 82 | lowerbound[0] = result_dict.get("lowerbound", lowerbound[0]) 83 | stopearly[0] = result_dict.get("stopearly", stopearly[0]) 84 | return SCIP_OKAY 85 | 86 | cdef SCIP_RETCODE PyPricerFarkas (SCIP* scip, SCIP_PRICER* pricer, SCIP_RESULT* result) noexcept with gil: 87 | cdef SCIP_PRICERDATA* pricerdata 88 | pricerdata = SCIPpricerGetData(pricer) 89 | PyPricer = pricerdata 90 | result[0] = PyPricer.pricerfarkas().get("result", result[0]) 91 | return SCIP_OKAY 92 | -------------------------------------------------------------------------------- /src/pyscipopt/reader.pxi: -------------------------------------------------------------------------------- 1 | ##@file reader.pxi 2 | #@brief Base class of the Reader Plugin 3 | cdef class Reader: 4 | cdef public Model model 5 | cdef public str name 6 | 7 | def readerfree(self): 8 | '''calls destructor and frees memory of reader''' 9 | pass 10 | 11 | def readerread(self, filename): 12 | '''calls read method of reader''' 13 | return {} 14 | 15 | def readerwrite(self, file, name, transformed, objsense, objscale, objoffset, binvars, intvars, 16 | implvars, contvars, fixedvars, startnvars, conss, maxnconss, startnconss, genericnames): 17 | '''calls write method of reader''' 18 | return {} 19 | 20 | 21 | cdef SCIP_RETCODE PyReaderCopy (SCIP* scip, SCIP_READER* reader) noexcept with gil: 22 | return SCIP_OKAY 23 | 24 | cdef SCIP_RETCODE PyReaderFree (SCIP* scip, SCIP_READER* reader) noexcept with gil: 25 | cdef SCIP_READERDATA* readerdata 26 | readerdata = SCIPreaderGetData(reader) 27 | PyReader = readerdata 28 | PyReader.readerfree() 29 | Py_DECREF(PyReader) 30 | return SCIP_OKAY 31 | 32 | cdef SCIP_RETCODE PyReaderRead (SCIP* scip, SCIP_READER* reader, const char* filename, SCIP_RESULT* result) noexcept with gil: 33 | cdef SCIP_READERDATA* readerdata 34 | readerdata = SCIPreaderGetData(reader) 35 | PyReader = readerdata 36 | PyFilename = filename.decode('utf-8') 37 | result_dict = PyReader.readerread(PyFilename) 38 | result[0] = result_dict.get("result", result[0]) 39 | return SCIP_OKAY 40 | 41 | cdef SCIP_RETCODE PyReaderWrite (SCIP* scip, SCIP_READER* reader, FILE* file, 42 | const char* name, SCIP_PROBDATA* probdata, SCIP_Bool transformed, 43 | SCIP_OBJSENSE objsense, SCIP_Real objscale, SCIP_Real objoffset, 44 | SCIP_VAR** vars, int nvars, int nbinvars, int nintvars, int nimplvars, int ncontvars, 45 | SCIP_VAR** fixedvars, int nfixedvars, int startnvars, 46 | SCIP_CONS** conss, int nconss, int maxnconss, int startnconss, 47 | SCIP_Bool genericnames, SCIP_RESULT* result) noexcept with gil: 48 | cdef SCIP_READERDATA* readerdata = SCIPreaderGetData(reader) 49 | cdef int fd = fileno(file) 50 | cdef int i 51 | 52 | PyFile = os.fdopen(fd, "w", closefd=False) 53 | PyName = name.decode('utf-8') 54 | PyBinVars = [Variable.create(vars[i]) for i in range(nbinvars)] 55 | PyIntVars = [Variable.create(vars[i]) for i in range(nbinvars, nintvars)] 56 | PyImplVars = [Variable.create(vars[i]) for i in range(nintvars, nimplvars)] 57 | PyContVars = [Variable.create(vars[i]) for i in range(nimplvars, ncontvars)] 58 | PyFixedVars = [Variable.create(fixedvars[i]) for i in range(nfixedvars)] 59 | PyConss = [Constraint.create(conss[i]) for i in range(nconss)] 60 | PyReader = readerdata 61 | result_dict = PyReader.readerwrite(PyFile, PyName, transformed, objsense, objscale, objoffset, 62 | PyBinVars, PyIntVars, PyImplVars, PyContVars, PyFixedVars, startnvars, 63 | PyConss, maxnconss, startnconss, genericnames) 64 | result[0] = result_dict.get("result", result[0]) 65 | 66 | return SCIP_OKAY 67 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes sub-package 2 | 3 | This sub-package provides a set of functions for common usecases for pyscipopt. This sub-package is for all functions 4 | that don't necessarily reflect the core functionality of SCIP, but are useful for working with the solver. The functions 5 | implemented in this sub-package might not be the most efficient way to solve/formulate a problem but would provide a 6 | good starting point. 7 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/37a1ed6da9a7ebb43a643daca2ff94b3e72f82b8/src/pyscipopt/recipes/__init__.py -------------------------------------------------------------------------------- /src/pyscipopt/recipes/infeasibilities.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quicksum 2 | 3 | 4 | def get_infeasible_constraints(orig_model: Model, verbose=False): 5 | """ 6 | Given a model, adds slack variables to all the constraints and minimizes a binary variable that indicates if they're positive. 7 | Positive slack variables correspond to infeasible constraints. 8 | """ 9 | 10 | model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model 11 | 12 | slack = {} 13 | aux = {} 14 | binary = {} 15 | aux_binary = {} 16 | 17 | for c in model.getConss(): 18 | 19 | slack[c.name] = model.addVar(lb=-float("inf"), name="s_"+c.name) 20 | model.addConsCoeff(c, slack[c.name], 1) 21 | binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857. 22 | 23 | # getting the absolute value because of <= and >= constraints 24 | aux[c.name] = model.addVar() 25 | model.addCons(aux[c.name] >= slack[c.name]) 26 | model.addCons(aux[c.name] >= -slack[c.name]) 27 | 28 | # modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation 29 | aux_binary[c.name] = model.addVar(vtype="B") 30 | model.addCons(binary[c.name]+aux_binary[c.name] == 1) 31 | model.addConsSOS1([aux[c.name], aux_binary[c.name]]) 32 | 33 | model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss())) 34 | model.hideOutput() 35 | model.optimize() 36 | 37 | n_infeasibilities_detected = 0 38 | for c in binary: 39 | if model.isGT(model.getVal(binary[c]), 0): 40 | n_infeasibilities_detected += 1 41 | print("Constraint %s is causing an infeasibility." % c) 42 | 43 | if verbose: 44 | if n_infeasibilities_detected > 0: 45 | print("If the constraint names are unhelpful, consider giving them\ 46 | a suitable name when creating the model with model.addCons(..., name=\"the_name_you_want\")") 47 | else: 48 | print("Model is feasible.") 49 | 50 | return n_infeasibilities_detected, aux -------------------------------------------------------------------------------- /src/pyscipopt/recipes/nonlinear.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | 3 | 4 | def set_nonlinear_objective(model: Model, expr, sense="minimize"): 5 | """ 6 | Takes a nonlinear expression and performs an epigraph reformulation. 7 | """ 8 | 9 | assert expr.degree() > 1, "For linear objectives, please use the setObjective method." 10 | new_obj = model.addVar(lb=-float("inf"), obj=1) 11 | if sense == "minimize": 12 | model.addCons(expr <= new_obj) 13 | model.setMinimize() 14 | elif sense == "maximize": 15 | model.addCons(expr >= new_obj) 16 | model.setMaximize() 17 | else: 18 | raise Warning("unrecognized optimization sense: %s" % sense) 19 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/piecewise.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyscipopt import Model, quicksum, Variable, Constraint 4 | 5 | 6 | def add_piecewise_linear_cons(model: Model, X: Variable, Y: Variable, a: List[float], b: List[float]) -> Constraint: 7 | """add constraint of the form y = f(x), where f is a piecewise linear function 8 | 9 | :param model: pyscipopt model to add the constraint to 10 | :param X: x variable 11 | :param Y: y variable 12 | :param a: array with x-coordinates of the points in the piecewise linear relation 13 | :param b: array with y-coordinate of the points in the piecewise linear relation 14 | 15 | Disclaimer: For the moment, can only model 2d piecewise linear functions 16 | Adapted from https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished/piecewise.py 17 | """ 18 | assert len(a) == len(b), "Must have the same number of x and y-coordinates" 19 | 20 | K = len(a) - 1 21 | w, z = {}, {} 22 | for k in range(K): 23 | w[k] = model.addVar(lb=-model.infinity()) 24 | z[k] = model.addVar(vtype="B") 25 | 26 | for k in range(K): 27 | model.addCons(w[k] >= a[k] * z[k]) 28 | model.addCons(w[k] <= a[k + 1] * z[k]) 29 | 30 | model.addCons(quicksum(z[k] for k in range(K)) == 1) 31 | 32 | model.addCons(X == quicksum(w[k] for k in range(K))) 33 | 34 | c = [float(b[k + 1] - b[k]) / (a[k + 1] - a[k]) for k in range(K)] 35 | d = [b[k] - c[k] * a[k] for k in range(K)] 36 | 37 | new_cons = model.addCons(Y == quicksum(d[k] * z[k] + c[k] * w[k] for k in range(K))) 38 | 39 | return new_cons 40 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/primal_dual_evolution.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, Eventhdlr, SCIP_EVENTTYPE, Eventhdlr 2 | 3 | def attach_primal_dual_evolution_eventhdlr(model: Model): 4 | """ 5 | Attaches an event handler to a given SCIP model that collects primal and dual solutions, 6 | along with the solving time when they were found. 7 | The data is saved in model.data["primal_log"] and model.data["dual_log"]. They consist of 8 | a list of tuples, each tuple containing the solving time and the corresponding solution. 9 | 10 | A usage example can be found in examples/finished/plot_primal_dual_evolution.py. The 11 | example takes the information provided by this recipe and uses it to plot the evolution 12 | of the dual and primal bounds over time. 13 | """ 14 | class GapEventhdlr(Eventhdlr): 15 | 16 | def eventinit(self): # we want to collect best primal solutions and best dual solutions 17 | self.model.catchEvent(SCIP_EVENTTYPE.BESTSOLFOUND, self) 18 | self.model.catchEvent(SCIP_EVENTTYPE.LPSOLVED, self) 19 | self.model.catchEvent(SCIP_EVENTTYPE.NODESOLVED, self) 20 | 21 | 22 | def eventexec(self, event): 23 | # if a new best primal solution was found, we save when it was found and also its objective 24 | if event.getType() == SCIP_EVENTTYPE.BESTSOLFOUND: 25 | self.model.data["primal_log"].append([self.model.getSolvingTime(), self.model.getPrimalbound()]) 26 | 27 | if not self.model.data["dual_log"]: 28 | self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) 29 | 30 | if self.model.getObjectiveSense() == "minimize": 31 | if self.model.isGT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): 32 | self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) 33 | else: 34 | if self.model.isLT(self.model.getDualbound(), self.model.data["dual_log"][-1][1]): 35 | self.model.data["dual_log"].append([self.model.getSolvingTime(), self.model.getDualbound()]) 36 | 37 | 38 | if not hasattr(model, "data") or model.data==None: 39 | model.data = {} 40 | 41 | model.data["primal_log"] = [] 42 | model.data["dual_log"] = [] 43 | hdlr = GapEventhdlr() 44 | model.includeEventhdlr(hdlr, "gapEventHandler", "Event handler which collects primal and dual solution evolution") 45 | 46 | return model -------------------------------------------------------------------------------- /src/pyscipopt/relax.pxi: -------------------------------------------------------------------------------- 1 | ##@file relax.pxi 2 | #@brief Base class of the Relaxator Plugin 3 | cdef class Relax: 4 | cdef public Model model 5 | cdef public str name 6 | 7 | def relaxfree(self): 8 | '''calls destructor and frees memory of relaxation handler''' 9 | pass 10 | 11 | def relaxinit(self): 12 | '''initializes relaxation handler''' 13 | pass 14 | 15 | def relaxexit(self): 16 | '''calls exit method of relaxation handler''' 17 | pass 18 | 19 | def relaxinitsol(self): 20 | '''informs relaxaton handler that the branch and bound process is being started''' 21 | pass 22 | 23 | def relaxexitsol(self): 24 | '''informs relaxation handler that the branch and bound process data is being freed''' 25 | pass 26 | 27 | def relaxexec(self): 28 | '''callls execution method of relaxation handler''' 29 | print("python error in relaxexec: this method needs to be implemented") 30 | return{} 31 | 32 | 33 | cdef SCIP_RETCODE PyRelaxCopy (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 34 | return SCIP_OKAY 35 | 36 | cdef SCIP_RETCODE PyRelaxFree (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 37 | cdef SCIP_RELAXDATA* relaxdata 38 | relaxdata = SCIPrelaxGetData(relax) 39 | PyRelax = relaxdata 40 | PyRelax.relaxfree() 41 | Py_DECREF(PyRelax) 42 | return SCIP_OKAY 43 | 44 | cdef SCIP_RETCODE PyRelaxInit (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 45 | cdef SCIP_RELAXDATA* relaxdata 46 | relaxdata = SCIPrelaxGetData(relax) 47 | PyRelax = relaxdata 48 | PyRelax.relaxinit() 49 | return SCIP_OKAY 50 | 51 | cdef SCIP_RETCODE PyRelaxExit (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 52 | cdef SCIP_RELAXDATA* relaxdata 53 | relaxdata = SCIPrelaxGetData(relax) 54 | PyRelax = relaxdata 55 | PyRelax.relaxexit() 56 | return SCIP_OKAY 57 | 58 | cdef SCIP_RETCODE PyRelaxInitsol (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 59 | cdef SCIP_RELAXDATA* relaxdata 60 | relaxdata = SCIPrelaxGetData(relax) 61 | PyRelax = relaxdata 62 | PyRelax.relaxinitsol() 63 | return SCIP_OKAY 64 | 65 | cdef SCIP_RETCODE PyRelaxExitsol (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 66 | cdef SCIP_RELAXDATA* relaxdata 67 | relaxdata = SCIPrelaxGetData(relax) 68 | PyRelax = relaxdata 69 | PyRelax.relaxexitsol() 70 | return SCIP_OKAY 71 | 72 | cdef SCIP_RETCODE PyRelaxExec (SCIP* scip, SCIP_RELAX* relax, SCIP_Real* lowerbound, SCIP_RESULT* result) noexcept with gil: 73 | cdef SCIP_RELAXDATA* relaxdata 74 | relaxdata = SCIPrelaxGetData(relax) 75 | PyRelax = relaxdata 76 | result_dict = PyRelax.relaxexec() 77 | assert isinstance(result_dict, dict), "relaxexec() must return a dictionary." 78 | lowerbound[0] = result_dict.get("lowerbound", lowerbound[0]) 79 | result[0] = result_dict.get("result", result[0]) 80 | return SCIP_OKAY -------------------------------------------------------------------------------- /src/pyscipopt/scip.pyx: -------------------------------------------------------------------------------- 1 | # This redirection to scip.pxi is necessary to allow for code coverage to work 2 | # as it was producing syntax errors when parsing the .pyx file. 3 | include "scip.pxi" -------------------------------------------------------------------------------- /src/pyscipopt/sepa.pxi: -------------------------------------------------------------------------------- 1 | ##@file sepa.pxi 2 | #@brief Base class of the Separator Plugin 3 | cdef class Sepa: 4 | cdef public Model model 5 | cdef public str name 6 | 7 | def sepafree(self): 8 | '''calls destructor and frees memory of separator''' 9 | pass 10 | 11 | def sepainit(self): 12 | '''initializes separator''' 13 | pass 14 | 15 | def sepaexit(self): 16 | '''calls exit method of separator''' 17 | pass 18 | 19 | def sepainitsol(self): 20 | '''informs separator that the branch and bound process is being started''' 21 | pass 22 | 23 | def sepaexitsol(self): 24 | '''informs separator that the branch and bound process data is being freed''' 25 | pass 26 | 27 | def sepaexeclp(self): 28 | '''calls LP separation method of separator''' 29 | return {} 30 | 31 | def sepaexecsol(self, solution): 32 | '''calls primal solution separation method of separator''' 33 | return {} 34 | 35 | 36 | 37 | cdef SCIP_RETCODE PySepaCopy (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 38 | return SCIP_OKAY 39 | 40 | cdef SCIP_RETCODE PySepaFree (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 41 | cdef SCIP_SEPADATA* sepadata 42 | sepadata = SCIPsepaGetData(sepa) 43 | PySepa = sepadata 44 | PySepa.sepafree() 45 | Py_DECREF(PySepa) 46 | return SCIP_OKAY 47 | 48 | cdef SCIP_RETCODE PySepaInit (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 49 | cdef SCIP_SEPADATA* sepadata 50 | sepadata = SCIPsepaGetData(sepa) 51 | PySepa = sepadata 52 | PySepa.sepainit() 53 | return SCIP_OKAY 54 | 55 | cdef SCIP_RETCODE PySepaExit (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 56 | cdef SCIP_SEPADATA* sepadata 57 | sepadata = SCIPsepaGetData(sepa) 58 | PySepa = sepadata 59 | PySepa.sepaexit() 60 | return SCIP_OKAY 61 | 62 | cdef SCIP_RETCODE PySepaInitsol (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 63 | cdef SCIP_SEPADATA* sepadata 64 | sepadata = SCIPsepaGetData(sepa) 65 | PySepa = sepadata 66 | PySepa.sepainitsol() 67 | return SCIP_OKAY 68 | 69 | cdef SCIP_RETCODE PySepaExitsol (SCIP* scip, SCIP_SEPA* sepa) noexcept with gil: 70 | cdef SCIP_SEPADATA* sepadata 71 | sepadata = SCIPsepaGetData(sepa) 72 | PySepa = sepadata 73 | PySepa.sepaexitsol() 74 | return SCIP_OKAY 75 | 76 | cdef SCIP_RETCODE PySepaExeclp (SCIP* scip, SCIP_SEPA* sepa, SCIP_RESULT* result, unsigned int allowlocal, int depth) noexcept with gil: 77 | cdef SCIP_SEPADATA* sepadata 78 | sepadata = SCIPsepaGetData(sepa) 79 | PySepa = sepadata 80 | result_dict = PySepa.sepaexeclp() 81 | result[0] = result_dict.get("result", result[0]) 82 | return SCIP_OKAY 83 | 84 | cdef SCIP_RETCODE PySepaExecsol (SCIP* scip, SCIP_SEPA* sepa, SCIP_SOL* sol, SCIP_RESULT* result, unsigned int allowlocal, int depth) noexcept with gil: 85 | cdef SCIP_SEPADATA* sepadata 86 | sepadata = SCIPsepaGetData(sepa) 87 | solution = Solution.create(scip, sol) 88 | PySepa = sepadata 89 | result_dict = PySepa.sepaexecsol(solution) 90 | result[0] = result_dict.get("result", result[0]) 91 | return SCIP_OKAY 92 | -------------------------------------------------------------------------------- /tests/test_benders.py: -------------------------------------------------------------------------------- 1 | """ 2 | flp-benders.py: model for solving the capacitated facility location problem using Benders' decomposition 3 | 4 | minimize the total (weighted) travel cost from n customers 5 | to some facilities with fixed costs and capacities. 6 | 7 | Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 8 | """ 9 | from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING 10 | import pdb 11 | 12 | def flp(I,J,d,M,f,c): 13 | """flp -- model for the capacitated facility location problem 14 | Parameters: 15 | - I: set of customers 16 | - J: set of facilities 17 | - d[i]: demand for customer i 18 | - M[j]: capacity of facility j 19 | - f[j]: fixed cost for using a facility in point j 20 | - c[i,j]: unit cost of servicing demand point i from facility j 21 | Returns a model, ready to be solved. 22 | """ 23 | 24 | master = Model("flp-master") 25 | subprob = Model("flp-subprob") 26 | 27 | # creating the problem 28 | y = {} 29 | for j in J: 30 | y[j] = master.addVar(vtype="B", name="y(%s)"%j) 31 | 32 | master.setObjective( 33 | quicksum(f[j]*y[j] for j in J), 34 | "minimize") 35 | master.data = y 36 | 37 | # creating the subproblem 38 | x,y = {},{} 39 | for j in J: 40 | y[j] = subprob.addVar(vtype="B", name="y(%s)"%j) 41 | for i in I: 42 | x[i,j] = subprob.addVar(vtype="C", name="x(%s,%s)"%(i,j)) 43 | 44 | for i in I: 45 | subprob.addCons(quicksum(x[i,j] for j in J) == d[i], "Demand(%s)"%i) 46 | 47 | for j in M: 48 | subprob.addCons(quicksum(x[i,j] for i in I) <= M[j]*y[j], "Capacity(%s)"%i) 49 | 50 | for (i,j) in x: 51 | subprob.addCons(x[i,j] <= d[i]*y[j], "Strong(%s,%s)"%(i,j)) 52 | 53 | subprob.setObjective( 54 | quicksum(c[i,j]*x[i,j] for i in I for j in J), 55 | "minimize") 56 | subprob.data = x,y 57 | 58 | return master, subprob 59 | 60 | 61 | def make_data(): 62 | I,d = multidict({1:80, 2:270, 3:250, 4:160, 5:180}) # demand 63 | J,M,f = multidict({1:[500,1000], 2:[500,1000], 3:[500,1000]}) # capacity, fixed costs 64 | c = {(1,1):4, (1,2):6, (1,3):9, # transportation costs 65 | (2,1):5, (2,2):4, (2,3):7, 66 | (3,1):6, (3,2):3, (3,3):4, 67 | (4,1):8, (4,2):5, (4,3):3, 68 | (5,1):10, (5,2):8, (5,3):4, 69 | } 70 | return I,J,d,M,f,c 71 | 72 | 73 | def test_flpbenders(): 74 | ''' 75 | test the Benders' decomposition plugins with the facility location problem. 76 | ''' 77 | I,J,d,M,f,c = make_data() 78 | master, subprob = flp(I,J,d,M,f,c) 79 | # initializing the default Benders' decomposition with the subproblem 80 | master.setPresolve(SCIP_PARAMSETTING.OFF) 81 | master.setBoolParam("misc/allowstrongdualreds", False) 82 | master.setBoolParam("benders/copybenders", False) 83 | master.initBendersDefault(subprob) 84 | 85 | # optimizing the problem using Benders' decomposition 86 | master.optimize() 87 | 88 | # solving the subproblems to get the best solution 89 | master.computeBestSolSubproblems() 90 | 91 | EPS = 1.e-6 92 | y = master.data 93 | facilities = [j for j in y if master.getVal(y[j]) > EPS] 94 | 95 | x, suby = subprob.data 96 | edges = [(i,j) for (i,j) in x if subprob.getVal(x[i,j]) > EPS] 97 | 98 | print("Optimal value:", master.getObjVal()) 99 | print("Facilities at nodes:", facilities) 100 | print("Edges:", edges) 101 | 102 | master.printStatistics() 103 | 104 | # since computeBestSolSubproblems() was called above, we need to free the 105 | # subproblems. This must happen after the solution is extracted, otherwise 106 | # the solution will be lost 107 | master.freeBendersSubproblems() 108 | 109 | assert 5.61e+03 - 10e-6 < master.getObjVal() < 5.61e+03 + 10e-6 110 | -------------------------------------------------------------------------------- /tests/test_branch_mostinfeas.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Branchrule, SCIP_RESULT 2 | from helpers.utils import random_mip_1 3 | 4 | 5 | class MostInfBranchRule(Branchrule): 6 | 7 | def __init__(self, scip): 8 | self.scip = scip 9 | 10 | def branchexeclp(self, allowaddcons): 11 | 12 | # Get the branching candidates. Only consider the number of priority candidates (they are sorted to be first) 13 | # The implicit integer candidates in general shouldn't be branched on. Unless specified by the user 14 | # npriocands and ncands are the same (npriocands are variables that have been designated as priorities) 15 | branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands() 16 | 17 | # Find the variable that is most fractional 18 | best_cand_idx = 0 19 | best_dist = float('inf') 20 | for i in range(npriocands): 21 | if abs(branch_cand_fracs[i] - 0.5) <= best_dist: 22 | best_dist = abs(branch_cand_fracs[i] - 0.5) 23 | best_cand_idx = i 24 | 25 | # Branch on the variable with the largest score 26 | down_child, eq_child, up_child = self.model.branchVarVal( 27 | branch_cands[best_cand_idx], branch_cand_sols[best_cand_idx]) 28 | 29 | return {"result": SCIP_RESULT.BRANCHED} 30 | 31 | 32 | def test_branch_mostinfeas(): 33 | scip = random_mip_1(node_lim=1000, small=True) 34 | most_inf_branch_rule = MostInfBranchRule(scip) 35 | scip.includeBranchrule(most_inf_branch_rule, "python-mostinf", "custom most infeasible branching rule", 36 | priority=10000000, maxdepth=-1, maxbounddist=1) 37 | scip.optimize() 38 | -------------------------------------------------------------------------------- /tests/test_branch_probing_lp.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, Branchrule, SCIP_RESULT, quicksum, SCIP_PARAMSETTING 2 | 3 | 4 | class MyBranching(Branchrule): 5 | 6 | def __init__(self, model, cont): 7 | self.model = model 8 | self.cont = cont 9 | self.count = 0 10 | self.was_called_val = False 11 | self.was_called_int = False 12 | 13 | def branchexeclp(self, allowaddcons): 14 | self.count += 1 15 | if self.count >= 2: 16 | return {"result": SCIP_RESULT.DIDNOTRUN} 17 | assert allowaddcons 18 | 19 | assert not self.model.inRepropagation() 20 | assert not self.model.inProbing() 21 | self.model.startProbing() 22 | assert not self.model.isObjChangedProbing() 23 | self.model.fixVarProbing(self.cont, 2.0) 24 | self.model.constructLP() 25 | self.model.solveProbingLP() 26 | self.model.getLPObjVal() 27 | self.model.endProbing() 28 | 29 | self.integral = self.model.getLPBranchCands()[0][0] 30 | 31 | if self.count == 1: 32 | down, eq, up = self.model.branchVarVal(self.cont, 1.3) 33 | self.model.chgVarLbNode(down, self.cont, -1.5) 34 | self.model.chgVarUbNode(up, self.cont, 3.0) 35 | self.was_called_val = True 36 | down2, eq2, up2 = self.model.branchVar(self.integral) 37 | self.was_called_int = True 38 | self.model.createChild(6, 7) 39 | return {"result": SCIP_RESULT.BRANCHED} 40 | 41 | 42 | def test_branching(): 43 | m = Model() 44 | m.setHeuristics(SCIP_PARAMSETTING.OFF) 45 | m.setIntParam("presolving/maxrounds", 0) 46 | #m.setLongintParam("lp/rootiterlim", 3) 47 | m.setLongintParam("limits/nodes", 1) 48 | 49 | x0 = m.addVar(lb=-2, ub=4) 50 | r1 = m.addVar() 51 | r2 = m.addVar() 52 | y0 = m.addVar(lb=3) 53 | t = m.addVar(lb=None) 54 | l = m.addVar(vtype="I", lb=-9, ub=18) 55 | u = m.addVar(vtype="I", lb=-3, ub=99) 56 | 57 | more_vars = [] 58 | for i in range(1000): 59 | more_vars.append(m.addVar(vtype="I", lb= -12, ub=40)) 60 | m.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) 61 | 62 | for i in range(1000): 63 | more_vars.append(m.addVar(vtype="I", lb= -52, ub=10)) 64 | m.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[405::2])) 65 | 66 | 67 | m.addCons(r1 >= x0) 68 | m.addCons(r2 >= -x0) 69 | m.addCons(y0 == r1 +r2) 70 | #m.addCons(t * l + l * u >= 4) 71 | m.addCons(t + l + 7* u <= 300) 72 | m.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5* more_vars[9]) 73 | m.addCons(more_vars[3] >= l + 2) 74 | m.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) 75 | m.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) 76 | 77 | 78 | m.setObjective(t - quicksum(j*v for j, v in enumerate(more_vars[20:-40]))) 79 | #m.addCons(t >= r1 * (r1 - x0) + r2 * (r2 + x0)) 80 | 81 | my_branchrule = MyBranching(m, x0) 82 | m.includeBranchrule(my_branchrule, "test branch", "test branching and probing and lp functions", 83 | priority=10000000, maxdepth=3, maxbounddist=1) 84 | 85 | m.optimize() 86 | 87 | print("x0", m.getVal(x0)) 88 | print("r1", m.getVal(r1)) 89 | print("r2", m.getVal(r2)) 90 | print("y0", m.getVal(y0)) 91 | print("t", m.getVal(t)) 92 | 93 | assert my_branchrule.was_called_val 94 | assert my_branchrule.was_called_int -------------------------------------------------------------------------------- /tests/test_copy.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | from helpers.utils import random_mip_1 3 | 4 | def test_copy(): 5 | # create solver instance 6 | s = Model() 7 | 8 | # add some variables 9 | x = s.addVar("x", vtype = 'C', obj = 1.0) 10 | y = s.addVar("y", vtype = 'C', obj = 2.0) 11 | s.setObjective(4.0 * y, clear = False) 12 | 13 | c = s.addCons(x + 2 * y >= 1.0) 14 | 15 | s2 = Model(sourceModel=s) 16 | 17 | # solve problems 18 | s.optimize() 19 | s2.optimize() 20 | 21 | assert s.getObjVal() == s2.getObjVal() 22 | 23 | -------------------------------------------------------------------------------- /tests/test_cutsel.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quicksum, SCIP_RESULT, SCIP_PARAMSETTING 2 | from pyscipopt.scip import Cutsel 3 | import itertools 4 | 5 | 6 | class MaxEfficacyCutsel(Cutsel): 7 | 8 | def cutselselect(self, cuts, forcedcuts, root, maxnselectedcuts): 9 | """ 10 | Selects the 10 cuts with largest efficacy. Ensures that all forced cuts are passed along. 11 | Overwrites the base cutselselect of Cutsel. 12 | 13 | :param cuts: the cuts which we want to select from. Is a list of scip Rows 14 | :param forcedcuts: the cuts which we must add. Is a list of scip Rows 15 | :return: sorted cuts and forcedcuts 16 | """ 17 | 18 | scip = self.model 19 | 20 | scores = [0] * len(cuts) 21 | for i in range(len(scores)): 22 | scores[i] = scip.getCutEfficacy(cuts[i]) 23 | 24 | rankings = sorted(range(len(cuts)), key=lambda x: scores[x], reverse=True) 25 | 26 | sorted_cuts = [cuts[rank] for rank in rankings] 27 | 28 | assert len(sorted_cuts) == len(cuts) 29 | 30 | return {'cuts': sorted_cuts, 'nselectedcuts': min(maxnselectedcuts, len(cuts), 10), 31 | 'result': SCIP_RESULT.SUCCESS} 32 | 33 | 34 | def test_cut_selector(): 35 | scip = Model() 36 | scip.setIntParam("presolving/maxrounds", 3) 37 | # scip.setHeuristics(SCIP_PARAMSETTING.OFF) 38 | 39 | cutsel = MaxEfficacyCutsel() 40 | scip.includeCutsel(cutsel, 'max_efficacy', 'maximises efficacy', 5000000) 41 | 42 | # Make a basic minimum spanning hypertree problem 43 | # Let's construct a problem with 15 vertices and 40 hyperedges. The hyperedges are our variables. 44 | v = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] 45 | e = {} 46 | for i in range(40): 47 | e[i] = scip.addVar(vtype='B', name='hyperedge_{}'.format(i)) 48 | 49 | # Construct a dummy incident matrix 50 | A = [[1, 2, 3], [2, 3, 4, 5], [4, 9], [7, 8, 9], [0, 8, 9], 51 | [1, 6, 8], [0, 1, 2, 9], [0, 3, 5, 7, 8], [2, 3], [6, 9], 52 | [5, 8], [1, 9], [2, 7, 8, 9], [3, 8], [2, 4], 53 | [0, 1], [0, 1, 4], [2, 5], [1, 6, 7, 8], [1, 3, 4, 7, 9], 54 | [11, 14], [0, 2, 14], [2, 7, 8, 10], [0, 7, 10, 14], [1, 6, 11], 55 | [5, 8, 12], [3, 4, 14], [0, 12], [4, 8, 12], [4, 7, 9, 11, 14], 56 | [3, 12, 13], [2, 3, 4, 7, 11, 14], [0, 5, 10], [2, 7, 13], [4, 9, 14], 57 | [7, 8, 10], [10, 13], [3, 6, 11], [2, 8, 9, 11], [3, 13]] 58 | 59 | # Create a cost vector for each hyperedge 60 | c = [2.5, 2.9, 3.2, 7, 1.2, 0.5, 61 | 8.6, 9, 6.7, 0.3, 4, 62 | 0.9, 1.8, 6.7, 3, 2.1, 63 | 1.8, 1.9, 0.5, 4.3, 5.6, 64 | 3.8, 4.6, 4.1, 1.8, 2.5, 65 | 3.2, 3.1, 0.5, 1.8, 9.2, 66 | 2.5, 6.4, 2.1, 1.9, 2.7, 67 | 1.6, 0.7, 8.2, 7.9, 3] 68 | 69 | # Add constraint that your hypertree touches all vertices 70 | scip.addCons(quicksum((len(A[i]) - 1) * e[i] for i in range(len(A))) == len(v) - 1) 71 | 72 | # Now add the sub-tour elimination constraints. 73 | for i in range(2, len(v) + 1): 74 | for combination in itertools.combinations(v, i): 75 | scip.addCons(quicksum(max(len(set(combination) & set(A[j])) - 1, 0) * e[j] for j in range(len(A))) <= i - 1, 76 | name='cons_{}'.format(combination)) 77 | 78 | # Add objective to minimise the cost 79 | scip.setObjective(quicksum(c[i] * e[i] for i in range(len(A))), sense='minimize') 80 | 81 | scip.optimize() 82 | -------------------------------------------------------------------------------- /tests/test_knapsack.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quicksum 2 | 3 | def test_knapsack(): 4 | # create solver instance 5 | s = Model("Knapsack") 6 | s.hideOutput() 7 | 8 | # setting the objective sense to maximise 9 | s.setMaximize() 10 | 11 | # item weights 12 | weights = [4, 2, 6, 3, 7, 5] 13 | # item costs 14 | costs = [7, 2, 5, 4, 3, 4] 15 | 16 | assert len(weights) == len(costs) 17 | 18 | # knapsack size 19 | knapsackSize = 15 20 | 21 | # adding the knapsack variables 22 | knapsackVars = [] 23 | varNames = [] 24 | varBaseName = "Item" 25 | for i in range(len(weights)): 26 | varNames.append(varBaseName + "_" + str(i)) 27 | knapsackVars.append(s.addVar(varNames[i], vtype='I', obj=costs[i], ub=1.0)) 28 | 29 | 30 | # adding a linear constraint for the knapsack constraint 31 | s.addCons(quicksum(w*v for (w, v) in zip(weights, knapsackVars)) <= knapsackSize) 32 | 33 | # solve problem 34 | s.optimize() 35 | 36 | s.printStatistics() 37 | 38 | # print solution 39 | varSolutions = [] 40 | for i in range(len(weights)): 41 | solValue = round(s.getVal(knapsackVars[i])) 42 | varSolutions.append(solValue) 43 | if solValue > 0: 44 | print (varNames[i], "Times Selected:", solValue) 45 | print ("\tIncluded Weight:", weights[i]*solValue, "\tItem Cost:", costs[i]*solValue) 46 | 47 | includedWeight = sum([weights[i]*varSolutions[i] for i in range(len(weights))]) 48 | assert includedWeight > 0 and includedWeight <= knapsackSize -------------------------------------------------------------------------------- /tests/test_memory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use 3 | 4 | def test_not_freed(): 5 | if is_optimized_mode(): 6 | pytest.skip() 7 | m = Model() 8 | assert not is_memory_freed() 9 | 10 | def test_freed(): 11 | if is_optimized_mode(): 12 | pytest.skip() 13 | m = Model() 14 | del m 15 | assert is_memory_freed() 16 | 17 | def test_print_memory_in_use(): 18 | print_memory_in_use() 19 | 20 | def is_optimized_mode(): 21 | model = Model() 22 | return is_memory_freed() -------------------------------------------------------------------------------- /tests/test_nlrow.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyscipopt import Model, Heur, SCIP_RESULT, quicksum, SCIP_PARAMSETTING, SCIP_HEURTIMING 4 | 5 | class MyHeur(Heur): 6 | def heurexec(self, heurtiming, nodeinfeasible): 7 | self.model.interruptSolve() 8 | return {"result": SCIP_RESULT.DIDNOTFIND} 9 | 10 | def test_nlrow(): 11 | 12 | # create nonlinear model 13 | m = Model("nlrow") 14 | 15 | # add heuristic to interrupt solve: the methods we wanna test can only be called in solving stage 16 | heuristic = MyHeur() 17 | m.includeHeur(heuristic, "PyHeur", "heur to interrupt", "Y", timingmask=SCIP_HEURTIMING.BEFORENODE) 18 | 19 | # create variables 20 | x = m.addVar(name="x", lb=-3, ub=3, obj=-1) 21 | y = m.addVar(name="y", lb=-3, ub=3, obj=-1) 22 | 23 | # create constraints 24 | m.addCons(1*x + 2*y + 3 * x**2 + 4*y**2 + 5*x*y <= 6) 25 | m.addCons(7*x**2 + 8*y**2 == 9) 26 | m.addCons(10*x + 11*y <= 12) 27 | 28 | # optimize without presolving 29 | m.setPresolve(SCIP_PARAMSETTING.OFF) 30 | m.optimize() 31 | 32 | # check whether NLP has been constructed and there are 3 nonlinear rows that match the above constraints 33 | assert m.isNLPConstructed() 34 | assert m.getNNlRows() == 3 35 | 36 | # collect nonlinear rows 37 | nlrows = m.getNlRows() 38 | 39 | # test getNNlRows 40 | assert len(nlrows) == m.getNNlRows() 41 | 42 | # to test printing of NLRows 43 | for row in nlrows: 44 | m.printNlRow(row) 45 | 46 | # the nlrow that corresponds to the linear (third) constraint is added before the nonlinear rows, 47 | # because Initsol of the linear conshdlr gets called first 48 | # therefore the ordering is: nlrows[0] is for constraint 3, nlrows[1] is for constraint 1, 49 | # nlrows[2] is for constraint 2 50 | 51 | # check first nonlinear row that represents constraint 3 52 | assert nlrows[0].getLhs() == -m.infinity() 53 | assert nlrows[0].getRhs() == 12 54 | 55 | linterms = nlrows[0].getLinearTerms() 56 | assert len(linterms) == 2 57 | assert str(linterms[0][0]) == "t_x" 58 | assert linterms[0][1] == 10 59 | assert str(linterms[1][0]) == "t_y" 60 | assert linterms[1][1] == 11 61 | 62 | linterms = nlrows[1].getLinearTerms() 63 | assert len(linterms) == 2 64 | assert str(linterms[0][0]) == "t_x" 65 | assert linterms[0][1] == 1 66 | assert str(linterms[1][0]) == "t_y" 67 | assert linterms[1][1] == 2 68 | 69 | # check third nonlinear row that represents constraint 2 70 | assert nlrows[2].getLhs() == 9 71 | assert nlrows[2].getRhs() == 9 72 | 73 | linterms = nlrows[2].getLinearTerms() 74 | assert len(linterms) == 0 75 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import SCIP_RESULT, Eventhdlr, SCIP_EVENTTYPE, scip 2 | from helpers.utils import random_mip_1 3 | 4 | class cutoffEventHdlr(Eventhdlr): 5 | def eventinit(self): 6 | self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self) 7 | 8 | def eventexec(self, event): 9 | self.model.cutoffNode(self.model.getCurrentNode()) 10 | return {'result': SCIP_RESULT.SUCCESS} 11 | 12 | def test_cutoffNode(): 13 | m = random_mip_1(disable_heur=True, disable_presolve=True, disable_sepa=True) 14 | 15 | hdlr = cutoffEventHdlr() 16 | 17 | m.includeEventhdlr(hdlr, "test", "test") 18 | 19 | m.optimize() 20 | 21 | assert m.getNSols() == 0 22 | 23 | class focusEventHdlr(Eventhdlr): 24 | def eventinit(self): 25 | self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self) 26 | 27 | def eventexec(self, event): 28 | assert self.model.getNSiblings() in [0,1] 29 | assert len(self.model.getSiblings()) == self.model.getNSiblings() 30 | for node in self.model.getSiblings(): 31 | assert isinstance(node, scip.Node) 32 | 33 | assert self.model.getNLeaves() >= 0 34 | assert len(self.model.getLeaves()) == self.model.getNLeaves() 35 | for node in self.model.getLeaves(): 36 | assert isinstance(node, scip.Node) 37 | 38 | assert self.model.getNChildren() >= 0 39 | assert len(self.model.getChildren()) == self.model.getNChildren() 40 | for node in self.model.getChildren(): 41 | assert isinstance(node, scip.Node) 42 | 43 | leaves, children, siblings = self.model.getOpenNodes() 44 | assert leaves == self.model.getLeaves() 45 | assert children == self.model.getChildren() 46 | assert siblings == self.model.getSiblings() 47 | 48 | return {'result': SCIP_RESULT.SUCCESS} 49 | 50 | def test_tree_methods(): 51 | m = random_mip_1(disable_heur=True, disable_presolve=True, disable_sepa=True) 52 | m.setParam("limits/nodes", 10) 53 | 54 | hdlr = focusEventHdlr() 55 | 56 | m.includeEventhdlr(hdlr, "test", "test") 57 | 58 | m.optimize() 59 | 60 | assert m.getNSols() == 0 -------------------------------------------------------------------------------- /tests/test_nodesel.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, SCIP_PARAMSETTING 2 | from pyscipopt.scip import Nodesel 3 | from helpers.utils import random_mip_1 4 | 5 | class FiFo(Nodesel): 6 | 7 | def nodeselect(self): 8 | """first method called in each iteration in the main solving loop.""" 9 | 10 | leaves, children, siblings = self.model.getOpenNodes() 11 | nodes = leaves + children + siblings 12 | 13 | return {"selnode": nodes[0]} if len(nodes) > 0 else {} 14 | 15 | def nodecomp(self, node1, node2): 16 | """ 17 | compare two leaves of the current branching tree 18 | 19 | It should return the following values: 20 | 21 | value < 0, if node 1 comes before (is better than) node 2 22 | value = 0, if both nodes are equally good 23 | value > 0, if node 1 comes after (is worse than) node 2. 24 | """ 25 | return 0 26 | 27 | 28 | # Depth First Search Node Selector 29 | class DFS(Nodesel): 30 | 31 | def __init__(self, scip, *args, **kwargs): 32 | super().__init__(*args, **kwargs) 33 | self.scip = scip 34 | 35 | def nodeselect(self): 36 | 37 | selnode = self.scip.getPrioChild() 38 | if selnode is None: 39 | selnode = self.scip.getPrioSibling() 40 | if selnode is None: 41 | selnode = self.scip.getBestLeaf() 42 | 43 | return {"selnode": selnode} 44 | 45 | def nodecomp(self, node1, node2): 46 | """ 47 | compare two leaves of the current branching tree 48 | 49 | It should return the following values: 50 | 51 | value < 0, if node 1 comes before (is better than) node 2 52 | value = 0, if both nodes are equally good 53 | value > 0, if node 1 comes after (is worse than) node 2. 54 | """ 55 | depth_1 = node1.getDepth() 56 | depth_2 = node2.getDepth() 57 | if depth_1 > depth_2: 58 | return -1 59 | elif depth_1 < depth_2: 60 | return 1 61 | else: 62 | lb_1 = node1.getLowerbound() 63 | lb_2 = node2.getLowerbound() 64 | if lb_1 < lb_2: 65 | return -1 66 | elif lb_1 > lb_2: 67 | return 1 68 | else: 69 | return 0 70 | 71 | 72 | def test_nodesel_fifo(): 73 | m = Model() 74 | 75 | # include node selector 76 | m.includeNodesel(FiFo(), "testnodeselector", "Testing a node selector.", 1073741823, 536870911) 77 | 78 | # add Variables 79 | x0 = m.addVar(vtype="C", name="x0", obj=-1) 80 | x1 = m.addVar(vtype="C", name="x1", obj=-1) 81 | x2 = m.addVar(vtype="C", name="x2", obj=-1) 82 | 83 | # add constraints 84 | m.addCons(x0 >= 2) 85 | m.addCons(x0 ** 2 <= x1) 86 | m.addCons(x1 * x2 >= x0) 87 | 88 | m.setObjective(x1 + x0) 89 | m.optimize() 90 | 91 | def test_nodesel_dfs(): 92 | m = random_mip_1(node_lim=500) 93 | 94 | # include node selector 95 | dfs_node_sel = DFS(m) 96 | m.includeNodesel(dfs_node_sel, "DFS", "Depth First Search Nodesel.", 1000000, 1000000) 97 | 98 | m.optimize() -------------------------------------------------------------------------------- /tests/test_nogil.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor, as_completed 2 | from pyscipopt import Model 3 | from helpers.utils import random_mip_1 4 | 5 | N_Threads = 4 6 | 7 | 8 | def test_optimalNogil(): 9 | ori_model = random_mip_1(disable_sepa=False, disable_heur=False, disable_presolve=False, node_lim=2000, small=True) 10 | models = [Model(sourceModel=ori_model) for _ in range(N_Threads)] 11 | for i in range(N_Threads): 12 | models[i].setParam("randomization/permutationseed", i) 13 | 14 | ori_model.optimize() 15 | 16 | with ThreadPoolExecutor(max_workers=N_Threads) as executor: 17 | futures = [executor.submit(Model.optimizeNogil, model) for model in models] 18 | for future in as_completed(futures): 19 | pass 20 | for model in models: 21 | assert model.getStatus() == "optimal" 22 | assert abs(ori_model.getObjVal() - model.getObjVal()) < 1e-6 23 | 24 | -------------------------------------------------------------------------------- /tests/test_numerics.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | import pytest 3 | 4 | def test_numerical_checks(): 5 | m = Model() 6 | 7 | m.setParam("numerics/epsilon", 1e-10) 8 | m.setParam("numerics/feastol", 1e-3) 9 | 10 | assert m.isFeasEQ(1, 1.00001) 11 | assert not m.isEQ(1, 1.00001) 12 | 13 | assert m.isFeasLE(1, 0.99999) 14 | assert not m.isLE(1, 0.99999) 15 | 16 | assert m.isFeasGE(1, 1.00001) 17 | assert not m.isGE(1, 1.00001) 18 | 19 | assert not m.isFeasGT(1, 0.99999) 20 | assert m.isGT(1, 0.99999) 21 | -------------------------------------------------------------------------------- /tests/test_quadcons.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | 3 | def test_niceqp(): 4 | s = Model() 5 | 6 | x = s.addVar("x") 7 | y = s.addVar("y") 8 | s.addCons(x >= 2) 9 | s.addCons(x*x <= y) 10 | s.setObjective(y, sense='minimize') 11 | 12 | s.optimize() 13 | 14 | assert round(s.getVal(x)) == 2.0 15 | assert round(s.getVal(y)) == 4.0 16 | 17 | def test_niceqcqp(): 18 | s = Model() 19 | 20 | x = s.addVar("x") 21 | y = s.addVar("y") 22 | s.addCons(x*x + y*y <= 2) 23 | s.setObjective(x + y, sense='maximize') 24 | 25 | s.optimize() 26 | 27 | assert round(s.getVal(x)) == 1.0 28 | assert round(s.getVal(y)) == 1.0 -------------------------------------------------------------------------------- /tests/test_quickprod.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quickprod 2 | from pyscipopt.scip import CONST 3 | from operator import mul 4 | import functools 5 | 6 | def test_quickprod_model(): 7 | m = Model("quickprod") 8 | x = m.addVar("x") 9 | y = m.addVar("y") 10 | z = m.addVar("z") 11 | c = 2.3 12 | 13 | q = quickprod([x,y,z,c]) == 0.0 14 | s = functools.reduce(mul,[x,y,z,c],1) == 0.0 15 | 16 | assert(q.expr.terms == s.expr.terms) 17 | 18 | def test_quickprod(): 19 | empty = quickprod(1 for i in []) 20 | assert len(empty.terms) == 1 21 | assert CONST in empty.terms 22 | 23 | def test_largequadratic(): 24 | # inspired from performance issue on 25 | # http://stackoverflow.com/questions/38434300 26 | 27 | m = Model("dense_quadratic") 28 | dim = 20 29 | x = [m.addVar("x_%d" % i) for i in range(dim)] 30 | expr = quickprod((i+j+1)*x[i]*x[j] 31 | for i in range(dim) 32 | for j in range(dim)) 33 | cons = expr <= 1.0 34 | # upper triangle, diagonal 35 | assert cons.expr.degree() == 2*dim*dim 36 | m.addCons(cons) 37 | # TODO: what can we test beyond the lack of crashes? 38 | -------------------------------------------------------------------------------- /tests/test_quicksum.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quicksum 2 | from pyscipopt.scip import CONST 3 | 4 | def test_quicksum_model(): 5 | m = Model("quicksum") 6 | x = m.addVar("x") 7 | y = m.addVar("y") 8 | z = m.addVar("z") 9 | c = 2.3 10 | 11 | q = quicksum([x,y,z,c]) == 0.0 12 | s = sum([x,y,z,c]) == 0.0 13 | 14 | assert(q.expr.terms == s.expr.terms) 15 | 16 | def test_quicksum(): 17 | empty = quicksum(1 for i in []) 18 | assert len(empty.terms) == 1 19 | assert CONST in empty.terms 20 | 21 | def test_largequadratic(): 22 | # inspired from performance issue on 23 | # http://stackoverflow.com/questions/38434300 24 | 25 | m = Model("dense_quadratic") 26 | dim = 200 27 | x = [m.addVar("x_%d" % i) for i in range(dim)] 28 | expr = quicksum((i+j+1)*x[i]*x[j] 29 | for i in range(dim) 30 | for j in range(dim)) 31 | cons = expr <= 1.0 32 | # upper triangle, diagonal 33 | assert len(cons.expr.terms) == dim * (dim-1) / 2 + dim 34 | m.addCons(cons) 35 | # TODO: what can we test beyond the lack of crashes? 36 | -------------------------------------------------------------------------------- /tests/test_recipe_infeasibilities.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | from pyscipopt.recipes.infeasibilities import get_infeasible_constraints 3 | 4 | 5 | def test_get_infeasible_constraints(): 6 | m = Model() 7 | 8 | x = m.addVar(lb=0) 9 | m.setObjective(2*x) 10 | 11 | m.addCons(x <= 4) 12 | 13 | n_infeasibilities_detected = get_infeasible_constraints(m)[0] 14 | assert n_infeasibilities_detected == 0 15 | 16 | m.addCons(x <= -1) 17 | 18 | n_infeasibilities_detected = get_infeasible_constraints(m)[0] 19 | assert n_infeasibilities_detected == 1 20 | 21 | m.addCons(x == 2) 22 | 23 | n_infeasibilities_detected = get_infeasible_constraints(m)[0] 24 | assert n_infeasibilities_detected == 1 25 | 26 | m.addCons(x == -4) 27 | 28 | n_infeasibilities_detected = get_infeasible_constraints(m)[0] 29 | assert n_infeasibilities_detected == 2 -------------------------------------------------------------------------------- /tests/test_recipe_nonlinear.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, exp, log, sqrt, sin 2 | from pyscipopt.recipes.nonlinear import set_nonlinear_objective 3 | 4 | def test_nonlinear_objective(): 5 | model = Model() 6 | 7 | v = model.addVar() 8 | w = model.addVar() 9 | x = model.addVar() 10 | y = model.addVar() 11 | z = model.addVar() 12 | 13 | obj = 0 14 | obj += exp(v) 15 | obj += log(w) 16 | obj += sqrt(x) 17 | obj += sin(y) 18 | obj += z**3 * y 19 | 20 | model.addCons(v + w + x + y + z <= 1) 21 | set_nonlinear_objective(model, obj, sense='maximize') 22 | 23 | model2 = Model() 24 | 25 | a = model2.addVar() 26 | b = model2.addVar() 27 | c = model2.addVar() 28 | d = model2.addVar() 29 | e = model2.addVar() 30 | 31 | obj2 = 0 32 | obj2 += exp(a) 33 | obj2 += log(b) 34 | obj2 += sqrt(c) 35 | obj2 += sin(d) 36 | obj2 += e**3 * d 37 | 38 | model2.addCons(a + b + c + d + e <= 1) 39 | 40 | t = model2.addVar(lb=-float("inf"),obj=1) 41 | model2.addCons(t <= obj2) 42 | model2.setMaximize() 43 | 44 | obj_expr = model.getObjective() 45 | assert obj_expr.degree() == 1 46 | 47 | model.setParam("numerics/epsilon", 10**(-5)) # bigger eps due to nonlinearities 48 | model2.setParam("numerics/epsilon", 10**(-5)) 49 | 50 | model.optimize() 51 | model2.optimize() 52 | assert model.isEQ(model.getObjVal(), model2.getObjVal()) -------------------------------------------------------------------------------- /tests/test_recipe_piecewise.py: -------------------------------------------------------------------------------- 1 | 2 | from pyscipopt import Model 3 | from pyscipopt.recipes.piecewise import add_piecewise_linear_cons 4 | 5 | def test_add_piecewise_linear_cons(): 6 | m = Model() 7 | 8 | xpoints = [1, 3, 5] 9 | ypoints = [1, 2, 4] 10 | x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) 11 | y = m.addVar(lb=-m.infinity(), obj=-3) 12 | add_piecewise_linear_cons(m, x, y, xpoints, ypoints) 13 | 14 | m.optimize() 15 | assert m.isEQ(m.getObjVal(), -2) 16 | 17 | def test_add_piecewise_linear_cons2(): 18 | m = Model() 19 | 20 | xpoints = [1, 3, 5] 21 | ypoints = [1, 2, 4] 22 | x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) 23 | y = m.addVar(lb=-m.infinity(), obj=-3) 24 | add_piecewise_linear_cons(m, x, y, xpoints, ypoints) 25 | 26 | m.setMaximize() 27 | 28 | m.optimize() 29 | assert m.isEQ(m.getObjVal(), 0) 30 | -------------------------------------------------------------------------------- /tests/test_recipe_primal_dual_evolution.py: -------------------------------------------------------------------------------- 1 | from pyscipopt.recipes.primal_dual_evolution import attach_primal_dual_evolution_eventhdlr 2 | from helpers.utils import bin_packing_model 3 | 4 | def test_primal_dual_evolution(): 5 | from random import randint 6 | 7 | model = bin_packing_model(sizes=[randint(1,40) for _ in range(120)], capacity=50) 8 | model.setParam("limits/time",5) 9 | 10 | model.data = {"test": True} 11 | model = attach_primal_dual_evolution_eventhdlr(model) 12 | 13 | assert "test" in model.data 14 | assert "primal_log" in model.data 15 | 16 | model.optimize() 17 | 18 | for i in range(1, len(model.data["primal_log"])): 19 | if model.getObjectiveSense() == "minimize": 20 | assert model.data["primal_log"][i][1] <= model.data["primal_log"][i-1][1] 21 | else: 22 | assert model.data["primal_log"][i][1] >= model.data["primal_log"][i-1][1] 23 | 24 | for i in range(1, len(model.data["dual_log"])): 25 | if model.getObjectiveSense() == "minimize": 26 | assert model.data["dual_log"][i][1] >= model.data["dual_log"][i-1][1] 27 | else: 28 | assert model.data["dual_log"][i][1] <= model.data["dual_log"][i-1][1] 29 | -------------------------------------------------------------------------------- /tests/test_relax.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, SCIP_RESULT 2 | from pyscipopt.scip import Relax 3 | import pytest 4 | from helpers.utils import random_mip_1 5 | 6 | calls = [] 7 | 8 | 9 | class SoncRelax(Relax): 10 | def relaxexec(self): 11 | calls.append('relaxexec') 12 | return { 13 | 'result': SCIP_RESULT.SUCCESS, 14 | 'lowerbound': 10e4 15 | } 16 | 17 | 18 | def test_relaxator(): 19 | m = Model() 20 | m.hideOutput() 21 | 22 | # include relaxator 23 | m.includeRelax(SoncRelax(), 'testrelaxator', 24 | 'Test that relaxator gets included') 25 | 26 | # add Variables 27 | x0 = m.addVar(vtype="I", name="x0") 28 | x1 = m.addVar(vtype="I", name="x1") 29 | x2 = m.addVar(vtype="I", name="x2") 30 | 31 | # addCons 32 | m.addCons(x0 >= 2) 33 | m.addCons(x0**2 <= x1) 34 | m.addCons(x1 * x2 >= x0) 35 | 36 | m.setObjective(x1 + x0) 37 | m.optimize() 38 | 39 | assert 'relaxexec' in calls 40 | assert len(calls) >= 1 41 | assert m.getObjVal() > 10e4 42 | 43 | class EmptyRelaxator(Relax): 44 | def relaxexec(self): 45 | pass 46 | # doesn't return anything 47 | 48 | def test_empty_relaxator(): 49 | m = Model() 50 | m.hideOutput() 51 | 52 | m.includeRelax(EmptyRelaxator(), "", "") 53 | 54 | x0 = m.addVar(vtype="I", name="x0") 55 | x1 = m.addVar(vtype="I", name="x1") 56 | x2 = m.addVar(vtype="I", name="x2") 57 | 58 | m.addCons(x0 >= 2) 59 | m.addCons(x0**2 <= x1) 60 | m.addCons(x1 * x2 >= x0) 61 | 62 | m.setObjective(x1 + x0) 63 | 64 | with pytest.raises(Exception): 65 | m.optimize() 66 | 67 | def test_relax(): 68 | model = random_mip_1() 69 | 70 | x = model.addVar(vtype="B") 71 | 72 | model.relax() 73 | 74 | assert x.getLbGlobal() == 0 and x.getUbGlobal() == 1 75 | 76 | for var in model.getVars(): 77 | assert var.vtype() == "CONTINUOUS" -------------------------------------------------------------------------------- /tests/test_reopt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyscipopt import Model 3 | 4 | class ReoptimizationTest(unittest.TestCase): 5 | 6 | def test_reopt(self): 7 | 8 | m = Model() 9 | m.enableReoptimization() 10 | 11 | x = m.addVar(name="x", ub=5) 12 | y = m.addVar(name="y", lb=-2, ub=10) 13 | 14 | 15 | m.addCons(2 * x + y >= 8) 16 | m.setObjective(x + y) 17 | m.optimize() 18 | print("x", m.getVal(x)) 19 | print("y", m.getVal(y)) 20 | self.assertEqual(m.getVal(x), 5.0) 21 | self.assertEqual(m.getVal(y), -2.0) 22 | 23 | m.freeReoptSolve() 24 | m.addCons(y <= 3) 25 | m.addCons(y + x <= 6) 26 | m.chgReoptObjective(- x - 2 * y) 27 | 28 | m.optimize() 29 | print("x", m.getVal(x)) 30 | print("y", m.getVal(y)) 31 | self.assertEqual(m.getVal(x), 3.0) 32 | self.assertEqual(m.getVal(y), 3.0) 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /tests/test_row_dual.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, Sepa, SCIP_RESULT, SCIP_PARAMSETTING 2 | 3 | 4 | class SimpleSepa(Sepa): 5 | 6 | def __init__(self, x, y): 7 | self.cut = None 8 | self.x = x 9 | self.y = y 10 | self.has_checked = False 11 | 12 | def sepainit(self): 13 | scip = self.model 14 | self.trans_x = scip.getTransformedVar(self.x) 15 | self.trans_y = scip.getTransformedVar(self.y) 16 | 17 | def sepaexeclp(self): 18 | result = SCIP_RESULT.SEPARATED 19 | scip = self.model 20 | 21 | if self.cut is not None and not self.has_checked: 22 | # rhs * dual should be equal to optimal objective (= -1) 23 | assert scip.isFeasEQ(self.cut.getDualsol(), -1.0) 24 | self.has_checked = True 25 | 26 | cut = scip.createEmptyRowSepa(self, 27 | lhs=-scip.infinity(), 28 | rhs=1.0) 29 | 30 | scip.cacheRowExtensions(cut) 31 | 32 | scip.addVarToRow(cut, self.trans_x, 1.) 33 | scip.addVarToRow(cut, self.trans_y, 1.) 34 | 35 | scip.flushRowExtensions(cut) 36 | 37 | scip.addCut(cut, forcecut=True) 38 | 39 | self.cut = cut 40 | 41 | return {"result": result} 42 | 43 | def sepaexit(self): 44 | assert self.has_checked, "Separator called < 2 times" 45 | 46 | 47 | def model(): 48 | # create solver instance 49 | s = Model() 50 | 51 | # turn off presolve 52 | s.setPresolve(SCIP_PARAMSETTING.OFF) 53 | # turn off heuristics 54 | s.setHeuristics(SCIP_PARAMSETTING.OFF) 55 | # turn off propagation 56 | s.setIntParam("propagating/maxrounds", 0) 57 | s.setIntParam("propagating/maxroundsroot", 0) 58 | 59 | # turn off all other separators 60 | s.setIntParam("separating/strongcg/freq", -1) 61 | s.setIntParam("separating/gomory/freq", -1) 62 | s.setIntParam("separating/aggregation/freq", -1) 63 | s.setIntParam("separating/mcf/freq", -1) 64 | s.setIntParam("separating/closecuts/freq", -1) 65 | s.setIntParam("separating/clique/freq", -1) 66 | s.setIntParam("separating/zerohalf/freq", -1) 67 | s.setIntParam("separating/mixing/freq", -1) 68 | s.setIntParam("separating/rapidlearning/freq", -1) 69 | s.setIntParam("separating/rlt/freq", -1) 70 | 71 | # only two rounds of cuts 72 | # s.setIntParam("separating/maxroundsroot", 10) 73 | 74 | return s 75 | 76 | 77 | def test_row_dual(): 78 | s = model() 79 | # add variable 80 | x = s.addVar("x", vtype='I', obj=-1, lb=0.) 81 | y = s.addVar("y", vtype='I', obj=-1, lb=0.) 82 | 83 | # add constraint 84 | s.addCons(x <= 1.5) 85 | s.addCons(y <= 1.5) 86 | 87 | # include separator 88 | sepa = SimpleSepa(x, y) 89 | s.includeSepa(sepa, "python_simple", "generates a simple cut", 90 | priority=1000, 91 | freq=1) 92 | 93 | s.addCons(x + y <= 1.75) 94 | 95 | # solve problem 96 | s.optimize() 97 | -------------------------------------------------------------------------------- /tests/test_short.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | import pytest 3 | import os 4 | 5 | # This test requires a directory link in tests/ to check/ in the main SCIP directory. 6 | 7 | testset = [] 8 | primalsolutions = {} 9 | dualsolutions = {} 10 | tolerance = 1e-5 11 | infinity = 1e20 12 | 13 | testsetpath = 'check/testset/short.test' 14 | solufilepath = 'check/testset/short.solu' 15 | 16 | if not all(os.path.isfile(fn) for fn in [testsetpath, solufilepath]): 17 | if pytest.__version__ < "3.0.0": 18 | pytest.skip("Files for testset `short` not found (symlink missing?)") 19 | else: 20 | pytestmark = pytest.mark.skip 21 | 22 | else: 23 | with open(testsetpath, 'r') as f: 24 | for line in f.readlines(): 25 | testset.append('check/' + line.rstrip('\n')) 26 | 27 | with open(solufilepath, 'r') as f: 28 | for line in f.readlines(): 29 | 30 | if len(line.split()) == 2: 31 | [s, name] = line.split() 32 | else: 33 | [s, name, value] = line.split() 34 | 35 | if s == '=opt=': 36 | primalsolutions[name] = float(value) 37 | dualsolutions[name] = float(value) 38 | elif s == '=inf=': 39 | primalsolutions[name] = infinity 40 | dualsolutions[name] = infinity 41 | elif s == '=best=': 42 | primalsolutions[name] = float(value) 43 | elif s == '=best dual=': 44 | dualsolutions[name] = float(value) 45 | # status =unkn= needs no data 46 | 47 | def relGE(v1, v2, tol = tolerance): 48 | if v1 is None or v2 is None: 49 | return True 50 | else: 51 | reltol = tol * max(abs(v1), abs(v2), 1.0) 52 | return (v1 - v2) >= -reltol 53 | 54 | def relLE(v1, v2, tol = tolerance): 55 | if v1 is None or v2 is None: 56 | return True 57 | else: 58 | reltol = tol * max(abs(v1), abs(v2), 1.0) 59 | return (v1 - v2) <= reltol 60 | 61 | 62 | @pytest.mark.parametrize('instance', testset) 63 | def test_instance(instance): 64 | s = Model() 65 | s.hideOutput() 66 | s.readProblem(instance) 67 | s.optimize() 68 | name = os.path.split(instance)[1] 69 | if name.rsplit('.',1)[1].lower() == 'gz': 70 | name = name.rsplit('.',2)[0] 71 | else: 72 | name = name.rsplit('.',1)[0] 73 | 74 | # we do not need the solution status 75 | primalbound = s.getObjVal() 76 | dualbound = s.getDualbound() 77 | 78 | # get solution data from solu file 79 | primalsolu = primalsolutions.get(name, None) 80 | dualsolu = dualsolutions.get(name, None) 81 | 82 | if s.getObjectiveSense() == 'minimize': 83 | assert relGE(primalbound, dualsolu) 84 | assert relLE(dualbound, primalsolu) 85 | else: 86 | if( primalsolu == infinity ): primalsolu = -infinity 87 | if( dualsolu == infinity ): dualsolu = -infinity 88 | assert relLE(primalbound, dualsolu) 89 | assert relGE(dualbound, primalsolu) 90 | -------------------------------------------------------------------------------- /tests/test_sub_sol.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the usage of sub solutions found in heuristics with copyLargeNeighborhoodSearch() 3 | """ 4 | import pytest 5 | from pyscipopt import Model, Heur, SCIP_HEURTIMING, SCIP_RESULT 6 | 7 | 8 | class MyHeur(Heur): 9 | def __init__(self, model: Model, fix_vars, fix_vals): 10 | super().__init__() 11 | self.original_model = model 12 | self.used = False 13 | self.fix_vars = fix_vars 14 | self.fix_vals = fix_vals 15 | 16 | def heurexec(self, heurtiming, nodeinfeasible): 17 | self.used = True 18 | # fix z to 2 and optimize the remaining problem 19 | m2 = self.original_model.copyLargeNeighborhoodSearch(self.fix_vars, self.fix_vals) 20 | m2.optimize() 21 | 22 | # translate the solution to the original problem 23 | sub_sol = m2.getBestSol() 24 | sol_translation = self.original_model.translateSubSol(m2, sub_sol, self) 25 | 26 | accepted = self.original_model.trySol(sol_translation) 27 | assert accepted 28 | m2.freeProb() 29 | return {"result": SCIP_RESULT.FOUNDSOL} 30 | 31 | 32 | def test_sub_sol(): 33 | m = Model("sub_sol_test") 34 | x = m.addVar(name="x", lb=0, ub=3, obj=1) 35 | y = m.addVar(name="y", lb=0, ub=3, obj=2) 36 | z = m.addVar(name="z", lb=0, ub=3, obj=3) 37 | 38 | m.addCons(4 <= x + y + z) 39 | 40 | # include the heuristic 41 | my_heur = MyHeur(m, fix_vars= [z], fix_vals = [2]) 42 | m.includeHeur(my_heur, "name", "description", "Y", timingmask=SCIP_HEURTIMING.BEFOREPRESOL, usessubscip=True) 43 | 44 | #optimize 45 | m.optimize() 46 | # assert the heuristic did run 47 | assert my_heur.used 48 | 49 | heur_sol = [2, 0, 2] 50 | opt_sol = [3, 1, 0] 51 | 52 | found_solutions = [] 53 | for sol in m.getSols(): 54 | found_solutions.append([sol[x], sol[y], sol[z]]) 55 | 56 | # both the sub_solution and the real optimum should be in the solution pool 57 | assert heur_sol in found_solutions 58 | assert opt_sol in found_solutions 59 | -------------------------------------------------------------------------------- /tests/test_tree.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyscipopt import Model, Eventhdlr, SCIP_RESULT, SCIP_EVENTTYPE, SCIP_PARAMSETTING 4 | 5 | 6 | class NodeEventHandler(Eventhdlr): 7 | 8 | def __init__(self): 9 | self.calls = [] 10 | 11 | def eventinit(self): 12 | self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self) 13 | 14 | def eventexit(self): 15 | self.model.dropEvent(SCIP_EVENTTYPE.NODEFOCUSED, self) 16 | 17 | def eventexec(self, event): 18 | self.calls.append('eventexec') 19 | assert event.getType() == SCIP_EVENTTYPE.NODEFOCUSED 20 | node = event.getNode() 21 | 22 | if node.getDepth() == 0: 23 | assert node.getParent() is None 24 | assert node.getParentBranchings() is None 25 | return 26 | 27 | variables, branchbounds, boundtypes = node.getParentBranchings() 28 | assert len(variables) == 1 29 | assert len(branchbounds) == 1 30 | assert len(boundtypes) == 1 31 | domain_changes = node.getDomchg() 32 | bound_changes = domain_changes.getBoundchgs() 33 | assert len(bound_changes) == 1 34 | 35 | 36 | def test_tree(): 37 | # create solver instance 38 | s = Model() 39 | s.setMaximize() 40 | s.hideOutput() 41 | s.setPresolve(SCIP_PARAMSETTING.OFF) 42 | node_eventhdlr = NodeEventHandler() 43 | s.includeEventhdlr(node_eventhdlr, "NodeEventHandler", "python event handler to catch NODEFOCUSED") 44 | 45 | # add some variables 46 | n = 121 47 | x = [s.addVar("x{}".format(i), obj=1.0, vtype="INTEGER") for i in range(n)] 48 | 49 | # add some constraints 50 | for i in range(n): 51 | for j in range(i): 52 | dist = min(abs(i - j), abs(n - i - j)) 53 | if dist in (1, 3, 4): 54 | s.addCons(x[i] + x[j] <= 1) 55 | # solve problem 56 | s.optimize() 57 | 58 | # print solution 59 | assert round(s.getObjVal()) == 36.0 60 | 61 | del s 62 | 63 | assert len(node_eventhdlr.calls) > 3 -------------------------------------------------------------------------------- /tests/test_vars.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | 3 | def test_variablebounds(): 4 | 5 | m = Model() 6 | 7 | x0 = m.addVar(lb=-5, ub=8) 8 | r1 = m.addVar() 9 | r2 = m.addVar() 10 | y0 = m.addVar(lb=3) 11 | t = m.addVar(lb=None) 12 | z = m.addVar() 13 | 14 | m.chgVarLbGlobal(x0, -2) 15 | m.chgVarUbGlobal(x0, 4) 16 | 17 | infeas, tightened = m.tightenVarLb(x0, -5) 18 | assert not infeas 19 | assert not tightened 20 | infeas, tightened = m.tightenVarLbGlobal(x0, -1) 21 | assert not infeas 22 | assert tightened 23 | infeas, tightened = m.tightenVarUb(x0, 3) 24 | assert not infeas 25 | assert tightened 26 | infeas, tightened = m.tightenVarUbGlobal(x0, 9) 27 | assert not infeas 28 | assert not tightened 29 | infeas, fixed = m.fixVar(z, 7) 30 | assert not infeas 31 | assert fixed 32 | assert m.delVar(z) 33 | 34 | m.addCons(r1 >= x0) 35 | m.addCons(r2 >= -x0) 36 | m.addCons(y0 == r1 +r2) 37 | 38 | m.setObjective(t) 39 | m.addCons(t >= r1 * (r1 - x0) + r2 * (r2 + x0)) 40 | 41 | 42 | m.optimize() 43 | 44 | print("x0", m.getVal(x0)) 45 | print("r1", m.getVal(r1)) 46 | print("r2", m.getVal(r2)) 47 | print("y0", m.getVal(y0)) 48 | print("t", m.getVal(t)) 49 | 50 | def test_vtype(): 51 | m = Model() 52 | 53 | x = m.addVar(vtype= 'C', lb=-5.5, ub=8) 54 | y = m.addVar(vtype= 'I', lb=-5.2, ub=8) 55 | z = m.addVar(vtype= 'B', lb=-5.2, ub=8) 56 | w = m.addVar(vtype= 'M', lb=-5.2, ub=8) 57 | 58 | assert x.vtype() == "CONTINUOUS" 59 | assert y.vtype() == "INTEGER" 60 | assert z.vtype() == "BINARY" 61 | assert w.vtype() == "IMPLINT" 62 | 63 | m.chgVarType(x, 'I') 64 | assert x.vtype() == "INTEGER" 65 | 66 | m.chgVarType(y, 'M') 67 | assert y.vtype() == "IMPLINT" --------------------------------------------------------------------------------