├── setup.cfg ├── src └── pyscipopt │ ├── py.typed │ ├── recipes │ ├── __init__.py │ ├── README.md │ ├── nonlinear.py │ ├── piecewise.py │ ├── getLocalConss.py │ ├── infeasibilities.py │ └── primal_dual_evolution.py │ ├── _version.py │ ├── scip.pyx │ ├── Multidict.py │ ├── iisfinder.pxi │ ├── heuristic.pxi │ ├── relax.pxi │ ├── benderscut.pxi │ ├── sepa.pxi │ ├── event.pxi │ ├── __init__.py │ ├── reader.pxi │ ├── pricer.pxi │ ├── cutsel.pxi │ ├── nodesel.pxi │ ├── presol.pxi │ └── matrix.pxi ├── examples ├── finished │ ├── __init__.py │ ├── ssp.py │ ├── mkp.py │ ├── transp_nofn.py │ ├── markowitz_soco.py │ ├── prodmix_soco.py │ ├── sudoku.py │ ├── plot_primal_dual_evolution.py │ ├── lo_wines.py │ ├── logical.py │ ├── categorical_data.py │ ├── eoq_en.py │ ├── even.py │ ├── transp.py │ ├── gcp_fixed_k.py │ ├── ssa.py │ ├── flp.py │ ├── pfs.py │ └── kmedian.py ├── tutorial │ ├── puzzle.py │ └── logical.py └── unfinished │ ├── pareto_front.py │ ├── eoq_soco.py │ ├── portfolio_soco.py │ ├── kcenter.py │ └── vrp.py ├── requirements ├── coverage.txt └── test.txt ├── docs ├── api │ ├── row.rst │ ├── model.rst │ ├── node.rst │ ├── column.rst │ ├── event.rst │ ├── variable.rst │ └── constraint.rst ├── requirements.txt ├── _static │ ├── skippy_logo_blue.png │ └── scip_structure_landscape-compressed.png ├── tutorials │ ├── scipdex.rst │ ├── index.rst │ ├── eventhandler.rst │ ├── readwrite.rst │ └── nodeselector.rst ├── index.rst ├── Makefile ├── contributors.rst ├── api.rst ├── faq.rst ├── install.rst └── conf.py ├── .coveragerc ├── .landscape.yml ├── .readthedocs.yaml ├── tests ├── test_statistics.py ├── test_numerics.py ├── test_memory.py ├── test_copy.py ├── test_quadcons.py ├── test_recipe_infeasibilities.py ├── test_recipe_piecewise.py ├── test_nogil.py ├── test_reopt.py ├── test_quicksum.py ├── test_quickprod.py ├── test_recipe_primal_dual_evolution.py ├── test_recipe_nonlinear.py ├── test_knapsack.py ├── test_branch_mostinfeas.py ├── test_recipe_getLocalConss.py ├── test_tree.py ├── test_sub_sol.py ├── test_node.py ├── test_relax.py ├── test_nlrow.py ├── test_iis.py ├── test_row_dual.py ├── test_short.py ├── test_nodesel.py ├── test_cutsel.py ├── test_branch_probing_lp.py └── test_benders.py ├── .github ├── workflows │ ├── generate_docs.yml │ ├── test-release.yml │ ├── coverage.yml │ └── build_wheels.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── CITATION.bib ├── LICENSE ├── RELEASE.md ├── .gitignore ├── generate-docs.sh └── CODE_OF_CONDUCT.md /setup.cfg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pyscipopt/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/finished/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pyscipopt/_version.py: -------------------------------------------------------------------------------- 1 | __version__: str = '6.0.0' 2 | -------------------------------------------------------------------------------- /requirements/coverage.txt: -------------------------------------------------------------------------------- 1 | -r ./test.txt 2 | coverage>=7 3 | cython>=3 4 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | pytest>=8 2 | pytest-xdist>=3 3 | networkx>=3 # only needed for a few tests 4 | -------------------------------------------------------------------------------- /docs/api/row.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Row API 3 | ####### 4 | 5 | .. autoclass:: pyscipopt.scip.Row 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/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-jquery 3 | sphinxcontrib-bibtex 4 | sphinx-book-theme 5 | pyscipopt -------------------------------------------------------------------------------- /docs/_static/skippy_logo_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/HEAD/docs/_static/skippy_logo_blue.png -------------------------------------------------------------------------------- /docs/api/column.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Column API 3 | ########## 4 | 5 | .. autoclass:: pyscipopt.scip.Column 6 | :members: -------------------------------------------------------------------------------- /docs/api/event.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Event API 3 | ########## 4 | 5 | .. autoclass:: pyscipopt.scip.Event 6 | :members: -------------------------------------------------------------------------------- /docs/api/variable.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Variable API 3 | ############ 4 | 5 | .. autoclass:: pyscipopt.Variable 6 | :members: -------------------------------------------------------------------------------- /docs/api/constraint.rst: -------------------------------------------------------------------------------- 1 | ############## 2 | Constraint API 3 | ############## 4 | 5 | .. autoclass:: pyscipopt.Constraint 6 | :members: -------------------------------------------------------------------------------- /docs/_static/scip_structure_landscape-compressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scipopt/PySCIPOpt/HEAD/docs/_static/scip_structure_landscape-compressed.png -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | plugins = Cython.Coverage 3 | source = src/pyscipopt 4 | omit = 5 | tests 6 | __init__.py 7 | scip.pyx 8 | patch = subprocess 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/test_statistics.py: -------------------------------------------------------------------------------- 1 | import os 2 | from helpers.utils import random_mip_1 3 | from json import load 4 | 5 | def test_statistics_json(): 6 | model = random_mip_1() 7 | model.optimize() 8 | model.writeStatisticsJson("statistics.json") 9 | 10 | with open("statistics.json", "r") as f: 11 | data = load(f) 12 | assert data["origprob"]["problem_name"] == "model" 13 | 14 | os.remove("statistics.json") 15 | -------------------------------------------------------------------------------- /.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" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | iis 28 | scipdex -------------------------------------------------------------------------------- /src/pyscipopt/recipes/nonlinear.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model 2 | 3 | def set_nonlinear_objective(model: Model, expr, sense: str = "minimize"): 4 | """ 5 | Takes a nonlinear expression and performs an epigraph reformulation. 6 | """ 7 | 8 | assert expr.degree() > 1, "For linear objectives, please use the setObjective method." 9 | new_obj = model.addVar(lb=-float("inf"), obj=1) 10 | if sense == "minimize": 11 | model.addCons(expr <= new_obj) 12 | model.setMinimize() 13 | elif sense == "maximize": 14 | model.addCons(expr >= new_obj) 15 | model.setMaximize() 16 | else: 17 | raise Warning("unrecognized optimization sense: %s" % sense) 18 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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_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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zuse Institute Berlin (ZIB) 4 | 5 | Permission is hereby granted, free of charge, to any person btaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 10 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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()) -------------------------------------------------------------------------------- /src/pyscipopt/iisfinder.pxi: -------------------------------------------------------------------------------- 1 | ##@file iisfinder.pxi 2 | #@brief Base class of the IIS finder Plugin 3 | cdef class IISfinder: 4 | cdef public IIS iis 5 | cdef SCIP_IIS* scip_iis 6 | cdef SCIP_IISFINDER* scip_iisfinder 7 | 8 | def iisfinderfree(self): 9 | '''calls destructor and frees memory of iis finder''' 10 | pass 11 | 12 | def iisfinderexec(self): 13 | '''calls execution method of iis finder''' 14 | raise NotImplementedError("iisfinderexec() is a fundamental callback and should be implemented in the derived class") 15 | 16 | 17 | cdef SCIP_RETCODE PyiisfinderCopy (SCIP* scip, SCIP_IISFINDER* iisfinder) noexcept with gil: 18 | return SCIP_OKAY 19 | 20 | cdef SCIP_RETCODE PyiisfinderFree (SCIP* scip, SCIP_IISFINDER* iisfinder) noexcept with gil: 21 | cdef SCIP_IISFINDERDATA* iisfinderdata 22 | iisfinderdata = SCIPiisfinderGetData(iisfinder) 23 | PyIIS = iisfinderdata 24 | PyIIS.iisfinderfree() 25 | Py_DECREF(PyIIS) 26 | return SCIP_OKAY 27 | 28 | cdef SCIP_RETCODE PyiisfinderExec (SCIP_IIS* iis, SCIP_IISFINDER* iisfinder, SCIP_RESULT* result) noexcept with gil: 29 | cdef SCIP_IISFINDERDATA* iisfinderdata 30 | iisfinderdata = SCIPiisfinderGetData(iisfinder) 31 | PyIIS = iisfinderdata 32 | 33 | PyIIS.iis._iis = iis 34 | result_dict = PyIIS.iisfinderexec() 35 | assert isinstance(result_dict, dict), "iisfinderexec() must return a dictionary." 36 | return SCIP_OKAY -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/test-release.yml: -------------------------------------------------------------------------------- 1 | name: TestPyPI release 2 | 3 | env: 4 | version: 10.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/v${version}/scipoptsuite_${version}-1+jammy_amd64.deb" 21 | sudo apt-get update && sudo apt install -y ./scipoptsuite_${version}-1+jammy_amd64.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 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_recipe_getLocalConss.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, SCIP_EVENTTYPE 2 | from pyscipopt.recipes.getLocalConss import * 3 | from helpers.utils import random_mip_1 4 | 5 | def localconss(model, event): 6 | local_conss = getLocalConss(model) 7 | assert len(local_conss[1]) == getNLocalConss(model)[1] 8 | assert len(local_conss[0]) == len(model.getConss()) 9 | assert local_conss[0] == model.getConss() 10 | 11 | vars = model.getVars() 12 | if model.getCurrentNode().getNumber() == 1: 13 | pass 14 | 15 | elif model.getCurrentNode().getNumber() == 2: 16 | model.data["local_cons1"] = model.addCons(vars[0] + vars[1] <= 1, name="c1", local=True) 17 | assert getNLocalConss(model)[1] == 1 18 | assert getLocalConss(model)[1][0] == model.data["local_cons1"] 19 | 20 | elif model.getCurrentNode().getNumber() == 4: 21 | local_conss = getLocalConss(model) 22 | model.data["local_cons2"] = model.addCons(vars[1] + vars[2] <= 1, name="c2", local=True) 23 | model.data["local_cons3"] = model.addCons(vars[2] + vars[3] <= 1, name="c3", local=True) 24 | assert getNLocalConss(model)[1] == 3 25 | assert getLocalConss(model)[1][0] == model.data["local_cons1"] 26 | assert getLocalConss(model)[1][1] == model.data["local_cons2"] 27 | assert getLocalConss(model)[1][2] == model.data["local_cons3"] 28 | 29 | elif model.getCurrentNode().getParent().getNumber() not in [2,4]: 30 | assert getLocalConss(model) == [model.getConss(), []] 31 | 32 | def test_getLocalConss(): 33 | model = random_mip_1(node_lim=4) 34 | model.data = {} 35 | 36 | model.attachEventHandlerCallback(localconss, [SCIP_EVENTTYPE.NODEFOCUSED]) 37 | model.optimize() 38 | assert len(model.data) == 3 -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/getLocalConss.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, Constraint 2 | from typing import List 3 | 4 | def getLocalConss(model: Model, node = None) -> List[List[Constraint]]: 5 | """ 6 | Returns local constraints. 7 | 8 | Parameters 9 | ---------- 10 | model : Model 11 | The model from which to retrieve the local constraints. 12 | node : Node, optional 13 | The node from which to retrieve the local constraints. If not provided, the current node is used. 14 | 15 | Returns 16 | ------- 17 | list[Constraint] 18 | A list of local constraints. First entry are global constraints, second entry are all the added constraints. 19 | """ 20 | 21 | if node is None: 22 | assert model.getStageName() in ["INITPRESOLVE", "PRESOLVING", "EXITPRESOLVE", "SOLVING"], "Model cannot be called in stage %s." % model.getStageName() 23 | cur_node = model.getCurrentNode() 24 | else: 25 | cur_node = node 26 | 27 | added_conss = [] 28 | while cur_node is not None: 29 | added_conss = cur_node.getAddedConss() + added_conss 30 | cur_node = cur_node.getParent() 31 | 32 | return [model.getConss(), added_conss] 33 | 34 | def getNLocalConss(model: Model, node = None) -> List[int]: 35 | """ 36 | Returns the number of local constraints of a node. 37 | 38 | Parameters 39 | ---------- 40 | model : Model 41 | The model from which to retrieve the number of local constraints. 42 | node : Node, optional 43 | The node from which to retrieve the number of local constraints. If not provided, the current node is used. 44 | 45 | Returns 46 | ------- 47 | list[int] 48 | A list of the number of local constraints. First entry is the number of global constraints, second entry is the number of all the added constraints. 49 | """ 50 | local_conss = getLocalConss(model, node) 51 | return [len(local_conss[0]), len(local_conss[1])] -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Run tests with coverage 2 | env: 3 | version: 10.0.0 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/v${{ env.version }}/scipoptsuite_${{ env.version }}-1+jammy_amd64.deb" 29 | sudo apt-get update && sudo apt install -y ./scipoptsuite_${{ env.version }}-1+jammy_amd64.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 -r requirements/coverage.txt 40 | 41 | - name: Install PySCIPOpt 42 | run: | 43 | export CFLAGS="-O0 -ggdb -Wall -Wextra -Werror -Wno-error=deprecated-declarations" # Debug mode. More warnings. Warnings as errors, but allow deprecated declarations. 44 | python -m pip install . -v 2>&1 | tee build.log 45 | grep -i "warning" build.log || true 46 | 47 | - name: Run pyscipopt tests 48 | run: | 49 | sudo apt-get install tzdata locales -y && sudo locale-gen pt_PT && sudo update-locale # add pt_PT locale that is used in tests 50 | coverage run -m pytest -nauto 51 | coverage combine 52 | coverage report -m 53 | coverage xml 54 | 55 | - name: Upload to codecov.io 56 | uses: codecov/codecov-action@v2 57 | with: 58 | fail_ci_if_error: false 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_relax.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, SCIP_RESULT, SCIP_PARAMSETTING 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.setPresolve(SCIP_PARAMSETTING.OFF) 21 | m.setHeuristics(SCIP_PARAMSETTING.OFF) 22 | m.setSeparating(SCIP_PARAMSETTING.OFF) 23 | m.setParam("limits/nodes", 1) 24 | m.hideOutput() 25 | 26 | # include relaxator 27 | m.includeRelax(SoncRelax(), 'testrelaxator', 28 | 'Test that relaxator gets included') 29 | 30 | # add Variables 31 | x0 = m.addVar(vtype="I", name="x0") 32 | x1 = m.addVar(vtype="I", name="x1") 33 | x2 = m.addVar(vtype="I", name="x2") 34 | 35 | # addCons 36 | m.addCons(x0 >= 2) 37 | m.addCons(x0**2 <= x1) 38 | m.addCons(x1 * x2 >= x0) 39 | 40 | m.setObjective(x1 + x0) 41 | m.optimize() 42 | 43 | assert 'relaxexec' in calls 44 | assert len(calls) >= 1 45 | assert m.isGE(m.getDualbound(), 10e4) 46 | 47 | class EmptyRelaxator(Relax): 48 | pass 49 | 50 | def test_empty_relaxator(): 51 | m = Model() 52 | m.setPresolve(SCIP_PARAMSETTING.OFF) 53 | m.hideOutput() 54 | 55 | m.includeRelax(EmptyRelaxator(), "", "") 56 | 57 | x0 = m.addVar(vtype="I", name="x0") 58 | x1 = m.addVar(vtype="I", name="x1") 59 | x2 = m.addVar(vtype="I", name="x2") 60 | 61 | m.addCons(x0 >= 2) 62 | m.addCons(x0**2 <= x1) 63 | m.addCons(x1 * x2 >= x0) 64 | 65 | m.setObjective(x1 + x0) 66 | 67 | with pytest.raises(Exception): 68 | with pytest.raises(AssertionError): 69 | m.optimize() 70 | 71 | def test_relax(): 72 | model = random_mip_1() 73 | 74 | x = model.addVar(vtype="B") 75 | 76 | model.relax() 77 | 78 | assert x.getLbGlobal() == 0 and x.getUbGlobal() == 1 79 | 80 | for var in model.getVars(): 81 | assert var.vtype() == "CONTINUOUS" -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/pyscipopt/recipes/infeasibilities.py: -------------------------------------------------------------------------------- 1 | from pyscipopt import Model, quicksum 2 | 3 | 4 | def get_infeasible_constraints(orig_model: Model, verbose: bool = 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.isInfinity(model.getVal(binary[c])) or 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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /tests/test_iis.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyscipopt import Model, SCIP_RESULT, IISfinder 4 | 5 | def infeasible_model(): 6 | m = Model() 7 | x1 = m.addVar("x1", vtype="B") 8 | x2 = m.addVar("x2", vtype="B") 9 | x3 = m.addVar("x3", vtype="B") 10 | 11 | m.addCons(x1 + x2 == 1, name="c1") 12 | m.addCons(x2 + x3 == 1, name="c2") 13 | m.addCons(x1 + x3 == 1, name="c3") 14 | m.addCons(x1 + x2 + x3 <= 0, name="c4") 15 | 16 | return m 17 | 18 | def test_generate_iis(): 19 | m = infeasible_model() 20 | 21 | m.optimize() 22 | 23 | # make sure IIS generation doesn't raise any exceptions 24 | iis = m.generateIIS() 25 | subscip = iis.getSubscip() 26 | assert iis.isSubscipIrreducible() 27 | assert subscip.getNConss() == 2 28 | assert iis.getNNodes() == 0 29 | assert m.isGE(iis.getTime(), 0) 30 | 31 | class myIIS(IISfinder): 32 | def __init__(self, skip=False): 33 | self.skip = skip 34 | self.called = False 35 | 36 | def iisfinderexec(self): 37 | self.called = True 38 | if self.skip: 39 | self.iis.setSubscipInfeasible(True) 40 | self.iis.setSubscipIrreducible(False) 41 | return {"result": SCIP_RESULT.SUCCESS} # success to attempt to skip further processing 42 | 43 | subscip = self.iis.getSubscip() 44 | for c in subscip.getConss(): 45 | if c.name in ["c2", "c4"]: 46 | subscip.delCons(c) 47 | 48 | self.iis.setSubscipInfeasible(True) 49 | self.iis.setSubscipIrreducible(True) 50 | return {"result": SCIP_RESULT.SUCCESS} 51 | 52 | def test_custom_iis_finder(): 53 | 54 | m = infeasible_model() 55 | my_iis = myIIS() 56 | 57 | m.setParam("iis/irreducible", False) 58 | m.setParam("iis/greedy/priority", -1000000) # lowering priority of greedy iis finder 59 | m.includeIISfinder(my_iis, "", "") 60 | 61 | m.generateIIS() 62 | assert my_iis.called 63 | 64 | iis = m.getIIS() 65 | assert iis.isSubscipIrreducible() 66 | assert iis.isSubscipInfeasible() 67 | subscip = iis.getSubscip() 68 | assert subscip.getNConss() == 2 69 | 70 | def test_iisGreddyMakeIrreducible(): 71 | m = infeasible_model() 72 | m.setParam("iis/irreducible", False) 73 | m.setParam("iis/greedy/priority", 1) # lowering priority of greedy iis finder 74 | 75 | my_iis = myIIS(skip=True) 76 | m.includeIISfinder(my_iis, "", "", priority=10000) 77 | 78 | iis = m.generateIIS() 79 | with pytest.raises(AssertionError): 80 | assert not iis.isSubscipIrreducible() # this should not fail 81 | 82 | assert iis.isSubscipInfeasible() 83 | 84 | iis.greedyMakeIrreducible() 85 | assert iis.isSubscipIrreducible() 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | raise NotImplementedError("heurexec() is a fundamental callback and should be implemented in the derived class") 30 | 31 | 32 | 33 | cdef SCIP_RETCODE PyHeurCopy (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 34 | return SCIP_OKAY 35 | 36 | cdef SCIP_RETCODE PyHeurFree (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 37 | cdef SCIP_HEURDATA* heurdata 38 | heurdata = SCIPheurGetData(heur) 39 | PyHeur = heurdata 40 | PyHeur.heurfree() 41 | Py_DECREF(PyHeur) 42 | return SCIP_OKAY 43 | 44 | cdef SCIP_RETCODE PyHeurInit (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 45 | cdef SCIP_HEURDATA* heurdata 46 | heurdata = SCIPheurGetData(heur) 47 | PyHeur = heurdata 48 | PyHeur.heurinit() 49 | return SCIP_OKAY 50 | 51 | cdef SCIP_RETCODE PyHeurExit (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 52 | cdef SCIP_HEURDATA* heurdata 53 | heurdata = SCIPheurGetData(heur) 54 | PyHeur = heurdata 55 | PyHeur.heurexit() 56 | return SCIP_OKAY 57 | 58 | cdef SCIP_RETCODE PyHeurInitsol (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 59 | cdef SCIP_HEURDATA* heurdata 60 | heurdata = SCIPheurGetData(heur) 61 | PyHeur = heurdata 62 | PyHeur.heurinitsol() 63 | return SCIP_OKAY 64 | 65 | cdef SCIP_RETCODE PyHeurExitsol (SCIP* scip, SCIP_HEUR* heur) noexcept with gil: 66 | cdef SCIP_HEURDATA* heurdata 67 | heurdata = SCIPheurGetData(heur) 68 | PyHeur = heurdata 69 | PyHeur.heurexitsol() 70 | return SCIP_OKAY 71 | 72 | cdef SCIP_RETCODE PyHeurExec (SCIP* scip, SCIP_HEUR* heur, SCIP_HEURTIMING heurtiming, SCIP_Bool nodeinfeasible, SCIP_RESULT* result) noexcept with gil: 73 | cdef SCIP_HEURDATA* heurdata 74 | heurdata = SCIPheurGetData(heur) 75 | PyHeur = heurdata 76 | result_dict = PyHeur.heurexec(heurtiming, nodeinfeasible) 77 | result[0] = result_dict.get("result", result[0]) 78 | return SCIP_OKAY 79 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | '''calls execution method of relaxation handler''' 29 | raise NotImplementedError("relaxexec() is a fundamental callback and should be implemented in the derived class") 30 | 31 | cdef SCIP_RETCODE PyRelaxCopy (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 32 | return SCIP_OKAY 33 | 34 | cdef SCIP_RETCODE PyRelaxFree (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 35 | cdef SCIP_RELAXDATA* relaxdata 36 | relaxdata = SCIPrelaxGetData(relax) 37 | PyRelax = relaxdata 38 | PyRelax.relaxfree() 39 | Py_DECREF(PyRelax) 40 | return SCIP_OKAY 41 | 42 | cdef SCIP_RETCODE PyRelaxInit (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 43 | cdef SCIP_RELAXDATA* relaxdata 44 | relaxdata = SCIPrelaxGetData(relax) 45 | PyRelax = relaxdata 46 | PyRelax.relaxinit() 47 | return SCIP_OKAY 48 | 49 | cdef SCIP_RETCODE PyRelaxExit (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 50 | cdef SCIP_RELAXDATA* relaxdata 51 | relaxdata = SCIPrelaxGetData(relax) 52 | PyRelax = relaxdata 53 | PyRelax.relaxexit() 54 | return SCIP_OKAY 55 | 56 | cdef SCIP_RETCODE PyRelaxInitsol (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 57 | cdef SCIP_RELAXDATA* relaxdata 58 | relaxdata = SCIPrelaxGetData(relax) 59 | PyRelax = relaxdata 60 | PyRelax.relaxinitsol() 61 | return SCIP_OKAY 62 | 63 | cdef SCIP_RETCODE PyRelaxExitsol (SCIP* scip, SCIP_RELAX* relax) noexcept with gil: 64 | cdef SCIP_RELAXDATA* relaxdata 65 | relaxdata = SCIPrelaxGetData(relax) 66 | PyRelax = relaxdata 67 | PyRelax.relaxexitsol() 68 | return SCIP_OKAY 69 | 70 | cdef SCIP_RETCODE PyRelaxExec (SCIP* scip, SCIP_RELAX* relax, SCIP_Real* lowerbound, SCIP_RESULT* result) noexcept with gil: 71 | cdef SCIP_RELAXDATA* relaxdata 72 | relaxdata = SCIPrelaxGetData(relax) 73 | PyRelax = relaxdata 74 | result_dict = PyRelax.relaxexec() 75 | assert isinstance(result_dict, dict), "relaxexec() must return a dictionary." 76 | lowerbound[0] = result_dict.get("lowerbound", lowerbound[0]) 77 | result[0] = result_dict.get("result", result[0]) 78 | return SCIP_OKAY -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /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 | raise NotImplementedError("benderscutexec() is a fundamental callback and should be implemented in the derived class") 25 | 26 | cdef SCIP_RETCODE PyBenderscutCopy (SCIP* scip, SCIP_BENDERS* benders, SCIP_BENDERSCUT* benderscut) noexcept with gil: 27 | return SCIP_OKAY 28 | 29 | cdef SCIP_RETCODE PyBenderscutFree (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 30 | cdef SCIP_BENDERSCUTDATA* benderscutdata 31 | benderscutdata = SCIPbenderscutGetData(benderscut) 32 | PyBenderscut = benderscutdata 33 | PyBenderscut.benderscutfree() 34 | Py_DECREF(PyBenderscut) 35 | return SCIP_OKAY 36 | 37 | cdef SCIP_RETCODE PyBenderscutInit (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 38 | cdef SCIP_BENDERSCUTDATA* benderscutdata 39 | benderscutdata = SCIPbenderscutGetData(benderscut) 40 | PyBenderscut = benderscutdata 41 | PyBenderscut.benderscutinit() 42 | return SCIP_OKAY 43 | 44 | cdef SCIP_RETCODE PyBenderscutExit (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 45 | cdef SCIP_BENDERSCUTDATA* benderscutdata 46 | benderscutdata = SCIPbenderscutGetData(benderscut) 47 | PyBenderscut = benderscutdata 48 | PyBenderscut.benderscutexit() 49 | return SCIP_OKAY 50 | 51 | cdef SCIP_RETCODE PyBenderscutInitsol (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 52 | cdef SCIP_BENDERSCUTDATA* benderscutdata 53 | benderscutdata = SCIPbenderscutGetData(benderscut) 54 | PyBenderscut = benderscutdata 55 | PyBenderscut.benderscutinitsol() 56 | return SCIP_OKAY 57 | 58 | cdef SCIP_RETCODE PyBenderscutExitsol (SCIP* scip, SCIP_BENDERSCUT* benderscut) noexcept with gil: 59 | cdef SCIP_BENDERSCUTDATA* benderscutdata 60 | benderscutdata = SCIPbenderscutGetData(benderscut) 61 | PyBenderscut = benderscutdata 62 | PyBenderscut.benderscutexitsol() 63 | return SCIP_OKAY 64 | 65 | 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: 66 | cdef SCIP_BENDERSCUTDATA* benderscutdata 67 | benderscutdata = SCIPbenderscutGetData(benderscut) 68 | PyBenderscut = benderscutdata 69 | if sol == NULL: 70 | solution = None 71 | else: 72 | solution = Solution.create(scip, sol) 73 | enfotype = type 74 | result_dict = PyBenderscut.benderscutexec(solution, probnumber, enfotype) 75 | result[0] = result_dict.get("result", result[0]) 76 | return SCIP_OKAY 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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: ubuntu-24.04-arm 32 | arch: aarch64 33 | - os: macos-14 34 | arch: arm64 35 | - os: macos-15-intel 36 | arch: x86_64 37 | - os: windows-latest 38 | arch: AMD64 39 | 40 | 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | 45 | - name: Build wheels 46 | uses: pypa/cibuildwheel@v3.2.1 47 | env: 48 | CIBW_ARCHS: ${{ matrix.arch }} 49 | CIBW_BEFORE_TEST: "pip install -r {project}/requirements/test.txt" 50 | CIBW_TEST_COMMAND: "pytest {project}/tests" 51 | 52 | - uses: actions/upload-artifact@v4 53 | with: 54 | name: wheels-${{ matrix.os}}-${{ matrix.arch }} 55 | path: ./wheelhouse/*.whl 56 | 57 | build_sdist: 58 | name: Build source distribution 59 | runs-on: ubuntu-22.04 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - name: Build sdist 64 | shell: bash -l {0} 65 | run: pipx run build --sdist 66 | 67 | - uses: actions/upload-artifact@v4 68 | with: 69 | name: source-distribution 70 | path: dist/*.tar.gz 71 | 72 | merge_artifacts: 73 | name: Merge Artifacts 74 | needs: [build_wheels, build_sdist] 75 | runs-on: ubuntu-22.04 76 | steps: 77 | - name: Merge Artifacts 78 | uses: actions/upload-artifact/merge@v4 79 | 80 | upload_pypi: 81 | needs: [build_wheels, build_sdist, merge_artifacts] 82 | runs-on: ubuntu-22.04 83 | if: github.event.inputs.upload_to_pypi == 'true' 84 | steps: 85 | - uses: actions/download-artifact@v4 86 | with: 87 | name: merged-artifacts 88 | path: dist 89 | 90 | - uses: pypa/gh-action-pypi-publish@release/v1 91 | if: github.event.inputs.test_pypi == 'false' 92 | with: 93 | user: __token__ 94 | password: ${{ secrets.PYPI_API_TOKEN }} 95 | verbose: true 96 | 97 | - uses: pypa/gh-action-pypi-publish@release/v1 98 | if: github.event.inputs.test_pypi == 'true' 99 | with: 100 | repository-url: https://test.pypi.org/legacy/ 101 | user: __token__ 102 | password: ${{ secrets.TESTPYPI_API_TOKEN }} 103 | verbose: true 104 | -------------------------------------------------------------------------------- /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 | raise NotImplementedError("eventexec() is a fundamental callback and should be implemented in the derived class") 38 | 39 | 40 | # local helper functions for the interface 41 | cdef Eventhdlr getPyEventhdlr(SCIP_EVENTHDLR* eventhdlr): 42 | cdef SCIP_EVENTHDLRDATA* eventhdlrdata 43 | eventhdlrdata = SCIPeventhdlrGetData(eventhdlr) 44 | return eventhdlrdata 45 | 46 | cdef SCIP_RETCODE PyEventCopy (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 47 | PyEventhdlr = getPyEventhdlr(eventhdlr) 48 | PyEventhdlr.eventcopy() 49 | return SCIP_OKAY 50 | 51 | cdef SCIP_RETCODE PyEventFree (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 52 | PyEventhdlr = getPyEventhdlr(eventhdlr) 53 | PyEventhdlr.eventfree() 54 | Py_DECREF(PyEventhdlr) 55 | return SCIP_OKAY 56 | 57 | cdef SCIP_RETCODE PyEventInit (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 58 | PyEventhdlr = getPyEventhdlr(eventhdlr) 59 | PyEventhdlr.eventinit() 60 | return SCIP_OKAY 61 | 62 | cdef SCIP_RETCODE PyEventExit (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 63 | PyEventhdlr = getPyEventhdlr(eventhdlr) 64 | PyEventhdlr.eventexit() 65 | return SCIP_OKAY 66 | 67 | cdef SCIP_RETCODE PyEventInitsol (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 68 | PyEventhdlr = getPyEventhdlr(eventhdlr) 69 | PyEventhdlr.eventinitsol() 70 | return SCIP_OKAY 71 | 72 | cdef SCIP_RETCODE PyEventExitsol (SCIP* scip, SCIP_EVENTHDLR* eventhdlr) noexcept with gil: 73 | PyEventhdlr = getPyEventhdlr(eventhdlr) 74 | PyEventhdlr.eventexitsol() 75 | return SCIP_OKAY 76 | 77 | cdef SCIP_RETCODE PyEventDelete (SCIP* scip, SCIP_EVENTHDLR* eventhdlr, SCIP_EVENTDATA** eventdata) noexcept with gil: 78 | PyEventhdlr = getPyEventhdlr(eventhdlr) 79 | PyEventhdlr.eventdelete() 80 | return SCIP_OKAY 81 | 82 | cdef SCIP_RETCODE PyEventExec (SCIP* scip, SCIP_EVENTHDLR* eventhdlr, SCIP_EVENT* event, SCIP_EVENTDATA* eventdata) noexcept with gil: 83 | PyEventhdlr = getPyEventhdlr(eventhdlr) 84 | PyEvent = Event() 85 | PyEvent.event = event 86 | PyEventhdlr.eventexec(PyEvent) 87 | return SCIP_OKAY 88 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 as multidict 11 | from pyscipopt.scip import Model as Model 12 | from pyscipopt.scip import Variable as Variable 13 | from pyscipopt.scip import MatrixVariable as MatrixVariable 14 | from pyscipopt.scip import Constraint as Constraint 15 | from pyscipopt.scip import MatrixConstraint as MatrixConstraint 16 | from pyscipopt.scip import Benders as Benders 17 | from pyscipopt.scip import Benderscut as Benderscut 18 | from pyscipopt.scip import Branchrule as Branchrule 19 | from pyscipopt.scip import Nodesel as Nodesel 20 | from pyscipopt.scip import Conshdlr as Conshdlr 21 | from pyscipopt.scip import Eventhdlr as Eventhdlr 22 | from pyscipopt.scip import Heur as Heur 23 | from pyscipopt.scip import Presol as Presol 24 | from pyscipopt.scip import Pricer as Pricer 25 | from pyscipopt.scip import Prop as Prop 26 | from pyscipopt.scip import Reader as Reader 27 | from pyscipopt.scip import Sepa as Sepa 28 | from pyscipopt.scip import LP as LP 29 | from pyscipopt.scip import IISfinder as IISfinder 30 | from pyscipopt.scip import PY_SCIP_LPPARAM as SCIP_LPPARAM 31 | from pyscipopt.scip import readStatistics as readStatistics 32 | from pyscipopt.scip import Expr as Expr 33 | from pyscipopt.scip import MatrixExpr as MatrixExpr 34 | from pyscipopt.scip import MatrixExprCons as MatrixExprCons 35 | from pyscipopt.scip import ExprCons as ExprCons 36 | from pyscipopt.scip import quicksum as quicksum 37 | from pyscipopt.scip import quickprod as quickprod 38 | from pyscipopt.scip import exp as exp 39 | from pyscipopt.scip import log as log 40 | from pyscipopt.scip import sqrt as sqrt 41 | from pyscipopt.scip import sin as sin 42 | from pyscipopt.scip import cos as cos 43 | from pyscipopt.scip import PY_SCIP_RESULT as SCIP_RESULT 44 | from pyscipopt.scip import PY_SCIP_PARAMSETTING as SCIP_PARAMSETTING 45 | from pyscipopt.scip import PY_SCIP_PARAMEMPHASIS as SCIP_PARAMEMPHASIS 46 | from pyscipopt.scip import PY_SCIP_STATUS as SCIP_STATUS 47 | from pyscipopt.scip import PY_SCIP_STAGE as SCIP_STAGE 48 | from pyscipopt.scip import PY_SCIP_NODETYPE as SCIP_NODETYPE 49 | from pyscipopt.scip import PY_SCIP_PROPTIMING as SCIP_PROPTIMING 50 | from pyscipopt.scip import PY_SCIP_PRESOLTIMING as SCIP_PRESOLTIMING 51 | from pyscipopt.scip import PY_SCIP_HEURTIMING as SCIP_HEURTIMING 52 | from pyscipopt.scip import PY_SCIP_EVENTTYPE as SCIP_EVENTTYPE 53 | from pyscipopt.scip import PY_SCIP_LOCKTYPE as SCIP_LOCKTYPE 54 | from pyscipopt.scip import PY_SCIP_LPSOLSTAT as SCIP_LPSOLSTAT 55 | from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR 56 | from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE 57 | from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE 58 | from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN 59 | from pyscipopt.scip import PY_SCIP_NODETYPE as SCIP_NODETYPE 60 | from pyscipopt.scip import PY_SCIP_IMPLINTTYPE as SCIP_IMPLINTTYPE 61 | -------------------------------------------------------------------------------- /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, objoffset, objscale, 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* filename, const char* name, SCIP_PROBDATA* probdata, SCIP_Bool transformed, 43 | SCIP_OBJSENSE objsense, SCIP_Real objoffset, SCIP_Real objscale, 44 | SCIP_RATIONAL* objoffsetexact, SCIP_RATIONAL* objscaleexact, 45 | SCIP_VAR** vars, int nvars, int nbinvars, int nintvars, int nimplvars, 46 | int ncontvars, SCIP_VAR** fixedvars, int nfixedvars, int startnvars, 47 | SCIP_CONS** conss, int nconss, int maxnconss, int startnconss, 48 | SCIP_Bool genericnames, SCIP_RESULT* result) noexcept with gil: 49 | cdef SCIP_READERDATA* readerdata = SCIPreaderGetData(reader) 50 | cdef int fd = fileno(file) 51 | cdef int i 52 | 53 | PyFile = os.fdopen(fd, "w", closefd=False) 54 | PyName = name.decode('utf-8') 55 | PyBinVars = [Variable.create(vars[i]) for i in range(nbinvars)] 56 | PyIntVars = [Variable.create(vars[i]) for i in range(nbinvars, nintvars)] 57 | PyImplVars = [Variable.create(vars[i]) for i in range(nintvars, nimplvars)] 58 | PyContVars = [Variable.create(vars[i]) for i in range(nimplvars, ncontvars)] 59 | PyFixedVars = [Variable.create(fixedvars[i]) for i in range(nfixedvars)] 60 | PyConss = [Constraint.create(conss[i]) for i in range(nconss)] 61 | PyReader = readerdata 62 | #TODO: provide rational objoffsetexact and objscaleexact 63 | result_dict = PyReader.readerwrite(PyFile, PyName, transformed, objsense, objoffset, objscale, 64 | PyBinVars, PyIntVars, PyImplVars, PyContVars, PyFixedVars, startnvars, 65 | PyConss, maxnconss, startnconss, genericnames) 66 | result[0] = result_dict.get("result", result[0]) 67 | 68 | return SCIP_OKAY 69 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | assert 5.61e+03 - 10e-6 < master.getObjVal() < 5.61e+03 + 10e-6 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | For a more complex example, see the `Hybrid Estimate Node Selector `_ on GitHub. 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /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/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 | if selected_node == None: 92 | selnode[0] = NULL 93 | else: 94 | selnode[0] = selected_node.scip_node 95 | 96 | return SCIP_OKAY 97 | 98 | cdef int PyNodeselComp (SCIP* scip, SCIP_NODESEL* nodesel, SCIP_NODE* node1, SCIP_NODE* node2) noexcept with gil: 99 | cdef SCIP_NODESELDATA* nodeseldata 100 | nodeseldata = SCIPnodeselGetData(nodesel) 101 | PyNodesel = nodeseldata 102 | n1 = Node.create(node1) 103 | n2 = Node.create(node2) 104 | result = PyNodesel.nodecomp(n1, n2) # 105 | return result 106 | -------------------------------------------------------------------------------- /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 | raise NotImplementedError("presolexec() is a fundamental callback and should be implemented in the derived class") 29 | 30 | 31 | 32 | cdef SCIP_RETCODE PyPresolCopy (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 33 | return SCIP_OKAY 34 | 35 | cdef SCIP_RETCODE PyPresolFree (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 36 | cdef SCIP_PRESOLDATA* presoldata 37 | presoldata = SCIPpresolGetData(presol) 38 | PyPresol = presoldata 39 | PyPresol.presolfree() 40 | Py_DECREF(PyPresol) 41 | return SCIP_OKAY 42 | 43 | cdef SCIP_RETCODE PyPresolInit (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 44 | cdef SCIP_PRESOLDATA* presoldata 45 | presoldata = SCIPpresolGetData(presol) 46 | PyPresol = presoldata 47 | PyPresol.presolinit() 48 | return SCIP_OKAY 49 | 50 | cdef SCIP_RETCODE PyPresolExit (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 51 | cdef SCIP_PRESOLDATA* presoldata 52 | presoldata = SCIPpresolGetData(presol) 53 | PyPresol = presoldata 54 | PyPresol.presolexit() 55 | return SCIP_OKAY 56 | 57 | 58 | cdef SCIP_RETCODE PyPresolInitpre (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 59 | cdef SCIP_PRESOLDATA* presoldata 60 | presoldata = SCIPpresolGetData(presol) 61 | PyPresol = presoldata 62 | PyPresol.presolinitpre() 63 | return SCIP_OKAY 64 | 65 | cdef SCIP_RETCODE PyPresolExitpre (SCIP* scip, SCIP_PRESOL* presol) noexcept with gil: 66 | cdef SCIP_PRESOLDATA* presoldata 67 | presoldata = SCIPpresolGetData(presol) 68 | PyPresol = presoldata 69 | PyPresol.presolexitpre() 70 | return SCIP_OKAY 71 | 72 | cdef SCIP_RETCODE PyPresolExec (SCIP* scip, SCIP_PRESOL* presol, int nrounds, SCIP_PRESOLTIMING presoltiming, 73 | int nnewfixedvars, int nnewaggrvars, int nnewchgvartypes, int nnewchgbds, int nnewholes, 74 | int nnewdelconss, int nnewaddconss, int nnewupgdconss, int nnewchgcoefs, int nnewchgsides, 75 | int* nfixedvars, int* naggrvars, int* nchgvartypes, int* nchgbds, int* naddholes, 76 | int* ndelconss, int* naddconss, int* nupgdconss, int* nchgcoefs, int* nchgsides, SCIP_RESULT* result) noexcept with gil: 77 | cdef SCIP_PRESOLDATA* presoldata 78 | presoldata = SCIPpresolGetData(presol) 79 | PyPresol = presoldata 80 | result_dict = PyPresol.presolexec(nrounds, presoltiming) 81 | result[0] = result_dict.get("result", result[0]) 82 | nfixedvars[0] += result_dict.get("nnewfixedvars", 0) 83 | naggrvars[0] += result_dict.get("nnewaggrvars", 0) 84 | nchgvartypes[0] += result_dict.get("nnewchgvartypes", 0) 85 | nchgbds[0] += result_dict.get("nnewchgbds", 0) 86 | naddholes[0] += result_dict.get("nnewaddholes", 0) 87 | ndelconss[0] += result_dict.get("nnewdelconss", 0) 88 | naddconss[0] += result_dict.get("nnewaddconss", 0) 89 | nupgdconss[0] += result_dict.get("nnewupgdconss", 0) 90 | nchgcoefs[0] += result_dict.get("nnewchgcoefs", 0) 91 | nchgsides[0] += result_dict.get("nnewchgsides", 0) 92 | return SCIP_OKAY 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/pyscipopt/matrix.pxi: -------------------------------------------------------------------------------- 1 | """ 2 | # TODO Cythonize things. Improve performance. 3 | # TODO Add tests 4 | """ 5 | 6 | import numpy as np 7 | from typing import Union 8 | 9 | 10 | def _is_number(e): 11 | try: 12 | f = float(e) 13 | return True 14 | except ValueError: # for malformed strings 15 | return False 16 | except TypeError: # for other types (Variable, Expr) 17 | return False 18 | 19 | 20 | def _matrixexpr_richcmp(self, other, op): 21 | def _richcmp(self, other, op): 22 | if op == 1: # <= 23 | return self.__le__(other) 24 | elif op == 5: # >= 25 | return self.__ge__(other) 26 | elif op == 2: # == 27 | return self.__eq__(other) 28 | else: 29 | raise NotImplementedError("Can only support constraints with '<=', '>=', or '=='.") 30 | 31 | if _is_number(other) or isinstance(other, Expr): 32 | res = np.empty(self.shape, dtype=object) 33 | res.flat = [_richcmp(i, other, op) for i in self.flat] 34 | 35 | elif isinstance(other, np.ndarray): 36 | out = np.broadcast(self, other) 37 | res = np.empty(out.shape, dtype=object) 38 | res.flat = [_richcmp(i, j, op) for i, j in out] 39 | 40 | else: 41 | raise TypeError(f"Unsupported type {type(other)}") 42 | 43 | return res.view(MatrixExprCons) 44 | 45 | 46 | class MatrixExpr(np.ndarray): 47 | def sum(self, **kwargs): 48 | """ 49 | Based on `numpy.ndarray.sum`, but returns a scalar if `axis=None`. 50 | This is useful for matrix expressions to compare with a matrix or a scalar. 51 | """ 52 | 53 | if kwargs.get("axis") is None: 54 | # Speed up `.sum()` #1070 55 | return quicksum(self.flat) 56 | return super().sum(**kwargs) 57 | 58 | def __le__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: 59 | return _matrixexpr_richcmp(self, other, 1) 60 | 61 | def __ge__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: 62 | return _matrixexpr_richcmp(self, other, 5) 63 | 64 | def __eq__(self, other: Union[float, int, "Expr", np.ndarray, "MatrixExpr"]) -> MatrixExprCons: 65 | return _matrixexpr_richcmp(self, other, 2) 66 | 67 | def __add__(self, other): 68 | return super().__add__(other).view(MatrixExpr) 69 | 70 | def __iadd__(self, other): 71 | return super().__iadd__(other).view(MatrixExpr) 72 | 73 | def __mul__(self, other): 74 | return super().__mul__(other).view(MatrixExpr) 75 | 76 | def __truediv__(self, other): 77 | return super().__truediv__(other).view(MatrixExpr) 78 | 79 | def __rtruediv__(self, other): 80 | return super().__rtruediv__(other).view(MatrixExpr) 81 | 82 | def __pow__(self, other): 83 | return super().__pow__(other).view(MatrixExpr) 84 | 85 | def __sub__(self, other): 86 | return super().__sub__(other).view(MatrixExpr) 87 | 88 | def __radd__(self, other): 89 | return super().__radd__(other).view(MatrixExpr) 90 | 91 | def __rmul__(self, other): 92 | return super().__rmul__(other).view(MatrixExpr) 93 | 94 | def __rsub__(self, other): 95 | return super().__rsub__(other).view(MatrixExpr) 96 | 97 | def __matmul__(self, other): 98 | return super().__matmul__(other).view(MatrixExpr) 99 | 100 | class MatrixGenExpr(MatrixExpr): 101 | pass 102 | 103 | class MatrixExprCons(np.ndarray): 104 | 105 | def __le__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: 106 | return _matrixexpr_richcmp(self, other, 1) 107 | 108 | def __ge__(self, other: Union[float, int, np.ndarray]) -> MatrixExprCons: 109 | return _matrixexpr_richcmp(self, other, 5) 110 | 111 | def __eq__(self, other): 112 | raise NotImplementedError("Cannot compare MatrixExprCons with '=='.") 113 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------