diff --git a/.gitignore b/.gitignore index 0158f1749..8486967c8 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,14 @@ venv.bak/ # mypy .mypy_cache/ + +# pytest +.pytest_cache/ + +# model (for tests) +model +model.cip +model.lp + +# VSCode +.vscode/ diff --git a/.travis.yml b/.travis.yml index 4f2d2340b..004e1f058 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,37 @@ +os: linux +dist: xenial +sudo: true + language: python python: - - '2.7' - - '3.6' + - 2.7 + - 3.5 + - 3.6 + - 3.7 + +env: + global: + - secure: "CML6W6GUTFcZxZavt2x9vT3pUeg9jA2tber8Wl+34zBI9QxXel8PxKlw896OI2jnGPMvL7ANRElklE6/WNaVvogjgZXKcXnqaGKPoPlJNsGenHw0poxjqrrs9VnuX2XU56h53ESsOZ9mq53oFNaimS6fbtIAs7xlS27nY0KJk42ZEicaS2E9cbzH/XqOIEzdIZCHy8NjViMXFCspE9fhndv04T3ic2opXmGDy2veoZ/oF2zbOcz0e9XLEjTs0yXz5qir8AGEnRS4lwI6hb3jkMBOxbNKIPx63gsno3xUHjXYjiwb4iQV9eybhY0csli/5br8isIX81vlg5xeoEfvSy6sZvZ8rErx3Eos5OdCu4vnxqtMZvpb+2pCVQU2IldZTl9B3/lv4ehZhKurF3l89rnqKW14eh4p2eT6WQ2s0tjPd5NuPdow4hT5x7WWSeS1395exlJJGgv1bt4ASM+KNFfA/4CK4TjszZJ7xLttiJ7nOgo/8KtSd/dM0PfBWeeBQxi/0YgCyD781ieL009ZUPwvKf4B0RJ8pPaSDePypKHvzmcm7UGgT86zz1FnCxsIEmHFJQGazXbdBmi0OvPAo1fCrAdMXipppf+ckAotckWjOLIK6IN9RlrF/E9YFll/SfSiXi6EdB0P+T6m8iBqNEToJbUiRqKhMznr7A4+JLs=" + - VERSION=6.0.0 notifications: email: false before_install: - - export VERSION=5.0.1 - - wget http://scip.zib.de/download/release/scipoptsuite-$VERSION.tgz - - tar xf scipoptsuite-$VERSION.tgz - - cd scipoptsuite-$VERSION - - mkdir build - - cd build; cmake .. -DCMAKE_C_STANDARD=99 -DCMAKE_INSTALL_PREFIX=~/scipdir; make -j8 install; cd .. - - cd .. - - rm -rf scipoptsuite-$VERSION + - sudo apt-get install libblas-dev liblapack-dev gfortran + - export VERSION=6.0.0 + - wget http://scip.zib.de/download/release/SCIPOptSuite-$VERSION-Linux.deb + - sudo dpkg -i SCIPOptSuite-$VERSION-Linux.deb install: - - pip install cython networkx pytest-cov codecov - - # SCIPOPTDIR=~/scipdir python setup.py build_ext --inplace --define CYTHON_TRACE - - SCIPOPTDIR=~/scipdir python setup.py install + - pip install cython networkx pytest-cov + - python setup.py install -script: py.test --cov +script: py.test #--cov -after_success: - - codecov +# after_success: + # - codecov deploy: provider: pypi @@ -34,3 +40,4 @@ deploy: secure: ePfiLq2vOJC4O5zYFChHk5wa+quza+m/lsCGPfKXBVpIyb7TvzTHaFDBYtYVZK7710LIKRIcHxvmJPELyKeK1l9QyLxi1x/jOHwk0VbKpf3f5fJjjPaYfXgAUKMMeUplrdhvzU6cgUMrsGhlUE1EIHxc97x5xOa2xlv3lis3j5yjdFUbP6e7MBCEb6c8yU88CclPU2BeHDATzOtMZp0dsyzFTjP9DI7fWbEvOfGy66e5uB/Cjk07mguBZVAUFoukkwKD0KUgBB7RlrAdE61uFVHG8nE5q+G9SZIhQcwULxPLz4v18osJf1aea0g/grZnnrgdG5F24rCA6dSBlvUhnA6aDJXDSgd/dCJ7FV/w3okwhsn18esnycBeM+i3O1pleHsmkq+yFCf2wTbZlm68Hxu+WSirKjie5AtzlSOHa82jQkTjkZI1AHE2syiShnWGvaWpPtoecJKr7aHdFylbJpKwyGvptsObRerWJH5GARXnOoH+FVJ4LrAKcahwCdx0CB63HU2s5p4JgYqAlQV+hFD6yfTDvcKO97/u+8BKlLe9Jnq+fSefEJW1ndOi4mJQ4xGG93sOCub13UCo6zGLvnFlO7R7vwHJeSMDL9Z0Jqmpo2sLhKmaYMr6PhyWvWpXauZOmLTaJEutcnJZ2cjXTU2VuULWwhNYzgXLu9rnVB0= on: tags: true + skip_existing: true diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 000000000..e1b1fc35b --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,52 @@ +Contributing to PySCIPOpt +========================= + +Code contributions are very welcome and should comply to a few rules: + +0. Read `Design principles of PySCIPOpt`_. + +1. Compatibility with both Python-2 and Python-3. + +2. All tests defined in the Continuous Integration setup need to pass: + + - `.travis.yml <.travis.yml>`__ + - `appveyor.yml `__ + +3. New features should be covered by tests *and* examples. Please extend `tests `__ and `examples `__. Tests uses pytest and examples are meant to be accessible for PySCIPOpt newcomers (even advanced examples). + +4. New code should be documented in the same style as the rest of the code. + +5. New code should be `pep8-compliant `__. Help yourself with the `style guide checker `__. + +6. Before implementing a new PySCIPOpt feature, check whether the feature exists in SCIP. If so, implement it as a pure wrapper, mimicking SCIP whenever possible. If the new feature does not exist in SCIP but it is close to an existing one, consider if implementing that way is substantially convenient (e.g. Pythonic). If it does something completely different, you are welcome to pull your request and discuss the implementation. + +7. PySCIPOpt uses `semantic versioning `__. Version number increase only happens on master and must be tagged to build a new PyPI release. + +For general reference, we suggest: + +- `PySCIPOpt README `__; +- `SCIP documentation `__; +- `SCIP mailing list `__ which can be easily searched with search engines (e.g. `Google `__); +- `open and closed PySCIPOpt issues `__; +- `SCIP/PySCIPOpt Stack Exchange `__. + +If you find this contributing guide unclear, please open an issue! :) + +Design principles of PySCIPOpt +============================== + +PySCIPOpt is meant to be a fast-prototyping interface of the pure SCIP C API. By design, we distinguish different functions in PySCIPOPT: + +- pure wrapping functions of SCIP; +- convenience functions. + +**PySCIPOpt wrappers of SCIP functions** should act: + +- with an expected behavior - and parameters, returns, attributes, ... - as close to SCIP as possible +- without *"breaking"* Python and the purpose for what the language it is meant. + +Ideally speaking, we want every SCIP function to be wrapped in PySCIPOpt. + +**Convenience functions** are additional, non-detrimental features meant to help prototyping the Python way. Since these functions are not in SCIP, we wish to limit them to prevent difference in features between SCIP and PySCIPOPT, which are always difficult to maintain. A few convenience functions survive in PySCIPOpt when keeping them is doubtless beneficial. + +Admittedly, *there is a middle ground where functions are not completely wrappers or just convenient*. That is the case, for instance, of fundamental :code:`Model` methods like :code:`addCons` or :code:`writeProblem`. We want to leave their development to negotiation. diff --git a/INSTALL.rst b/INSTALL.rst index 6d8add4c3..4a383b14a 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -44,7 +44,7 @@ your ``PATH`` environment variable: On Linux and OS X this is encoded in the generated PySCIPOpt library and therefore not necessary. -Building everything form source +Building everything from source =============================== PySCIPOpt requires `Cython `__, at least version 0.21 (``pip install cython``). @@ -76,3 +76,26 @@ To use debug information in PySCIPOpt you need to build it like this: Be aware that you will need the **debug library** of the SCIP Optimization Suite for this to work (``cmake .. -DCMAKE_BUILD_TYPE=Debug``). + +Testing new installation +======================== + +To test your brand-new installation of PySCIPOpt you need `pytest `__ on your system. Here is the `installation procedure `__. + +Tests can be run in the ``PySCIPOpt`` directory with: +:: + + py.test # all the available tests + py.test tests/test_name.py # a specific tests/test_name.py (Unix) + +Ideally, the status of your tests must be passed or skipped. Running tests with pytest creates the ``__pycache__`` directory in ``tests`` and, occasionally, a ``model`` file in the working directory. They can be removed harmlessly. + +Common errors +============= + +- readline: ``libreadline.so.6: undefined symbol: PC`` + This is a readline/ncurses compatibility issue that can be fixed like this (when using ``conda``): + + :: + + conda install -c conda-forge readline=6.2 diff --git a/README.rst b/README.rst index f572eec29..1f5b36a14 100644 --- a/README.rst +++ b/README.rst @@ -60,12 +60,12 @@ simple examples. Please notice that in most cases one needs to use a ``dictionary`` to specify the return values needed by SCIP. -Extend the interface -==================== +Extending the interface +======================= -The interface python-scip already provides many of the SCIP callable -library methods. You may also extend python-scip to increase the -functionality of this interface.The following will provide some +PySCIPOpt already covers many of the SCIP callable +library methods. You may also extend it to increase the +functionality of this interface. The following will provide some directions on how this can be achieved: The two most important files in PySCIPOpt are the ``scip.pxd`` and @@ -87,6 +87,10 @@ functions in python that reference the SCIP public functions included in ``scip.pxd``. This is achieved by modifying the ``scip.pyx`` file to add the functionality you require. +We are always happy to accept pull request containing patches or extensions! + +Please have a look at our `contribution guidelines `__. + Gotchas ======= @@ -122,9 +126,10 @@ Dual values ----------- While PySCIPOpt supports access to the dual values of a solution, there are some limitations involved: - + - Can only be used when presolving and propagation is disabled to ensure that the LP solver - which is providing the dual information - actually solves the unmodified problem. - Heuristics should also be disabled to avoid that the problem is solved before the LP solver is called. +- There should be no bound constraints, i.e., constraints with only one variable. This can cause incorrect values as explained in `#136 `__ Therefore, you should use the following settings when trying to work with dual information: diff --git a/appveyor.yml b/appveyor.yml index 73a1c8176..228e8c477 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,11 @@ version: '{build}' -branches: - only: - - master - environment: SCIPOPTDIR: C:\scipoptdir - password: + pypipw: secure: HEa8MAJyyfSv33snyK3Gleflk9SIfZBxbnTiS39hlWM= + optipw: + secure: mi/mkS8vYK1Yza0A1FB4/Q== matrix: - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 @@ -28,7 +26,12 @@ environment: TWINE: C:\Python36-x64\Scripts\twine install: - - ps: wget http://scip.zib.de/download/release/SCIPOptSuite-5.0.1-win64-VS15.exe -outfile scipopt-installer.exe + # - ps: $uri = 'http://opti-test.zib.de/v600-rc06/scip/download/release/SCIPOptSuite-6.0.0-win64-VS15.exe' + # - ps: $user = 'opti-test' + # - ps: $pass = $env:optipw | ConvertTo-SecureString -AsPlainText -Force + # - ps: $cred = New-Object Management.Automation.PSCredential ($user, ($pass)) + # - ps: Invoke-WebRequest -Uri $uri -Credential $cred -OutFile 'scipopt-installer.exe' + - ps: wget http://scip.zib.de/download/release/SCIPOptSuite-6.0.0-win64-VS15.exe -outfile scipopt-installer.exe - scipopt-installer.exe /S /D=%SCIPOPTDIR% - set PATH=%SCIPOPTDIR%\bin;%PYTHON%;%PATH% - if [%INCLUDE_REQUIRED%]==[true] copy .\VC9-include\* %SCIPOPTDIR%\include @@ -46,7 +49,7 @@ artifacts: after_test: - cmd: "echo [pypi] > %USERPROFILE%\\.pypirc" - cmd: "echo username: pyscipopt >> %USERPROFILE%\\.pypirc" - - cmd: "echo password: %password% >> %USERPROFILE%\\.pypirc" + - cmd: "echo password: %pypipw% >> %USERPROFILE%\\.pypirc" - python setup.py bdist_wheel on_success: diff --git a/examples/finished/eoq_en.py b/examples/finished/eoq_en.py index 448ff3857..b540ca829 100644 --- a/examples/finished/eoq_en.py +++ b/examples/finished/eoq_en.py @@ -1,5 +1,5 @@ """ -eoq.py: piecewise linear model to the multi-item economic ordering quantity problem. +eoq_en.py: piecewise linear model to the multi-item economic ordering quantity problem. Approach: use a convex combination formulation. diff --git a/examples/finished/even.py b/examples/finished/even.py new file mode 100644 index 000000000..91dca737b --- /dev/null +++ b/examples/finished/even.py @@ -0,0 +1,72 @@ +from pyscipopt import Model + +################################################################################ +# +# EVEN OR ODD? +# +# If a positional argument is given: +# prints if the argument is even/odd/neither +# else: +# prints if a value is even/odd/neither per each value in a example list +# +# This example is made for newcomers and motivated by: +# - modulus is unsupported for pyscipopt.scip.Variable and int +# - variables are non-integer by default +# Based on this: +# https://github.com/SCIP-Interfaces/PySCIPOpt/issues/172#issuecomment-394644046 +# +################################################################################ + +verbose = False +sdic = {0:"even",1:"odd"} + +def parity(number): + try: + assert number == int(round(number)) + m = Model() + m.hideOutput() + + ### variables are non-negative by default since 0 is the default lb. + ### To allow for negative values, give None as lower bound + ### (None means -infinity as lower bound and +infinity as upper bound) + x = m.addVar("x", vtype="I", lb=None, ub=None) #ub=None is default + n = m.addVar("n", vtype="I", lb=None) + s = m.addVar("s", vtype="B") + + ### CAVEAT: if number is negative, x's lb must be None + ### if x is set by default as non-negative and number is negative: + ### there is no feasible solution (trivial) but the program + ### does not highlight which constraints conflict. + m.addCons(x==number) + + m.addCons(s == x-2*n) + m.setObjective(s) + m.optimize() + + assert m.getStatus() == "optimal" + if verbose: + for v in m.getVars(): + print("%s %d" % (v,m.getVal(v))) + print("%d%%2 == %d?" % (m.getVal(x), m.getVal(s))) + print(m.getVal(s) == m.getVal(x)%2) + + xval = m.getVal(x) + sval = m.getVal(s) + sstr = sdic[sval] + print("%d is %s" % (xval, sstr)) + except (AssertionError, TypeError): + print("%s is neither even nor odd!" % number.__repr__()) + +if __name__ == "__main__": + import sys + from ast import literal_eval as leval + example_values = [0, 1, 1.5, "hallo welt", 20, 25, -101, -15., -10, -int(2**31), int(2**31-1), int(2**63)-1] + try: + try: + n = leval(sys.argv[1]) + except ValueError: + n = sys.argv[1] + parity(n) + except IndexError: + for n in example_values: + parity(n) diff --git a/examples/finished/flp-benders.py b/examples/finished/flp-benders.py new file mode 100644 index 000000000..35fd3b304 --- /dev/null +++ b/examples/finished/flp-benders.py @@ -0,0 +1,127 @@ +""" +flp-benders.py: model for solving the capacitated facility location problem using Benders' decomposition + +minimize the total (weighted) travel cost from n customers +to some facilities with fixed costs and capacities. + +Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 +""" +from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING +import pdb + +def flp(I,J,d,M,f,c): + """flp -- model for the capacitated facility location problem + Parameters: + - I: set of customers + - J: set of facilities + - d[i]: demand for customer i + - M[j]: capacity of facility j + - f[j]: fixed cost for using a facility in point j + - c[i,j]: unit cost of servicing demand point i from facility j + Returns a model, ready to be solved. + """ + + master = Model("flp-master") + subprob = Model("flp-subprob") + + # creating the problem + y = {} + for j in J: + y[j] = master.addVar(vtype="B", name="y(%s)"%j) + + master.setObjective( + quicksum(f[j]*y[j] for j in J), + "minimize") + master.data = y + + # creating the subproblem + x,y = {},{} + for j in J: + y[j] = subprob.addVar(vtype="B", name="y(%s)"%j) + for i in I: + x[i,j] = subprob.addVar(vtype="C", name="x(%s,%s)"%(i,j)) + + for i in I: + subprob.addCons(quicksum(x[i,j] for j in J) == d[i], "Demand(%s)"%i) + + for j in M: + subprob.addCons(quicksum(x[i,j] for i in I) <= M[j]*y[j], "Capacity(%s)"%i) + + for (i,j) in x: + subprob.addCons(x[i,j] <= d[i]*y[j], "Strong(%s,%s)"%(i,j)) + + subprob.setObjective( + quicksum(c[i,j]*x[i,j] for i in I for j in J), + "minimize") + subprob.data = x,y + + return master, subprob + + +def make_data(): + I,d = multidict({1:80, 2:270, 3:250, 4:160, 5:180}) # demand + J,M,f = multidict({1:[500,1000], 2:[500,1000], 3:[500,1000]}) # capacity, fixed costs + c = {(1,1):4, (1,2):6, (1,3):9, # transportation costs + (2,1):5, (2,2):4, (2,3):7, + (3,1):6, (3,2):3, (3,3):4, + (4,1):8, (4,2):5, (4,3):3, + (5,1):10, (5,2):8, (5,3):4, + } + return I,J,d,M,f,c + + + +if __name__ == "__main__": + I,J,d,M,f,c = make_data() + master, subprob = flp(I,J,d,M,f,c) + # initializing the default Benders' decomposition with the subproblem + master.setPresolve(SCIP_PARAMSETTING.OFF) + master.setBoolParam("misc/allowdualreds", False) + master.setBoolParam("benders/copybenders", False) + master.initBendersDefault(subprob) + + # optimizing the problem using Benders' decomposition + master.optimize() + + # solving the subproblems to get the best solution + master.computeBestSolSubproblems() + + EPS = 1.e-6 + y = master.data + facilities = [j for j in y if master.getVal(y[j]) > EPS] + + x, suby = subprob.data + edges = [(i,j) for (i,j) in x if subprob.getVal(x[i,j]) > EPS] + + print("Optimal value:", master.getObjVal()) + print("Facilities at nodes:", facilities) + print("Edges:", edges) + + master.printStatistics() + + # since computeBestSolSubproblems() was called above, we need to free the + # subproblems. This must happen after the solution is extracted, otherwise + # the solution will be lost + master.freeBendersSubproblems() + + try: # plot the result using networkx and matplotlib + import networkx as NX + import matplotlib.pyplot as P + P.clf() + G = NX.Graph() + + other = [j for j in y if j not in facilities] + customers = ["c%s"%i for i in d] + G.add_nodes_from(facilities) + G.add_nodes_from(other) + G.add_nodes_from(customers) + for (i,j) in edges: + G.add_edge("c%s"%i,j) + + position = NX.drawing.layout.spring_layout(G) + NX.draw(G,position,node_color="y",nodelist=facilities) + NX.draw(G,position,node_color="g",nodelist=other) + NX.draw(G,position,node_color="b",nodelist=customers) + P.show() + except ImportError: + print("install 'networkx' and 'matplotlib' for plotting") diff --git a/examples/finished/gcp_fixed_k.py b/examples/finished/gcp_fixed_k.py index 172035a2d..321ec57e2 100644 --- a/examples/finished/gcp_fixed_k.py +++ b/examples/finished/gcp_fixed_k.py @@ -1,4 +1,3 @@ -#todo """ gcp_fixed_k.py: solve the graph coloring problem with fixed-k model diff --git a/examples/finished/logical.py b/examples/finished/logical.py new file mode 100644 index 000000000..eccdda792 --- /dev/null +++ b/examples/finished/logical.py @@ -0,0 +1,78 @@ +from pyscipopt import Model +from pyscipopt import quicksum + +################################################################################ +# +# AND/OR/XOR CONSTRAINTS +# +# Tutorial example on how to use AND/OR/XOR constraints. +# +# N.B.: standard SCIP XOR constraint works differently from AND/OR by design. +# The constraint is set with a boolean rhs instead of an integer resultant. +# cf. http://listserv.zib.de/pipermail/scip/2018-May/003392.html +# A workaround to get the resultant as variable is here proposed. +# +################################################################################ + +def printFunc(name,m): + print("* %s *" % name) + objSet = bool(m.getObjective().terms.keys()) + print("* Is objective set? %s" % objSet) + if objSet: + print("* Sense: %s" % m.getObjectiveSense()) + for v in m.getVars(): + if v.name != "n": + print("%s: %d" % (v, round(m.getVal(v)))) + print("\n") + +# AND # +model = Model() +model.hideOutput() +x = model.addVar("x","B") +y = model.addVar("y","B") +z = model.addVar("z","B") +r = model.addVar("r","B") +model.addConsAnd([x,y,z],r) +model.addCons(x==1) +model.setObjective(r,sense="minimize") +model.optimize() +printFunc("AND",model) + +# OR # +model = Model() +model.hideOutput() +x = model.addVar("x","B") +y = model.addVar("y","B") +z = model.addVar("z","B") +r = model.addVar("r","B") +model.addConsOr([x,y,z],r) +model.addCons(x==0) +model.setObjective(r,sense="maximize") +model.optimize() +printFunc("OR",model) + +# XOR (r as boolean, standard) # +model = Model() +model.hideOutput() +x = model.addVar("x","B") +y = model.addVar("y","B") +z = model.addVar("z","B") +r = True +model.addConsXor([x,y,z],r) +model.addCons(x==1) +model.optimize() +printFunc("Standard XOR (as boolean)",model) + +# XOR (r as variable, custom) # +model = Model() +model.hideOutput() +x = model.addVar("x","B") +y = model.addVar("y","B") +z = model.addVar("z","B") +r = model.addVar("r","B") +n = model.addVar("n","I") #auxiliary +model.addCons(r+quicksum([x,y,z]) == 2*n) +model.addCons(x==0) +model.setObjective(r,sense="maximize") +model.optimize() +printFunc("Custom XOR (as variable)",model) diff --git a/examples/finished/lotsizing_lazy.py b/examples/finished/lotsizing_lazy.py index 9e2b9f2c5..c3db8f505 100644 --- a/examples/finished/lotsizing_lazy.py +++ b/examples/finished/lotsizing_lazy.py @@ -1,5 +1,5 @@ """ -lotsizing_cut.py: solve the single-item lot-sizing problem. +lotsizing_lazy.py: solve the single-item lot-sizing problem. Approaches: - sils: solve the problem using the standard formulation @@ -39,7 +39,7 @@ def addcut(self, checkonly, sol): cutsadded = True return cutsadded - def conscheck(self, constraints, solution, checkintegrality, checklprows, printreason): + def conscheck(self, constraints, solution, checkintegrality, checklprows, printreason, completely): if not self.addcut(checkonly = True, sol = solution): return {"result": SCIP_RESULT.INFEASIBLE} else: @@ -51,7 +51,7 @@ def consenfolp(self, constraints, nusefulconss, solinfeasible): else: return {"result": SCIP_RESULT.FEASIBLE} - def conslock(self, constraint, nlockspos, nlocksneg): + def conslock(self, constraint, locktype, nlockspos, nlocksneg): pass def sils(T,f,c,d,h): diff --git a/examples/finished/markowitz_soco.py b/examples/finished/markowitz_soco.py index 1de14c7f5..ef8db726f 100644 --- a/examples/finished/markowitz_soco.py +++ b/examples/finished/markowitz_soco.py @@ -25,7 +25,10 @@ def markowitz(I,sigma,r,alpha): model.addCons(quicksum(r[i]*x[i] for i in I) >= alpha) model.addCons(quicksum(x[i] for i in I) == 1) - model.setObjective(quicksum(sigma[i]**2 * x[i] * x[i] for i in I), "minimize") + # set nonlinear objective: SCIP only allow for linear objectives hence the following + obj = model.addVar(vtype="C", name="objective", lb = None, ub = None) # auxiliary variable to represent objective + model.addCons(quicksum(sigma[i]**2 * x[i] * x[i] for i in I) <= obj) + model.setObjective(obj, "minimize") model.data = x return model diff --git a/examples/finished/mctransp.py b/examples/finished/mctransp.py index b72dd3a17..c670e79df 100644 --- a/examples/finished/mctransp.py +++ b/examples/finished/mctransp.py @@ -1,5 +1,5 @@ """ -transp.py: a model for the multi-commodity transportation problem +mctransp.py: a model for the multi-commodity transportation problem Model for solving the multi-commodity transportation problem: minimize the total transportation cost for satisfying demand at diff --git a/examples/finished/pfs.py b/examples/finished/pfs.py index f25bb1315..fc178d307 100644 --- a/examples/finished/pfs.py +++ b/examples/finished/pfs.py @@ -1,5 +1,5 @@ """ -permutationflowshop.py: model for the permutation flow shop problem +pfs.py: model for the permutation flow shop problem Use a position index formulation for modeling the permutation flow shop problem, with the objective of minimizing the makespan (maximum diff --git a/examples/finished/transp_nofn.py b/examples/finished/transp_nofn.py index fd366db7c..f15f4b850 100644 --- a/examples/finished/transp_nofn.py +++ b/examples/finished/transp_nofn.py @@ -1,5 +1,5 @@ """ -transp.py: a model for the transportation problem +transp_nofn.py: a model for the transportation problem Model for solving a transportation problem: minimize the total transportation cost for satisfying demand at diff --git a/examples/tutorial/even.py b/examples/tutorial/even.py new file mode 100644 index 000000000..495a997d0 --- /dev/null +++ b/examples/tutorial/even.py @@ -0,0 +1,116 @@ +""" +even.py: Tutorial example to check whether values are even or odd + +Public Domain, WTFNMFPL Public Licence +""" +from pyscipopt import Model +from pprint import pformat as pfmt + +example_values = [ + 0, + 1, + 1.5, + "helloworld", + 20, + 25, + -101, + -15., + -10, + -2**31, + -int(2**31), + "2**31-1", + int(2**31-1), + int(2**63)-1 +] + +verbose = False +#verbose = True # uncomment for additional info on variables! +sdic = {0: "even", 1: "odd"} # remainder to 2 + +def parity(number): + """ + Prints if a value is even/odd/neither per each value in a example list + + This example is made for newcomers and motivated by: + - modulus is unsupported for pyscipopt.scip.Variable and int + - variables are non-integer by default + Based on this: #172#issuecomment-394644046 + + Args: + number: value which parity is checked + + Returns: + sval: 1 if number is odd, 0 if number is even, -1 if neither + """ + sval = -1 + if verbose: + print(80*"*") + try: + assert number == int(round(number)) + m = Model() + m.hideOutput() + + # x and n are integer, s is binary + # Irrespective to their type, variables are non-negative by default + # since 0 is the default lb. To allow for negative values, give None + # as lower bound. + # (None means -infinity as lower bound and +infinity as upper bound) + x = m.addVar("x", vtype="I", lb=None, ub=None) #ub=None is default + n = m.addVar("n", vtype="I", lb=None) + s = m.addVar("s", vtype="B") + # CAVEAT: if number is negative, x's lower bound must be None + # if x is set by default as non-negative and number is negative: + # there is no feasible solution (trivial) but the program + # does not highlight which constraints conflict. + + m.addCons(x==number) + + # minimize the difference between the number and twice a natural number + m.addCons(s == x-2*n) + m.setObjective(s) + m.optimize() + + assert m.getStatus() == "optimal" + boolmod = m.getVal(s) == m.getVal(x)%2 + if verbose: + for v in m.getVars(): + print("%*s: %d" % (fmtlen, v,m.getVal(v))) + print("%*d%%2 == %d?" % (fmtlen, m.getVal(x), m.getVal(s))) + print("%*s" % (fmtlen, boolmod)) + + xval = m.getVal(x) + sval = m.getVal(s) + sstr = sdic[sval] + print("%*d is %s" % (fmtlen, xval, sstr)) + except (AssertionError, TypeError): + print("%*s is neither even nor odd!" % (fmtlen, number.__repr__())) + finally: + if verbose: + print(80*"*") + print("") + return sval + +if __name__ == "__main__": + """ + If positional arguments are given: + the parity check is performed on each of them + Else: + the parity check is performed on each of the default example values + """ + import sys + from ast import literal_eval as leval + try: + # check parity for each positional arguments + sys.argv[1] + values = sys.argv[1:] + except IndexError: + # check parity for each default example value + values = example_values + # format lenght, cosmetics + fmtlen = max([len(fmt) for fmt in pfmt(values,width=1).split('\n')]) + for value in values: + try: + n = leval(value) + except (ValueError, SyntaxError): # for numbers or str w/ spaces + n = value + parity(n) diff --git a/examples/tutorial/logical.py b/examples/tutorial/logical.py new file mode 100644 index 000000000..14e40f233 --- /dev/null +++ b/examples/tutorial/logical.py @@ -0,0 +1,85 @@ +""" +logical.py: Tutorial example on how to use AND/OR/XOR constraints. + +N.B.: standard SCIP XOR constraint works differently from AND/OR by design. +The constraint is set with a boolean rhs instead of an integer resultant. +cf. http://listserv.zib.de/pipermail/scip/2018-May/003392.html +A workaround to get the resultant as variable is here proposed. + +Public Domain, WTFNMFPL Public Licence +""" +from pyscipopt import Model +from pyscipopt import quicksum + +def _init(): + model = Model() + model.hideOutput() + x = model.addVar("x","B") + y = model.addVar("y","B") + z = model.addVar("z","B") + return model, x, y, z + +def _optimize(name, m): + m.optimize() + print("* %s constraint *" % name) + objSet = bool(m.getObjective().terms.keys()) + print("* Is objective set? %s" % objSet) + if objSet: + print("* Sense: %s" % m.getObjectiveSense()) + status = m.getStatus() + print("* Model status: %s" % status) + if status == 'optimal': + for v in m.getVars(): + if v.name != "n": + print("%s: %d" % (v, round(m.getVal(v)))) + else: + print("* No variable is printed if model status is not optimal") + print("") + +def and_constraint(v=1, sense="minimize"): + # AND constraint + assert v in [0,1], "v must be 0 or 1 instead of %s" % v.__repr__() + model, x, y, z = _init() + r = model.addVar("r", "B") + model.addConsAnd([x,y,z], r) + model.addCons(x==v) + model.setObjective(r, sense=sense) + _optimize("AND", model) + + +def or_constraint(v=0, sense="maximize"): + # OR constraint + assert v in [0,1], "v must be 0 or 1 instead of %s" % v.__repr__() + model, x, y, z = _init() + r = model.addVar("r", "B") + model.addConsOr([x,y,z], r) + model.addCons(x==v) + model.setObjective(r, sense=sense) + _optimize("OR", model) + +def xors_constraint(v=1): + # XOR (r as boolean) standard constraint + assert v in [0,1], "v must be 0 or 1 instead of %s" % v.__repr__() + model, x, y, z = _init() + r = True + model.addConsXor([x,y,z], r) + model.addCons(x==v) + _optimize("Standard XOR (as boolean)", model) + +def xorc_constraint(v=0, sense="maximize"): + # XOR (r as variable) custom constraint + assert v in [0,1], "v must be 0 or 1 instead of %s" % v.__repr__() + model, x, y, z = _init() + r = model.addVar("r", "B") + n = model.addVar("n", "I") # auxiliary + model.addCons(r+quicksum([x,y,z]) == 2*n) + model.addCons(x==v) + model.setObjective(r, sense=sense) + _optimize("Custom XOR (as variable)", model) + +if __name__ == "__main__": + and_constraint() + or_constraint() + xors_constraint() + xorc_constraint() + diff --git a/examples/finished/puzzle.py b/examples/tutorial/puzzle.py similarity index 100% rename from examples/finished/puzzle.py rename to examples/tutorial/puzzle.py diff --git a/setup.py b/setup.py index e63552da8..91b4b02ba 100644 --- a/setup.py +++ b/setup.py @@ -53,10 +53,14 @@ extensions = cythonize(extensions) # extensions = cythonize(extensions, compiler_directives={'linetrace': True}) +with open('README.rst') as f: + long_description = f.read() + setup( name = 'PySCIPOpt', version = version, description = 'Python interface and modeling environment for SCIP', + long_description = long_description, url = 'https://github.com/SCIP-Interfaces/PySCIPOpt', author = 'Zuse Institute Berlin', author_email = 'scip@zib.de', diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index c69c4ab77..aeeaef79a 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.4.4' +__version__ = '2.1.0' # export user-relevant objects: from pyscipopt.Multidict import multidict @@ -14,6 +14,7 @@ from pyscipopt.scip import LP from pyscipopt.scip import Expr from pyscipopt.scip import quicksum +from pyscipopt.scip import quickprod from pyscipopt.scip import exp from pyscipopt.scip import log from pyscipopt.scip import sqrt @@ -26,3 +27,5 @@ from pyscipopt.scip import PY_SCIP_PRESOLTIMING as SCIP_PRESOLTIMING from pyscipopt.scip import PY_SCIP_HEURTIMING as SCIP_HEURTIMING from pyscipopt.scip import PY_SCIP_EVENTTYPE as SCIP_EVENTTYPE +from pyscipopt.scip import PY_SCIP_LPSOLSTAT as SCIP_LPSOLSTAT +from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR diff --git a/src/pyscipopt/benders.pxi b/src/pyscipopt/benders.pxi new file mode 100644 index 000000000..f74f46d77 --- /dev/null +++ b/src/pyscipopt/benders.pxi @@ -0,0 +1,180 @@ +cdef class Benders: + cdef public Model model + cdef public str name + + def bendersfree(self): + pass + + def bendersinit(self): + pass + + def bendersexit(self): + pass + + def bendersinitpre(self): + pass + + def bendersexitpre(self): + pass + + def bendersinitsol(self): + pass + + def bendersexitsol(self): + pass + + def benderscreatesub(self, probnumber): + print("python error in benderscreatesub: this method needs to be implemented") + return {} + + def benderspresubsolve(self, solution, enfotype, checkint): + pass + + def benderssolvesubconvex(self, solution, probnumber, onlyconvex): + pass + + def benderssolvesub(self, solution, probnumber): + pass + + def benderspostsolve(self, solution, enfotype, mergecandidates, npriomergecands, checkint, infeasible): + pass + + def bendersfreesub(self, probnumber): + pass + + def bendersgetvar(self, variable, probnumber): + print("python error in bendersgetvar: this method needs to be implemented") + return {} + + +cdef SCIP_RETCODE PyBendersCopy (SCIP* scip, SCIP_BENDERS* benders): + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersFree (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersfree() + Py_DECREF(PyBenders) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersInit (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersinit() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersExit (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersexit() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersInitpre (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersinitpre() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersExitpre (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersexitpre() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersInitsol (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersinitsol() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersExitsol (SCIP* scip, SCIP_BENDERS* benders): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersexitsol() + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersCreatesub (SCIP* scip, SCIP_BENDERS* benders, int probnumber): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.benderscreatesub(probnumber) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersPresubsolve (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, SCIP_BENDERSENFOTYPE type, SCIP_Bool checkint, SCIP_Bool* skipsolve, SCIP_RESULT* result): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + solution = Solution() + solution.sol = sol + enfotype = type + result_dict = PyBenders.benderspresubsolve(solution, enfotype, checkint) + skipsolve[0] = result_dict.get("skipsolve", False) + result[0] = result_dict.get("result", result[0]) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersSolvesubconvex (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, int probnumber, SCIP_Bool onlyconvex, SCIP_Real* objective, SCIP_RESULT* result): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + solution = Solution() + solution.sol = sol + result_dict = PyBenders.benderssolvesub(solution, probnumber, onlyconvex) + objective[0] = result_dict.get("objective", 1e+20) + result[0] = result_dict.get("result", result[0]) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersSolvesub (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, int probnumber, SCIP_Real* objective, SCIP_RESULT* result): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + solution = Solution() + solution.sol = sol + result_dict = PyBenders.benderssolvesub(solution, probnumber) + objective[0] = result_dict.get("objective", 1e+20) + result[0] = result_dict.get("result", result[0]) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersPostsolve (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, + SCIP_BENDERSENFOTYPE type, int* mergecands, int npriomergecands, int nmergecands, SCIP_Bool checkint, + SCIP_Bool infeasible, SCIP_Bool* merged): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + solution = Solution() + solution.sol = sol + enfotype = type + mergecandidates = [] + for i in range(nmergecands): + mergecandidates.append(mergecands[i]) + result_dict = PyBenders.benderspostsolve(solution, enfotype, mergecandidates, npriomergecands, checkint, infeasible) + merged[0] = result_dict.get("merged", False) + return SCIP_OKAY + +cdef SCIP_RETCODE PyBendersFreesub (SCIP* scip, SCIP_BENDERS* benders, int probnumber): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + PyBenders.bendersfreesub(probnumber) + return SCIP_OKAY + +#TODO: Really need to ask about the passing and returning of variables +cdef SCIP_RETCODE PyBendersGetvar (SCIP* scip, SCIP_BENDERS* benders, SCIP_VAR* var, SCIP_VAR** mappedvar, int probnumber): + cdef SCIP_BENDERSDATA* bendersdata + bendersdata = SCIPbendersGetData(benders) + PyBenders = bendersdata + variable = Variable() + variable.var = var + result_dict = PyBenders.bendersgetvar(variable, probnumber) + mappedvariable = result_dict.get("mappedvar", None) + if mappedvariable is None: + mappedvar[0] = NULL + else: + mappedvar[0] = mappedvariable.var + return SCIP_OKAY diff --git a/src/pyscipopt/conshdlr.pxi b/src/pyscipopt/conshdlr.pxi index bf71bfb65..6fa7df666 100644 --- a/src/pyscipopt/conshdlr.pxi +++ b/src/pyscipopt/conshdlr.pxi @@ -44,7 +44,7 @@ cdef class Conshdlr: print("python error in consenfolp: this method needs to be implemented") return {} - def consenforelax(self, constraints, nusefulconss, solinfeasible): + def consenforelax(self, solution, constraints, nusefulconss, solinfeasible): print("python error in consenforelax: this method needs to be implemented") return {} @@ -67,7 +67,7 @@ cdef class Conshdlr: def consresprop(self): return {} - def conslock(self, constraint, nlockspos, nlocksneg): + def conslock(self, constraint, locktype, nlockspos, nlocksneg): print("python error in conslock: this method needs to be implemented") return {} @@ -330,13 +330,13 @@ cdef SCIP_RETCODE PyConsResprop (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* PyConshdlr.consresprop() return SCIP_OKAY -cdef SCIP_RETCODE PyConsLock (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons, int nlockspos, int nlocksneg): +cdef SCIP_RETCODE PyConsLock (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons, SCIP_LOCKTYPE locktype, int nlockspos, int nlocksneg): PyConshdlr = getPyConshdlr(conshdlr) if cons == NULL: - PyConshdlr.conslock(None, nlockspos, nlocksneg) + PyConshdlr.conslock(None, locktype, nlockspos, nlocksneg) else: PyCons = getPyCons(cons) - PyConshdlr.conslock(PyCons, nlockspos, nlocksneg) + PyConshdlr.conslock(PyCons, locktype, nlockspos, nlocksneg) return SCIP_OKAY cdef SCIP_RETCODE PyConsActive (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons): diff --git a/src/pyscipopt/expr.pxi b/src/pyscipopt/expr.pxi index 4a3f9e819..9b27506cc 100644 --- a/src/pyscipopt/expr.pxi +++ b/src/pyscipopt/expr.pxi @@ -112,7 +112,7 @@ CONST = Term() # helper function def buildGenExprObj(expr): - if isinstance(expr, int) or isinstance(expr, float): + if _is_number(expr): return Constant(expr) elif isinstance(expr, Expr): # loop over terms and create a sumexpr with the sum of each term @@ -216,20 +216,32 @@ cdef class Expr: def __div__(self, other): ''' transforms Expr into GenExpr''' + if _is_number(other): + f = 1.0/float(other) + return f * self selfexpr = buildGenExprObj(self) return selfexpr.__div__(other) def __rdiv__(self, other): ''' other / self ''' + if _is_number(self): + f = 1.0/float(self) + return f * other otherexpr = buildGenExprObj(other) return otherexpr.__div__(self) def __truediv__(self,other): + if _is_number(other): + f = 1.0/float(other) + return f * self selfexpr = buildGenExprObj(self) return selfexpr.__truediv__(other) def __rtruediv__(self, other): ''' other / self ''' + if _is_number(self): + f = 1.0/float(self) + return f * other otherexpr = buildGenExprObj(other) return otherexpr.__truediv__(self) @@ -357,6 +369,15 @@ def quicksum(termlist): result += term return result +def quickprod(termlist): + '''multiply linear expressions and constants by avoiding intermediate + data structures and multiplying terms inplace + ''' + result = Expr() + 1 + for term in termlist: + result *= term + return result + class Op: const = 'const' @@ -524,7 +545,7 @@ cdef class GenExpr: divisor = buildGenExprObj(other) # we can't divide by 0 if divisor.getOp() == Operator.const and divisor.number == 0.0: - raise ValueError("cannot divide by 0") + raise ZeroDivisionError("cannot divide by 0") return self * divisor**(-1) def __rdiv__(self, other): @@ -536,7 +557,7 @@ cdef class GenExpr: divisor = buildGenExprObj(other) # we can't divide by 0 if divisor.getOp() == Operator.const and divisor.number == 0.0: - raise ValueError("cannot divide by 0") + raise ZeroDivisionError("cannot divide by 0") return self * divisor**(-1) def __rtruediv__(self, other): diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd old mode 100644 new mode 100755 index a7cc218de..95dc6739d --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -180,6 +180,14 @@ cdef extern from "scip/scip.h": SCIP_EXPR_USER = 69 SCIP_EXPR_LAST = 70 + + ctypedef enum SCIP_BASESTAT: + SCIP_BASESTAT_LOWER = 0 + SCIP_BASESTAT_BASIC = 1 + SCIP_BASESTAT_UPPER = 2 + SCIP_BASESTAT_ZERO = 3 + + ctypedef enum SCIP_EVENTTYPE: SCIP_EVENTTYPE_DISABLED = 0x00000000u SCIP_EVENTTYPE_VARADDED = 0x00000001u @@ -222,6 +230,32 @@ cdef extern from "scip/scip.h": SCIP_LPSOLQUALITY_ESTIMCONDITION = 0 SCIP_LPSOLQUALITY_EXACTCONDITION = 1 + ctypedef enum SCIP_LOCKTYPE: + SCIP_LOCKTYPE_MODEL = 0 + SCIP_LOCKTYPE_CONFLICT = 1 + + ctypedef enum SCIP_BENDERSENFOTYPE: + SCIP_BENDERSENFOTYPE_LP = 1 + SCIP_BENDERSENFOTYPE_RELAX = 2 + SCIP_BENDERSENFOTYPE_PSEUDO = 3 + SCIP_BENDERSENFOTYPE_CHECK = 4 + + ctypedef enum SCIP_LPSOLSTAT: + SCIP_LPSOLSTAT_NOTSOLVED = 0 + SCIP_LPSOLSTAT_OPTIMAL = 1 + SCIP_LPSOLSTAT_INFEASIBLE = 2 + SCIP_LPSOLSTAT_UNBOUNDEDRAY = 3 + SCIP_LPSOLSTAT_OBJLIMIT = 4 + SCIP_LPSOLSTAT_ITERLIMIT = 5 + SCIP_LPSOLSTAT_TIMELIMIT = 6 + SCIP_LPSOLSTAT_ERROR = 7 + + ctypedef enum SCIP_BRANCHDIR: + SCIP_BRANCHDIR_DOWNWARDS = 0 + SCIP_BRANCHDIR_UPWARDS = 1 + SCIP_BRANCHDIR_FIXED = 2 + SCIP_BRANCHDIR_AUTO = 3 + ctypedef bint SCIP_Bool ctypedef long long SCIP_Longint @@ -351,6 +385,9 @@ cdef extern from "scip/scip.h": ctypedef struct SCIP_MESSAGEHDLR: pass + ctypedef struct SCIP_MESSAGEHDLRDATA: + pass + ctypedef struct SCIP_LPI: pass @@ -366,6 +403,12 @@ cdef extern from "scip/scip.h": ctypedef struct SCIP_EXPRDATA_MONOMIAL: pass + ctypedef struct SCIP_BENDERS: + pass + + ctypedef struct SCIP_BENDERSDATA: + pass + ctypedef struct SCIP_QUADVAREVENTDATA: pass @@ -383,10 +426,26 @@ cdef extern from "scip/scip.h": SCIP_VAR* var2 SCIP_Real coef + ctypedef void (*messagecallback) (SCIP_MESSAGEHDLR *messagehdlr, FILE *file, const char *msg) + ctypedef void (*errormessagecallback) (void *data, FILE *file, const char *msg) + ctypedef SCIP_RETCODE (*messagehdlrfree) (SCIP_MESSAGEHDLR *messagehdlr) + # General SCIP Methods SCIP_RETCODE SCIPcreate(SCIP** scip) SCIP_RETCODE SCIPfree(SCIP** scip) + SCIP_RETCODE SCIPmessagehdlrCreate(SCIP_MESSAGEHDLR **messagehdlr, + SCIP_Bool bufferedoutput, + const char *filename, + SCIP_Bool quiet, + messagecallback, + messagecallback, + messagecallback, + messagehdlrfree, + SCIP_MESSAGEHDLRDATA *messagehdlrdata) + + SCIP_RETCODE SCIPsetMessagehdlr(SCIP* scip, SCIP_MESSAGEHDLR* messagehdlr) void SCIPsetMessagehdlrQuiet(SCIP* scip, SCIP_Bool quiet) + void SCIPmessageSetErrorPrinting(errormessagecallback, void* data) SCIP_Real SCIPversion() void SCIPprintVersion(SCIP* scip, FILE* outfile) SCIP_Real SCIPgetTotalTime(SCIP* scip) @@ -397,6 +456,29 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPsetProbName(SCIP* scip, char* name) const char* SCIPgetProbName(SCIP* scip) + # Diving methods + SCIP_RETCODE SCIPstartDive(SCIP* scip) + SCIP_RETCODE SCIPchgVarObjDive(SCIP* scip, SCIP_VAR* var, SCIP_Real newobj) + SCIP_RETCODE SCIPchgVarLbDive(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPchgVarUbDive(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) + SCIP_Real SCIPgetVarLbDive(SCIP* scip, SCIP_VAR* var) + SCIP_Real SCIPgetVarUbDive(SCIP* scip, SCIP_VAR* var) + SCIP_RETCODE SCIPsolveDiveLP(SCIP* scip, int itlim, SCIP_Bool* lperror, SCIP_Bool* cutoff) + SCIP_RETCODE SCIPchgRowLhsDive(SCIP* scip, SCIP_ROW* row, SCIP_Real newlhs) + SCIP_RETCODE SCIPchgRowRhsDive(SCIP* scip, SCIP_ROW* row, SCIP_Real newrhs) + SCIP_RETCODE SCIPaddRowDive(SCIP* scip, SCIP_ROW* row) + SCIP_RETCODE SCIPendDive(SCIP* scip) + + # Probing methods + SCIP_RETCODE SCIPstartProbing(SCIP* scip) + SCIP_RETCODE SCIPchgVarObjProbing(SCIP* scip, SCIP_VAR* var, SCIP_Real newobj) + SCIP_RETCODE SCIPsolveProbingLP(SCIP* scip, int itlim, SCIP_Bool* lperror, SCIP_Bool* cutoff) + SCIP_RETCODE SCIPendProbing(SCIP* scip) + SCIP_RETCODE SCIPfixVarProbing(SCIP* scip, SCIP_VAR* var, SCIP_Real fixedval) + SCIP_Bool SCIPisObjChangedProbing(SCIP* scip) + SCIP_Bool SCIPinProbing(SCIP* scip) + + # Event Methods SCIP_RETCODE SCIPcatchEvent(SCIP* scip, SCIP_EVENTTYPE eventtype, @@ -434,6 +516,12 @@ cdef extern from "scip/scip.h": int filterpos) SCIP_EVENTHDLR* SCIPfindEventhdlr(SCIP* scip, const char* name) SCIP_EVENTTYPE SCIPeventGetType(SCIP_EVENT* event) + SCIP_Real SCIPeventGetNewbound(SCIP_EVENT* event) + SCIP_Real SCIPeventGetOldbound(SCIP_EVENT* event) + SCIP_VAR* SCIPeventGetVar(SCIP_EVENT* event) + SCIP_NODE* SCIPeventGetNode(SCIP_EVENT* event) + SCIP_RETCODE SCIPinterruptSolve(SCIP* scip) + # Global Problem Methods SCIP_RETCODE SCIPcreateProbBasic(SCIP* scip, char* name) @@ -445,6 +533,7 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPsetObjsense(SCIP* scip, SCIP_OBJSENSE objsense) SCIP_OBJSENSE SCIPgetObjsense(SCIP* scip) SCIP_RETCODE SCIPsetObjlimit(SCIP* scip, SCIP_Real objlimit) + SCIP_RETCODE SCIPgetObjlimit(SCIP* scip) SCIP_RETCODE SCIPaddObjoffset(SCIP* scip, SCIP_Real addval) SCIP_RETCODE SCIPaddOrigObjoffset(SCIP* scip, SCIP_Real addval) SCIP_Real SCIPgetOrigObjoffset(SCIP* scip) @@ -461,6 +550,7 @@ cdef extern from "scip/scip.h": # Solve Methods SCIP_RETCODE SCIPsolve(SCIP* scip) SCIP_RETCODE SCIPfreeTransform(SCIP* scip) + SCIP_RETCODE SCIPpresolve(SCIP* scip) # Node Methods SCIP_NODE* SCIPgetCurrentNode(SCIP* scip) @@ -468,10 +558,15 @@ cdef extern from "scip/scip.h": SCIP_Longint SCIPnodeGetNumber(SCIP_NODE* node) int SCIPnodeGetDepth(SCIP_NODE* node) SCIP_Real SCIPnodeGetLowerbound(SCIP_NODE* node) + SCIP_RETCODE SCIPupdateNodeLowerbound(SCIP* scip, SCIP_NODE* node, SCIP_Real newbound) SCIP_Real SCIPnodeGetEstimate(SCIP_NODE* node) SCIP_NODETYPE SCIPnodeGetType(SCIP_NODE* node) SCIP_Bool SCIPnodeIsActive(SCIP_NODE* node) SCIP_Bool SCIPnodeIsPropagatedAgain(SCIP_NODE* node) + SCIP_Real SCIPcalcNodeselPriority(SCIP* scip, SCIP_VAR* var, SCIP_BRANCHDIR branchdir, SCIP_Real targetvalue) + SCIP_Real SCIPcalcChildEstimate(SCIP* scip, SCIP_VAR* var, SCIP_Real targetvalue) + SCIP_RETCODE SCIPcreateChild(SCIP* scip, SCIP_NODE** node, SCIP_Real nodeselprio, SCIP_Real estimate) + SCIP_Bool SCIPinRepropagation(SCIP* scip) # Variable Methods SCIP_RETCODE SCIPcreateVarBasic(SCIP* scip, @@ -484,6 +579,21 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPchgVarObj(SCIP* scip, SCIP_VAR* var, SCIP_Real newobj) SCIP_RETCODE SCIPchgVarLb(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) SCIP_RETCODE SCIPchgVarUb(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPchgVarLbGlobal(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPchgVarUbGlobal(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPchgVarLbNode(SCIP* scip, SCIP_NODE* node, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPchgVarUbNode(SCIP* scip, SCIP_NODE* node, SCIP_VAR* var, SCIP_Real newbound) + SCIP_RETCODE SCIPtightenVarLb(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound, + SCIP_Bool force, SCIP_Bool* infeasible, SCIP_Bool* tightened) + SCIP_RETCODE SCIPtightenVarUb(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound, + SCIP_Bool force, SCIP_Bool* infeasible, SCIP_Bool* tightened) + SCIP_RETCODE SCIPtightenVarLbGlobal(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound, + SCIP_Bool force, SCIP_Bool* infeasible, SCIP_Bool* tightened) + SCIP_RETCODE SCIPtightenVarUbGlobal(SCIP* scip, SCIP_VAR* var, SCIP_Real newbound, + SCIP_Bool force, SCIP_Bool* infeasible, SCIP_Bool* tightened) + SCIP_RETCODE SCIPfixVar(SCIP* scip, SCIP_VAR* var, SCIP_Real fixedval, SCIP_Bool* infeasible, SCIP_Bool* fixed) + SCIP_RETCODE SCIPdelVar(SCIP* scip, SCIP_VAR* var, SCIP_Bool* deleted) + SCIP_RETCODE SCIPchgVarType(SCIP* scip, SCIP_VAR* var, SCIP_VARTYPE vartype, SCIP_Bool* infeasible) SCIP_RETCODE SCIPcaptureVar(SCIP* scip, SCIP_VAR* var) SCIP_RETCODE SCIPaddPricedVar(SCIP* scip, SCIP_VAR* var, SCIP_Real score) @@ -509,6 +619,24 @@ cdef extern from "scip/scip.h": SCIP_Real SCIPvarGetObj(SCIP_VAR* var) SCIP_Real SCIPvarGetLPSol(SCIP_VAR* var) + # LP Methods + SCIP_RETCODE SCIPgetLPColsData(SCIP* scip, SCIP_COL*** cols, int* ncols) + SCIP_RETCODE SCIPgetLPRowsData(SCIP* scip, SCIP_ROW*** rows, int* nrows) + SCIP_RETCODE SCIPgetLPBasisInd(SCIP* scip, int* basisind) + SCIP_RETCODE SCIPgetLPBInvRow(SCIP* scip, int r, SCIP_Real* coefs, int* inds, int* ninds) + SCIP_RETCODE SCIPgetLPBInvARow(SCIP* scip, int r, SCIP_Real* binvrow, SCIP_Real* coefs, int* inds, int* ninds) + SCIP_RETCODE SCIPconstructLP(SCIP* scip, SCIP_Bool* cutoff) + SCIP_Real SCIPgetLPObjval(SCIP* scip) + SCIP_Bool SCIPisLPSolBasic(SCIP* scip) + SCIP_LPSOLSTAT SCIPgetLPSolstat(SCIP* scip) + int SCIPgetNLPRows(SCIP* scip) + int SCIPgetNLPCols(SCIP* scip) + + # Cutting Plane Methods + SCIP_RETCODE SCIPaddPoolCut(SCIP* scip, SCIP_ROW* row) + SCIP_Real SCIPgetCutEfficacy(SCIP* scip, SCIP_SOL* sol, SCIP_ROW* cut) + SCIP_Bool SCIPisCutEfficacious(SCIP* scip, SCIP_SOL* sol, SCIP_ROW* cut) + # Constraint Methods SCIP_RETCODE SCIPcaptureCons(SCIP* scip, SCIP_CONS* cons) SCIP_RETCODE SCIPreleaseCons(SCIP* scip, SCIP_CONS** cons) @@ -534,9 +662,6 @@ cdef extern from "scip/scip.h": const char* SCIPconshdlrGetName(SCIP_CONSHDLR* conshdlr) SCIP_RETCODE SCIPdelConsLocal(SCIP* scip, SCIP_CONS* cons) SCIP_RETCODE SCIPdelCons(SCIP* scip, SCIP_CONS* cons) - SCIP_VAR** SCIPgetVarsLinear(SCIP* scip, SCIP_CONS* cons) - int SCIPgetNVarsLinear(SCIP* scip, SCIP_CONS* cons) - SCIP_Real* SCIPgetValsLinear(SCIP* scip, SCIP_CONS* cons) SCIP_RETCODE SCIPsetConsChecked(SCIP *scip, SCIP_CONS *cons, SCIP_Bool check) SCIP_RETCODE SCIPsetConsRemovable(SCIP *scip, SCIP_CONS *cons, SCIP_Bool removable) SCIP_RETCODE SCIPsetConsInitial(SCIP *scip, SCIP_CONS *cons, SCIP_Bool initial) @@ -569,11 +694,21 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPcreateRow(SCIP* scip, SCIP_ROW** row, const char* name, int len, SCIP_COL** cols, SCIP_Real* vals, SCIP_Real lhs, SCIP_Real rhs, SCIP_Bool local, SCIP_Bool modifiable, SCIP_Bool removable) SCIP_RETCODE SCIPaddRow(SCIP* scip, SCIP_ROW* row, SCIP_Bool forcecut, SCIP_Bool* infeasible) + SCIP_RETCODE SCIPcreateEmptyRowSepa(SCIP* scip, SCIP_ROW** row, SCIP_SEPA* sepa, const char* name, SCIP_Real lhs, SCIP_Real rhs, SCIP_Bool local, SCIP_Bool modifiable, SCIP_Bool removable) + SCIP_RETCODE SCIPcreateEmptyRowUnspec(SCIP* scip, SCIP_ROW** row, const char* name, SCIP_Real lhs, SCIP_Real rhs, SCIP_Bool local, SCIP_Bool modifiable, SCIP_Bool removable) + SCIP_Real SCIPgetRowActivity(SCIP* scip, SCIP_ROW* row) + SCIP_Real SCIPgetRowLPActivity(SCIP* scip, SCIP_ROW* row) + SCIP_RETCODE SCIPreleaseRow(SCIP* scip, SCIP_ROW** row) + SCIP_RETCODE SCIPcacheRowExtensions(SCIP* scip, SCIP_ROW* row) + SCIP_RETCODE SCIPflushRowExtensions(SCIP* scip, SCIP_ROW* row) + SCIP_RETCODE SCIPaddVarToRow(SCIP* scip, SCIP_ROW* row, SCIP_VAR* var, SCIP_Real val) + SCIP_RETCODE SCIPprintRow(SCIP* scip, SCIP_ROW* row, FILE* file) # Dual Solution Methods SCIP_Real SCIPgetDualbound(SCIP* scip) SCIP_Real SCIPgetDualboundRoot(SCIP* scip) SCIP_Real SCIPgetVarRedcost(SCIP* scip, SCIP_VAR* var) + SCIP_RETCODE SCIPgetDualSolVal(SCIP* scip, SCIP_CONS* cons, SCIP_Real* dualsolval, SCIP_Bool* boundconstraint) # Reader plugin SCIP_RETCODE SCIPincludeReader(SCIP* scip, @@ -659,13 +794,13 @@ cdef extern from "scip/scip.h": SCIP_RETCODE (*conssepalp) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_RESULT* result), SCIP_RETCODE (*conssepasol) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_SOL* sol, SCIP_RESULT* result), SCIP_RETCODE (*consenfolp) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_Bool solinfeasible, SCIP_RESULT* result), - SCIP_RETCODE (*consenfolp) (SCIP* scip, SCIP_SOL* sol, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_Bool solinfeasible, SCIP_RESULT* result), + SCIP_RETCODE (*consenforelax) (SCIP* scip, SCIP_SOL* sol, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_Bool solinfeasible, SCIP_RESULT* result), SCIP_RETCODE (*consenfops) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, SCIP_Bool solinfeasible, SCIP_Bool objinfeasible, SCIP_RESULT* result), SCIP_RETCODE (*conscheck) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, SCIP_SOL* sol, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool printreason, SCIP_Bool completely, SCIP_RESULT* result), SCIP_RETCODE (*consprop) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nusefulconss, int nmarkedconss, SCIP_PROPTIMING proptiming, SCIP_RESULT* result), SCIP_RETCODE (*conspresol) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS** conss, int nconss, int nrounds, SCIP_PRESOLTIMING presoltiming, int nnewfixedvars, int nnewaggrvars, int nnewchgvartypes, int nnewchgbds, int nnewholes, int nnewdelconss, int nnewaddconss, int nnewupgdconss, int nnewchgcoefs, int nnewchgsides, int* nfixedvars, int* naggrvars, int* nchgvartypes, int* nchgbds, int* naddholes, int* ndelconss, int* naddconss, int* nupgdconss, int* nchgcoefs, int* nchgsides, SCIP_RESULT* result), SCIP_RETCODE (*consresprop) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons, SCIP_VAR* infervar, int inferinfo, SCIP_BOUNDTYPE boundtype, SCIP_BDCHGIDX* bdchgidx, SCIP_Real relaxedbd, SCIP_RESULT* result), - SCIP_RETCODE (*conslock) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons, int nlockspos, int nlocksneg), + SCIP_RETCODE (*conslock) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons, SCIP_LOCKTYPE locktype, int nlockspos, int nlocksneg), SCIP_RETCODE (*consactive) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons), SCIP_RETCODE (*consdeactive) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons), SCIP_RETCODE (*consenable) (SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons), @@ -720,6 +855,7 @@ cdef extern from "scip/scip.h": SCIP_RETCODE (*sepaexecsol) (SCIP* scip, SCIP_SEPA* sepa, SCIP_SOL* sol, SCIP_RESULT* result, unsigned int allowlocal), SCIP_SEPADATA* sepadata) SCIP_SEPADATA* SCIPsepaGetData(SCIP_SEPA* sepa) + SCIP_SEPA* SCIPfindSepa(SCIP* scip, const char* name) # Propagator plugin SCIP_RETCODE SCIPincludeProp(SCIP* scip, @@ -810,12 +946,82 @@ cdef extern from "scip/scip.h": SCIP_BRANCHRULEDATA* branchruledata) SCIP_BRANCHRULEDATA* SCIPbranchruleGetData(SCIP_BRANCHRULE* branchrule) + # Benders' decomposition plugin + SCIP_RETCODE SCIPincludeBenders(SCIP* scip, + const char* name, + const char* desc, + int priority, + SCIP_Bool cutlp, + SCIP_Bool cutpseudo, + SCIP_Bool cutrelax, + SCIP_Bool shareaux, + SCIP_RETCODE (*benderscopy) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersfree) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersinit) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersexit) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersinitpre) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersexitpre) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersinitsol) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersexitsol) (SCIP* scip, SCIP_BENDERS* benders), + SCIP_RETCODE (*bendersgetvar) (SCIP* scip, SCIP_BENDERS* benders, SCIP_VAR* var, SCIP_VAR** mappedvar, int probnumber), + SCIP_RETCODE (*benderscreatesub) (SCIP* scip, SCIP_BENDERS* benders, int probnumber), + SCIP_RETCODE (*benderspresubsolve) (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, SCIP_BENDERSENFOTYPE type, SCIP_Bool checkint, SCIP_Bool* skipsolve, SCIP_RESULT* result), + SCIP_RETCODE (*benderssolvesubconvex) (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, int probnumber, SCIP_Bool onlyconvex, SCIP_Real* objective, SCIP_RESULT* result), + SCIP_RETCODE (*benderssolvesub) (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, int probnumber, SCIP_Real* objective, SCIP_RESULT* result), + SCIP_RETCODE (*benderspostsolve) (SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, SCIP_BENDERSENFOTYPE type, int* mergecands, int npriomergecands, int nmergecands, SCIP_Bool checkint, SCIP_Bool infeasible, SCIP_Bool* merged), + SCIP_RETCODE (*bendersfreesub) (SCIP* scip, SCIP_BENDERS* benders, int probnumber), + SCIP_BENDERSDATA* bendersdata) + SCIP_BENDERS* SCIPfindBenders(SCIP* scip, const char* name) + SCIP_RETCODE SCIPactivateBenders(SCIP* scip, SCIP_BENDERS* benders) + SCIP_BENDERSDATA* SCIPbendersGetData(SCIP_BENDERS* benders) + SCIP_RETCODE SCIPcreateBendersDefault(SCIP* scip, SCIP** subproblems, int nsubproblems) + int SCIPbendersGetNSubproblems(SCIP_BENDERS* benders); + SCIP_RETCODE SCIPsolveBendersSubproblems(SCIP* scip, SCIP_BENDERS* benders, + SCIP_SOL* sol, SCIP_RESULT* result, SCIP_Bool* infeasible, + SCIP_Bool* auxviol, SCIP_BENDERSENFOTYPE type, SCIP_Bool checkint) + SCIP_RETCODE SCIPsetupBendersSubproblem(SCIP* scip, SCIP_BENDERS* benders, SCIP_SOL* sol, int probnumber) + SCIP_RETCODE SCIPsolveBendersSubproblem(SCIP* scip, SCIP_BENDERS* benders, + SCIP_SOL* sol, int probnumber, SCIP_Bool* infeasible, SCIP_BENDERSENFOTYPE type, + SCIP_Bool solvecip, SCIP_Real* objective) + SCIP_RETCODE SCIPfreeBendersSubproblem(SCIP* scip, SCIP_BENDERS* benders, int probnumber) + int SCIPgetNActiveBenders(SCIP* scip) + SCIP_BENDERS** SCIPgetBenders(SCIP* scip) + void SCIPbendersUpdateSubproblemLowerbound(SCIP_BENDERS* benders, int probnumber, SCIP_Real lowerbound) + + SCIP_RETCODE SCIPbranchVar(SCIP* scip, + SCIP_VAR* var, + SCIP_NODE** downchild, + SCIP_NODE** eqchild, + SCIP_NODE** upchild) + + SCIP_RETCODE SCIPbranchVarVal(SCIP* scip, + SCIP_VAR* var, + SCIP_Real val, + SCIP_NODE** downchild, + SCIP_NODE** eqchild, + SCIP_NODE** upchild) + int SCIPgetNLPBranchCands(SCIP* scip) + SCIP_RETCODE SCIPgetLPBranchCands(SCIP* scip, SCIP_VAR*** lpcands, SCIP_Real** lpcandssol, + SCIP_Real** lpcandsfrac, int* nlpcands, int* npriolpcands, int* nfracimplvars) + + # Numerical Methods SCIP_Real SCIPinfinity(SCIP* scip) + SCIP_Real SCIPfrac(SCIP* scip, SCIP_Real val) + SCIP_Real SCIPfeasFrac(SCIP* scip, SCIP_Real val) + SCIP_Bool SCIPisZero(SCIP* scip, SCIP_Real val) + SCIP_Bool SCIPisFeasZero(SCIP* scip, SCIP_Real val) + SCIP_Bool SCIPisFeasNegative(SCIP* scip, SCIP_Real val) + SCIP_Bool SCIPisInfinity(SCIP* scip, SCIP_Real val) + SCIP_Bool SCIPisLE(SCIP* scip, SCIP_Real val1, SCIP_Real val2) + SCIP_Bool SCIPisLT(SCIP* scip, SCIP_Real val1, SCIP_Real val2) + SCIP_Bool SCIPisGE(SCIP* scip, SCIP_Real val1, SCIP_Real val2) + SCIP_Bool SCIPisGT(SCIP* scip, SCIP_Real val1, SCIP_Real val2) # Statistic Methods SCIP_RETCODE SCIPprintStatistics(SCIP* scip, FILE* outfile) SCIP_Longint SCIPgetNNodes(SCIP* scip) + SCIP_Longint SCIPgetNLPs(SCIP* scip) # Parameter Functions SCIP_RETCODE SCIPsetBoolParam(SCIP* scip, char* name, SCIP_Bool value) @@ -912,6 +1118,9 @@ cdef extern from "scip/cons_linear.h": SCIP_Real SCIPgetLhsLinear(SCIP* scip, SCIP_CONS* cons) SCIP_Real SCIPgetRhsLinear(SCIP* scip, SCIP_CONS* cons) SCIP_Real SCIPgetActivityLinear(SCIP* scip, SCIP_CONS* cons, SCIP_SOL* sol) + SCIP_VAR** SCIPgetVarsLinear(SCIP* scip, SCIP_CONS* cons) + int SCIPgetNVarsLinear(SCIP* scip, SCIP_CONS* cons) + SCIP_Real* SCIPgetValsLinear(SCIP* scip, SCIP_CONS* cons) cdef extern from "scip/cons_quadratic.h": SCIP_RETCODE SCIPcreateConsQuadratic(SCIP* scip, @@ -1010,6 +1219,60 @@ cdef extern from "scip/cons_sos2.h": SCIP_CONS* cons, SCIP_VAR* var) +cdef extern from "scip/cons_and.h": + SCIP_RETCODE SCIPcreateConsAnd(SCIP* scip, + SCIP_CONS** cons, + const char* name, + SCIP_VAR* resvar, + int nvars, + SCIP_VAR** vars, + SCIP_Bool initial, + SCIP_Bool separate, + SCIP_Bool enforce, + SCIP_Bool check, + SCIP_Bool propagate, + SCIP_Bool local, + SCIP_Bool modifiable, + SCIP_Bool dynamic, + SCIP_Bool removable, + SCIP_Bool stickingatnode) + +cdef extern from "scip/cons_or.h": + SCIP_RETCODE SCIPcreateConsOr(SCIP* scip, + SCIP_CONS** cons, + const char* name, + SCIP_VAR* resvar, + int nvars, + SCIP_VAR** vars, + SCIP_Bool initial, + SCIP_Bool separate, + SCIP_Bool enforce, + SCIP_Bool check, + SCIP_Bool propagate, + SCIP_Bool local, + SCIP_Bool modifiable, + SCIP_Bool dynamic, + SCIP_Bool removable, + SCIP_Bool stickingatnode) + +cdef extern from "scip/cons_xor.h": + SCIP_RETCODE SCIPcreateConsXor(SCIP* scip, + SCIP_CONS** cons, + const char* name, + SCIP_Bool rhs, + int nvars, + SCIP_VAR** vars, + SCIP_Bool initial, + SCIP_Bool separate, + SCIP_Bool enforce, + SCIP_Bool check, + SCIP_Bool propagate, + SCIP_Bool local, + SCIP_Bool modifiable, + SCIP_Bool dynamic, + SCIP_Bool removable, + SCIP_Bool stickingatnode) + cdef extern from "blockmemshell/memory.h": void BMScheckEmptyMemory() long long BMSgetMemoryUsed() @@ -1141,3 +1404,24 @@ cdef extern from "scip/paramset.h": char SCIPparamGetChar(SCIP_PARAM* param) char* SCIPparamGetString(SCIP_PARAM* param) +cdef extern from "scip/pub_lp.h": + # Row Methods + SCIP_Real SCIProwGetLhs(SCIP_ROW* row) + SCIP_Real SCIProwGetRhs(SCIP_ROW* row) + SCIP_Real SCIProwGetConstant(SCIP_ROW* row) + int SCIProwGetLPPos(SCIP_ROW* row) + SCIP_BASESTAT SCIProwGetBasisStatus(SCIP_ROW* row) + SCIP_Bool SCIProwIsIntegral(SCIP_ROW* row) + SCIP_Bool SCIProwIsModifiable(SCIP_ROW* row) + int SCIProwGetNNonz(SCIP_ROW* row) + int SCIProwGetNLPNonz(SCIP_ROW* row) + SCIP_COL** SCIProwGetCols(SCIP_ROW* row) + SCIP_Real* SCIProwGetVals(SCIP_ROW* row) + # Column Methods + int SCIPcolGetLPPos(SCIP_COL* col) + SCIP_BASESTAT SCIPcolGetBasisStatus(SCIP_COL* col) + SCIP_Bool SCIPcolIsIntegral(SCIP_COL* col) + SCIP_VAR* SCIPcolGetVar(SCIP_COL* col) + SCIP_Real SCIPcolGetPrimsol(SCIP_COL* col) + SCIP_Real SCIPcolGetLb(SCIP_COL* col) + SCIP_Real SCIPcolGetUb(SCIP_COL* col) diff --git a/src/pyscipopt/scip.pyx b/src/pyscipopt/scip.pyx old mode 100644 new mode 100755 index 715ae8b84..32c8cf183 --- a/src/pyscipopt/scip.pyx +++ b/src/pyscipopt/scip.pyx @@ -1,3 +1,5 @@ +##@file scip.pyx +#@brief holding functions in python that reference the SCIP public functions included in scip.pxd import weakref from os.path import abspath from os.path import splitext @@ -10,6 +12,8 @@ from libc.stdio cimport fdopen include "expr.pxi" include "lp.pxi" +include "benders.pxi" +include "benderscut.pxi" include "branchrule.pxi" include "conshdlr.pxi" include "event.pxi" @@ -18,11 +22,12 @@ include "presol.pxi" include "pricer.pxi" include "propagator.pxi" include "sepa.pxi" +include "relax.pxi" # recommended SCIP version; major version is required -MAJOR = 5 +MAJOR = 6 MINOR = 0 -PATCH = 1 +PATCH = 0 # for external user functions use def; for functions used only inside the interface (starting with _) use cdef # todo: check whether this is currently done like this @@ -182,6 +187,21 @@ cdef class PY_SCIP_EVENTTYPE: ROWSIDECHANGED = SCIP_EVENTTYPE_ROWSIDECHANGED SYNC = SCIP_EVENTTYPE_SYNC +cdef class PY_SCIP_LPSOLSTAT: + NOTSOLVED = SCIP_LPSOLSTAT_NOTSOLVED + OPTIMAL = SCIP_LPSOLSTAT_OPTIMAL + INFEASIBLE = SCIP_LPSOLSTAT_INFEASIBLE + UNBOUNDEDRAY = SCIP_LPSOLSTAT_UNBOUNDEDRAY + OBJLIMIT = SCIP_LPSOLSTAT_OBJLIMIT + ITERLIMIT = SCIP_LPSOLSTAT_ITERLIMIT + TIMELIMIT = SCIP_LPSOLSTAT_TIMELIMIT + ERROR = SCIP_LPSOLSTAT_ERROR + +cdef class PY_SCIP_BRANCHDIR: + DOWNWARDS = SCIP_BRANCHDIR_DOWNWARDS + UPWARDS = SCIP_BRANCHDIR_UPWARDS + FIXED = SCIP_BRANCHDIR_FIXED + AUTO = SCIP_BRANCHDIR_AUTO def PY_SCIP_CALL(SCIP_RETCODE rc): if rc == SCIP_OKAY: @@ -226,6 +246,8 @@ def PY_SCIP_CALL(SCIP_RETCODE rc): cdef class Event: cdef SCIP_EVENT* event + # can be used to store problem data + cdef public object data @staticmethod cdef create(SCIP_EVENT* scip_event): @@ -234,34 +256,155 @@ cdef class Event: return event def getType(self): + """gets type of event""" return SCIPeventGetType(self.event) def __repr__(self): return self.getType() + def getNewBound(self): + """gets new bound for a bound change event""" + return SCIPeventGetNewbound(self.event) + + def getOldBound(self): + """gets old bound for a bound change event""" + return SCIPeventGetOldbound(self.event) + + def getVar(self): + """gets variable for a variable event (var added, var deleted, var fixed, objective value or domain change, domain hole added or removed)""" + cdef SCIP_VAR* var = SCIPeventGetVar(self.event) + return Variable.create(var) + + def getNode(self): + """gets node for a node or LP event""" + cdef SCIP_NODE* node = SCIPeventGetNode(self.event) + return Node.create(node) + cdef class Column: """Base class holding a pointer to corresponding SCIP_COL""" - cdef SCIP_COL* col + cdef SCIP_COL* scip_col + # can be used to store problem data + cdef public object data @staticmethod - cdef create(SCIP_COL* scip_col): + cdef create(SCIP_COL* scipcol): col = Column() - col.col = scip_col + col.scip_col = scipcol return col + def getLPPos(self): + """gets position of column in current LP, or -1 if it is not in LP""" + return SCIPcolGetLPPos(self.scip_col) + + def getBasisStatus(self): + """gets the basis status of a column in the LP solution, Note: returns basis status `zero` for columns not in the current SCIP LP""" + cdef SCIP_BASESTAT stat = SCIPcolGetBasisStatus(self.scip_col) + if stat == SCIP_BASESTAT_LOWER: + return "lower" + elif stat == SCIP_BASESTAT_BASIC: + return "basic" + elif stat == SCIP_BASESTAT_UPPER: + return "upper" + elif stat == SCIP_BASESTAT_ZERO: + return "zero" + else: + raise Exception('SCIP returned unknown base status!') + + def isIntegral(self): + """returns whether the associated variable is of integral type (binary, integer, implicit integer)""" + return SCIPcolIsIntegral(self.scip_col) + + def getVar(self): + """gets variable this column represents""" + cdef SCIP_VAR* var = SCIPcolGetVar(self.scip_col) + return Variable.create(var) + + def getPrimsol(self): + """gets the primal LP solution of a column""" + return SCIPcolGetPrimsol(self.scip_col) + + def getLb(self): + """gets lower bound of column""" + return SCIPcolGetLb(self.scip_col) + + def getUb(self): + """gets upper bound of column""" + return SCIPcolGetUb(self.scip_col) + cdef class Row: """Base class holding a pointer to corresponding SCIP_ROW""" - cdef SCIP_ROW* row + cdef SCIP_ROW* scip_row + # can be used to store problem data + cdef public object data @staticmethod - cdef create(SCIP_ROW* scip_row): + cdef create(SCIP_ROW* sciprow): row = Row() - row.row = scip_row + row.scip_row = sciprow return row + def getLhs(self): + """returns the left hand side of row""" + return SCIProwGetLhs(self.scip_row) + + def getRhs(self): + """returns the right hand side of row""" + return SCIProwGetRhs(self.scip_row) + + def getConstant(self): + """gets constant shift of row""" + return SCIProwGetConstant(self.scip_row) + + def getLPPos(self): + """gets position of row in current LP, or -1 if it is not in LP""" + return SCIProwGetLPPos(self.scip_row) + + def getBasisStatus(self): + """gets the basis status of a row in the LP solution, Note: returns basis status `basic` for rows not in the current SCIP LP""" + cdef SCIP_BASESTAT stat = SCIProwGetBasisStatus(self.scip_row) + if stat == SCIP_BASESTAT_LOWER: + return "lower" + elif stat == SCIP_BASESTAT_BASIC: + return "basic" + elif stat == SCIP_BASESTAT_UPPER: + return "upper" + elif stat == SCIP_BASESTAT_ZERO: + # this shouldn't happen! + raise Exception('SCIP returned base status zero for a row!') + else: + raise Exception('SCIP returned unknown base status!') + + def isIntegral(self): + """returns TRUE iff the activity of the row (without the row's constant) is always integral in a feasible solution """ + return SCIProwIsIntegral(self.scip_row) + + def isModifiable(self): + """returns TRUE iff row is modifiable during node processing (subject to column generation) """ + return SCIProwIsModifiable(self.scip_row) + + def getNNonz(self): + """get number of nonzero entries in row vector""" + return SCIProwGetNNonz(self.scip_row) + + def getNLPNonz(self): + """get number of nonzero entries in row vector that correspond to columns currently in the SCIP LP""" + return SCIProwGetNLPNonz(self.scip_row) + + def getCols(self): + """gets list with columns of nonzero entries""" + cdef SCIP_COL** cols = SCIProwGetCols(self.scip_row) + return [Column.create(cols[i]) for i in range(self.getNNonz())] + + def getVals(self): + """gets list with coefficients of nonzero entries""" + cdef SCIP_Real* vals = SCIProwGetVals(self.scip_row) + return [vals[i] for i in range(self.getNNonz())] + cdef class Solution: """Base class holding a pointer to corresponding SCIP_SOL""" cdef SCIP_SOL* sol + # can be used to store problem data + cdef public object data @staticmethod cdef create(SCIP_SOL* scip_sol): @@ -271,77 +414,81 @@ cdef class Solution: cdef class Node: """Base class holding a pointer to corresponding SCIP_NODE""" - cdef SCIP_NODE* node + cdef SCIP_NODE* scip_node + # can be used to store problem data + cdef public object data @staticmethod - cdef create(SCIP_NODE* scip_node): + cdef create(SCIP_NODE* scipnode): node = Node() - node.node = scip_node + node.scip_node = scipnode return node def getParent(self): """Retrieve parent node.""" - return Node.create(SCIPnodeGetParent(self.node)) + return Node.create(SCIPnodeGetParent(self.scip_node)) def getNumber(self): """Retrieve number of node.""" - return SCIPnodeGetNumber(self.node) + return SCIPnodeGetNumber(self.scip_node) def getDepth(self): """Retrieve depth of node.""" - return SCIPnodeGetDepth(self.node) + return SCIPnodeGetDepth(self.scip_node) def getType(self): """Retrieve type of node.""" - return SCIPnodeGetType(self.node) + return SCIPnodeGetType(self.scip_node) def getLowerbound(self): """Retrieve lower bound of node.""" - return SCIPnodeGetLowerbound(self.node) + return SCIPnodeGetLowerbound(self.scip_node) def getEstimate(self): """Retrieve the estimated value of the best feasible solution in subtree of the node""" - return SCIPnodeGetEstimate(self.node) + return SCIPnodeGetEstimate(self.scip_node) def getNAddedConss(self): """Retrieve number of added constraints at this node""" - return SCIPnodeGetNAddedConss(self.node) + return SCIPnodeGetNAddedConss(self.scip_node) def isActive(self): """Is the node in the path to the current node?""" - return SCIPnodeIsActive(self.node) + return SCIPnodeIsActive(self.scip_node) def isPropagatedAgain(self): """Is the node marked to be propagated again?""" - return SCIPnodeIsPropagatedAgain(self.node) + return SCIPnodeIsPropagatedAgain(self.scip_node) cdef class Variable(Expr): """Is a linear expression and has SCIP_VAR*""" - cdef SCIP_VAR* var + cdef SCIP_VAR* scip_var + # can be used to store problem data + cdef public object data @staticmethod cdef create(SCIP_VAR* scipvar): var = Variable() - var.var = scipvar + var.scip_var = scipvar Expr.__init__(var, {Term(var) : 1.0}) return var property name: def __get__(self): - cname = bytes( SCIPvarGetName(self.var) ) + cname = bytes( SCIPvarGetName(self.scip_var) ) return cname.decode('utf-8') def ptr(self): """ """ - return (self.var) + return (self.scip_var) def __repr__(self): return self.name def vtype(self): """Retrieve the variables type (BINARY, INTEGER or CONTINUOUS)""" - vartype = SCIPvarGetType(self.var) + vartype = SCIPvarGetType(self.scip_var) if vartype == SCIP_VARTYPE_BINARY: return "BINARY" elif vartype == SCIP_VARTYPE_INTEGER: @@ -351,66 +498,67 @@ cdef class Variable(Expr): def isOriginal(self): """Retrieve whether the variable belongs to the original problem""" - return SCIPvarIsOriginal(self.var) + return SCIPvarIsOriginal(self.scip_var) def isInLP(self): """Retrieve whether the variable is a COLUMN variable that is member of the current LP""" - return SCIPvarIsInLP(self.var) + return SCIPvarIsInLP(self.scip_var) def getCol(self): """Retrieve column of COLUMN variable""" cdef SCIP_COL* scip_col - scip_col = SCIPvarGetCol(self.var) + scip_col = SCIPvarGetCol(self.scip_var) return Column.create(scip_col) def getLbOriginal(self): """Retrieve original lower bound of variable""" - return SCIPvarGetLbOriginal(self.var) + return SCIPvarGetLbOriginal(self.scip_var) def getUbOriginal(self): """Retrieve original upper bound of variable""" - return SCIPvarGetUbOriginal(self.var) + return SCIPvarGetUbOriginal(self.scip_var) def getLbGlobal(self): """Retrieve global lower bound of variable""" - return SCIPvarGetLbGlobal(self.var) + return SCIPvarGetLbGlobal(self.scip_var) def getUbGlobal(self): """Retrieve global upper bound of variable""" - return SCIPvarGetUbGlobal(self.var) + return SCIPvarGetUbGlobal(self.scip_var) def getLbLocal(self): """Retrieve current lower bound of variable""" - return SCIPvarGetLbLocal(self.var) + return SCIPvarGetLbLocal(self.scip_var) def getUbLocal(self): """Retrieve current upper bound of variable""" - return SCIPvarGetUbLocal(self.var) + return SCIPvarGetUbLocal(self.scip_var) def getObj(self): """Retrieve current objective value of variable""" - return SCIPvarGetObj(self.var) + return SCIPvarGetObj(self.scip_var) def getLPSol(self): """Retrieve the current LP solution value of variable""" - return SCIPvarGetLPSol(self.var) + return SCIPvarGetLPSol(self.scip_var) cdef class Constraint: - cdef SCIP_CONS* cons - cdef public object data #storage for python user + cdef SCIP_CONS* scip_cons + # can be used to store problem data + cdef public object data @staticmethod cdef create(SCIP_CONS* scipcons): if scipcons == NULL: raise Warning("cannot create Constraint with SCIP_CONS* == NULL") cons = Constraint() - cons.cons = scipcons + cons.scip_cons = scipcons return cons property name: def __get__(self): - cname = bytes( SCIPconsGetName(self.cons) ) + cname = bytes( SCIPconsGetName(self.scip_cons) ) return cname.decode('utf-8') def __repr__(self): @@ -418,63 +566,74 @@ cdef class Constraint: def isOriginal(self): """Retrieve whether the constraint belongs to the original problem""" - return SCIPconsIsOriginal(self.cons) + return SCIPconsIsOriginal(self.scip_cons) def isInitial(self): """Retrieve True if the relaxation of the constraint should be in the initial LP""" - return SCIPconsIsInitial(self.cons) + return SCIPconsIsInitial(self.scip_cons) def isSeparated(self): """Retrieve True if constraint should be separated during LP processing""" - return SCIPconsIsSeparated(self.cons) + return SCIPconsIsSeparated(self.scip_cons) def isEnforced(self): """Retrieve True if constraint should be enforced during node processing""" - return SCIPconsIsEnforced(self.cons) + return SCIPconsIsEnforced(self.scip_cons) def isChecked(self): """Retrieve True if constraint should be checked for feasibility""" - return SCIPconsIsChecked(self.cons) + return SCIPconsIsChecked(self.scip_cons) def isPropagated(self): """Retrieve True if constraint should be propagated during node processing""" - return SCIPconsIsPropagated(self.cons) + return SCIPconsIsPropagated(self.scip_cons) def isLocal(self): """Retrieve True if constraint is only locally valid or not added to any (sub)problem""" - return SCIPconsIsLocal(self.cons) + return SCIPconsIsLocal(self.scip_cons) def isModifiable(self): """Retrieve True if constraint is modifiable (subject to column generation)""" - return SCIPconsIsModifiable(self.cons) + return SCIPconsIsModifiable(self.scip_cons) def isDynamic(self): """Retrieve True if constraint is subject to aging""" - return SCIPconsIsDynamic(self.cons) + return SCIPconsIsDynamic(self.scip_cons) def isRemovable(self): """Retrieve True if constraint's relaxation should be removed from the LP due to aging or cleanup""" - return SCIPconsIsRemovable(self.cons) + return SCIPconsIsRemovable(self.scip_cons) def isStickingAtNode(self): """Retrieve True if constraint is only locally valid or not added to any (sub)problem""" - return SCIPconsIsStickingAtNode(self.cons) + return SCIPconsIsStickingAtNode(self.scip_cons) def isLinear(self): """Retrieve True if constraint is linear""" - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype == 'linear' def isQuadratic(self): """Retrieve True if constraint is quadratic""" - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8') return constype == 'quadratic' + +cdef void relayMessage(SCIP_MESSAGEHDLR *messagehdlr, FILE *file, const char *msg): + sys.stdout.write(msg.decode('UTF-8')) + +cdef void relayErrorMessage(void *messagehdlr, FILE *file, const char *msg): + sys.stderr.write(msg.decode('UTF-8')) + # - remove create(), includeDefaultPlugins(), createProbBasic() methods # - replace free() by "destructor" # - interface SCIPfreeProb() +## +#@anchor Model +## cdef class Model: cdef SCIP* _scip + cdef SCIP_Bool* _valid # store best solution to get the solution values easier cdef Solution _bestSol # can be used to store problem data @@ -482,20 +641,33 @@ cdef class Model: # make Model weak referentiable cdef object __weakref__ - def __init__(self, problemName='model', defaultPlugins=True): + def __init__(self, problemName='model', defaultPlugins=True, Model sourceModel=None, origcopy=False, globalcopy=True, enablepricing=False): """ :param problemName: name of the problem (default 'model') :param defaultPlugins: use default plugins? (default True) + :param sourceModel: create a copy of the given Model instance (default None) + :param origcopy: whether to call copy or copyOrig (default False) + :param globalcopy: whether to create a global or a local copy (default True) + :param enablepricing: whether to enable pricing in copy (default False) """ if self.version() < MAJOR: raise Exception("linked SCIP is not compatible to this version of PySCIPOpt - use at least version", MAJOR) if self.version() < MAJOR + MINOR/10.0 + PATCH/100.0: warnings.warn("linked SCIP {} is not recommended for this version of PySCIPOpt - use version {}.{}.{}".format(self.version(), MAJOR, MINOR, PATCH)) - self.create() - self._bestSol = None - if defaultPlugins: - self.includeDefaultPlugins() - self.createProbBasic(problemName) + if sourceModel is None: + self.create() + self._bestSol = None + if defaultPlugins: + self.includeDefaultPlugins() + self.createProbBasic(problemName) + else: + self.create() + self._bestSol = sourceModel._bestSol + n = str_conversion(problemName) + if origcopy: + PY_SCIP_CALL(SCIPcopyOrig(sourceModel._scip, self._scip, NULL, NULL, n, enablepricing, True, self._valid)) + else: + PY_SCIP_CALL(SCIPcopy(sourceModel._scip, self._scip, NULL, NULL, n, globalcopy, enablepricing, True, self._valid)) def __dealloc__(self): # call C function directly, because we can no longer call this object's methods, according to @@ -583,6 +755,46 @@ cdef class Model: """Retrieve feasibility tolerance""" return SCIPfeastol(self._scip) + def feasFrac(self, value): + """returns fractional part of value, i.e. x - floor(x) in feasible tolerance: x - floor(x+feastol)""" + return SCIPfeasFrac(self._scip, value) + + def frac(self, value): + """returns fractional part of value, i.e. x - floor(x) in epsilon tolerance: x - floor(x+eps)""" + return SCIPfrac(self._scip, value) + + def isZero(self, value): + """returns whether abs(value) < eps""" + return SCIPisZero(self._scip, value) + + def isFeasZero(self, value): + """returns whether abs(value) < feastol""" + return SCIPisFeasZero(self._scip, value) + + def isInfinity(self, value): + """returns whether value is SCIP's infinity""" + return SCIPisInfinity(self._scip, value) + + def isFeasNegative(self, value): + """returns whether value < -feastol""" + return SCIPisFeasNegative(self._scip, value) + + def isLE(self, val1, val2): + """returns whether val1 <= val2 + eps""" + return SCIPisLE(self._scip, val1, val2) + + def isLT(self, val1, val2): + """returns whether val1 < val2 - eps""" + return SCIPisLT(self._scip, val1, val2) + + def isGE(self, val1, val2): + """returns whether val1 >= val2 - eps""" + return SCIPisGE(self._scip, val1, val2) + + def isGT(self, val1, val2): + """returns whether val1 > val2 + eps""" + return SCIPisGT(self._scip, val1, val2) + def getCondition(self, exact=False): """Get the current LP's condition number @@ -618,6 +830,10 @@ cdef class Model: """ PY_SCIP_CALL(SCIPsetObjlimit(self._scip, objlimit)) + def getObjlimit(self): + """returns current limit on objective function.""" + return SCIPgetObjlimit(self._scip) + def setObjective(self, coeffs, sense = 'minimize', clear = 'true'): """Establish the objective function as a linear expression. @@ -628,7 +844,11 @@ cdef class Model: """ cdef SCIP_VAR** _vars cdef int _nvars - assert isinstance(coeffs, Expr) + + # turn the constant value into an Expr instance for further processing + if not isinstance(coeffs, Expr): + assert(_is_number(coeffs)), "given coefficients are neither Expr or number but %s" % coeffs.__class__.__name__ + coeffs = Expr() + coeffs if coeffs.degree() > 1: raise ValueError("Nonlinear objective functions are not supported!") @@ -647,7 +867,7 @@ cdef class Model: if term != CONST: assert len(term) == 1 var = term[0] - PY_SCIP_CALL(SCIPchgVarObj(self._scip, var.var, coef)) + PY_SCIP_CALL(SCIPchgVarObj(self._scip, var.scip_var, coef)) if sense == "minimize": self.setMinimize() @@ -740,19 +960,19 @@ cdef class Model: fn = str_conversion(filename) fn, ext = splitext(fn) if len(ext) == 0: - filename += '.cip' ext = str_conversion('.cip') + fn = fn + ext ext = ext[1:] if trans: PY_SCIP_CALL(SCIPwriteTransProblem(self._scip, fn, ext, False)) else: PY_SCIP_CALL(SCIPwriteOrigProblem(self._scip, fn, ext, False)) - print('wrote problem to file ' + filename) + print('wrote problem to file ' + str(fn)) # Variable Functions def addVar(self, name='', vtype='C', lb=0.0, ub=None, obj=0.0, pricedVar = False): - """Create a new variable. + """Create a new variable. Default variable is non-negative and continuous. :param name: name of the variable, generic if empty (Default value = '') :param vtype: type of the variable (Default value = 'C') @@ -773,6 +993,7 @@ cdef class Model: if lb is None: lb = -SCIPinfinity(self._scip) cdef SCIP_VAR* scip_var + vtype = vtype.upper() if vtype in ['C', 'CONTINUOUS']: PY_SCIP_CALL(SCIPcreateVarBasic(self._scip, &scip_var, cname, lb, ub, obj, SCIP_VARTYPE_CONTINUOUS)) elif vtype in ['B', 'BINARY']: @@ -790,6 +1011,9 @@ cdef class Model: PY_SCIP_CALL(SCIPaddVar(self._scip, scip_var)) pyVar = Variable.create(scip_var) + + #setting the variable data + SCIPvarSetData(scip_var, pyVar) PY_SCIP_CALL(SCIPreleaseVar(self._scip, &scip_var)) return pyVar @@ -799,7 +1023,7 @@ cdef class Model: :param Variable var: variable to be released """ - PY_SCIP_CALL(SCIPreleaseVar(self._scip, &var.var)) + PY_SCIP_CALL(SCIPreleaseVar(self._scip, &var.scip_var)) def getTransformedVar(self, Variable var): """Retrieve the transformed variable. @@ -808,7 +1032,7 @@ cdef class Model: """ cdef SCIP_VAR* _tvar - PY_SCIP_CALL(SCIPtransformVar(self._scip, var.var, &_tvar)) + PY_SCIP_CALL(SCIPtransformVar(self._scip, var.scip_var, &_tvar)) return Variable.create(_tvar) def addVarLocks(self, Variable var, nlocksdown, nlocksup): @@ -819,7 +1043,97 @@ cdef class Model: :param nlocksup: new number of up locks """ - PY_SCIP_CALL(SCIPaddVarLocks(self._scip, var.var, nlocksdown, nlocksup)) + PY_SCIP_CALL(SCIPaddVarLocks(self._scip, var.scip_var, nlocksdown, nlocksup)) + + def fixVar(self, Variable var, val): + """Fixes the variable var to the value val if possible. + + :param Variable var: variable to fix + :param val: float, the fix value + :return: tuple (infeasible, fixed) of booleans + + """ + cdef SCIP_Bool infeasible + cdef SCIP_Bool fixed + PY_SCIP_CALL(SCIPfixVar(self._scip, var.scip_var, val, &infeasible, &fixed)) + return infeasible, fixed + + def delVar(self, Variable var): + """Delete a variable. + + :param var: the variable which shall be deleted + :return: bool, was deleting succesful + + """ + cdef SCIP_Bool deleted + PY_SCIP_CALL(SCIPdelVar(self._scip, var.scip_var, &deleted)) + return deleted + + def tightenVarLb(self, Variable var, lb, force=False): + """Tighten the lower bound in preprocessing or current node, if the bound is tighter. + + :param var: SCIP variable + :param lb: possible new lower bound + :param force: force tightening even if below bound strengthening tolerance + :return: tuple of bools, (infeasible, tightened) + infeasible: whether new domain is empty + tightened: whether the bound was tightened + + """ + cdef SCIP_Bool infeasible + cdef SCIP_Bool tightened + PY_SCIP_CALL(SCIPtightenVarLb(self._scip, var.scip_var, lb, force, &infeasible, &tightened)) + return infeasible, tightened + + + def tightenVarUb(self, Variable var, ub, force=False): + """Tighten the upper bound in preprocessing or current node, if the bound is tighter. + + :param var: SCIP variable + :param ub: possible new upper bound + :param force: force tightening even if below bound strengthening tolerance + :return: tuple of bools, (infeasible, tightened) + infeasible: whether new domain is empty + tightened: whether the bound was tightened + + """ + cdef SCIP_Bool infeasible + cdef SCIP_Bool tightened + PY_SCIP_CALL(SCIPtightenVarUb(self._scip, var.scip_var, ub, force, &infeasible, &tightened)) + return infeasible, tightened + + + def tightenVarUbGlobal(self, Variable var, ub, force=False): + """Tighten the global upper bound, if the bound is tighter. + + :param var: SCIP variable + :param ub: possible new upper bound + :param force: force tightening even if below bound strengthening tolerance + :return: tuple of bools, (infeasible, tightened) + infeasible: whether new domain is empty + tightened: whether the bound was tightened + + """ + cdef SCIP_Bool infeasible + cdef SCIP_Bool tightened + PY_SCIP_CALL(SCIPtightenVarUbGlobal(self._scip, var.scip_var, ub, force, &infeasible, &tightened)) + return infeasible, tightened + + def tightenVarLbGlobal(self, Variable var, lb, force=False): + """Tighten the global upper bound, if the bound is tighter. + + :param var: SCIP variable + :param lb: possible new upper bound + :param force: force tightening even if below bound strengthening tolerance + :return: tuple of bools, (infeasible, tightened) + infeasible: whether new domain is empty + tightened: whether the bound was tightened + + """ + cdef SCIP_Bool infeasible + cdef SCIP_Bool tightened + PY_SCIP_CALL(SCIPtightenVarLbGlobal(self._scip, var.scip_var, lb, force, &infeasible, &tightened)) + return infeasible, tightened def chgVarLb(self, Variable var, lb): """Changes the lower bound of the specified variable. @@ -830,7 +1144,7 @@ cdef class Model: """ if lb is None: lb = -SCIPinfinity(self._scip) - PY_SCIP_CALL(SCIPchgVarLb(self._scip, var.var, lb)) + PY_SCIP_CALL(SCIPchgVarLb(self._scip, var.scip_var, lb)) def chgVarUb(self, Variable var, ub): """Changes the upper bound of the specified variable. @@ -841,7 +1155,52 @@ cdef class Model: """ if ub is None: ub = SCIPinfinity(self._scip) - PY_SCIP_CALL(SCIPchgVarUb(self._scip, var.var, ub)) + PY_SCIP_CALL(SCIPchgVarUb(self._scip, var.scip_var, ub)) + + + def chgVarLbGlobal(self, Variable var, lb): + """Changes the global lower bound of the specified variable. + + :param Variable var: variable to change bound of + :param lb: new lower bound (set to None for -infinity) + + """ + if lb is None: + lb = -SCIPinfinity(self._scip) + PY_SCIP_CALL(SCIPchgVarLbGlobal(self._scip, var.scip_var, lb)) + + def chgVarUbGlobal(self, Variable var, ub): + """Changes the global upper bound of the specified variable. + + :param Variable var: variable to change bound of + :param ub: new upper bound (set to None for +infinity) + + """ + if ub is None: + ub = SCIPinfinity(self._scip) + PY_SCIP_CALL(SCIPchgVarUbGlobal(self._scip, var.scip_var, ub)) + + def chgVarLbNode(self, Node node, Variable var, lb): + """Changes the lower bound of the specified variable at the given node. + + :param Variable var: variable to change bound of + :param lb: new lower bound (set to None for -infinity) + """ + + if lb is None: + lb = -SCIPinfinity(self._scip) + PY_SCIP_CALL(SCIPchgVarLbNode(self._scip, node.scip_node, var.scip_var, lb)) + + def chgVarUbNode(self, Node node, Variable var, ub): + """Changes the upper bound of the specified variable at the given node. + + :param Variable var: variable to change bound of + :param ub: new upper bound (set to None for +infinity) + + """ + if ub is None: + ub = SCIPinfinity(self._scip) + PY_SCIP_CALL(SCIPchgVarUbNode(self._scip, node.scip_node, var.scip_var, ub)) def chgVarType(self, Variable var, vtype): """Changes the type of a variable @@ -852,11 +1211,11 @@ cdef class Model: """ cdef SCIP_Bool infeasible if vtype in ['C', 'CONTINUOUS']: - PY_SCIP_CALL(SCIPchgVarType(self._scip, var.var, SCIP_VARTYPE_CONTINUOUS, &infeasible)) + PY_SCIP_CALL(SCIPchgVarType(self._scip, var.scip_var, SCIP_VARTYPE_CONTINUOUS, &infeasible)) elif vtype in ['B', 'BINARY']: - PY_SCIP_CALL(SCIPchgVarType(self._scip, var.var, SCIP_VARTYPE_BINARY, &infeasible)) + PY_SCIP_CALL(SCIPchgVarType(self._scip, var.scip_var, SCIP_VARTYPE_BINARY, &infeasible)) elif vtype in ['I', 'INTEGER']: - PY_SCIP_CALL(SCIPchgVarType(self._scip, var.var, SCIP_VARTYPE_INTEGER, &infeasible)) + PY_SCIP_CALL(SCIPchgVarType(self._scip, var.scip_var, SCIP_VARTYPE_INTEGER, &infeasible)) else: raise Warning("unrecognized variable type") if infeasible: @@ -882,6 +1241,203 @@ cdef class Model: return [Variable.create(_vars[i]) for i in range(_nvars)] + def getNVars(self): + """Retrieve number of variables in the problems""" + return SCIPgetNVars(self._scip) + + def getNConss(self): + """Retrieve the number of constraints.""" + return SCIPgetNConss(self._scip) + + def updateNodeLowerbound(self, Node node, lb): + """if given value is larger than the node's lower bound (in transformed problem), + sets the node's lower bound to the new value + + :param node: Node, the node to update + :param newbound: float, new bound (if greater) for the node + + """ + PY_SCIP_CALL(SCIPupdateNodeLowerbound(self._scip, node.scip_node, lb)) + + # LP Methods + def getLPSolstat(self): + """Gets solution status of current LP""" + return SCIPgetLPSolstat(self._scip) + + + def constructLP(self): + """makes sure that the LP of the current node is loaded and + may be accessed through the LP information methods + + :return: bool cutoff, i.e. can the node be cut off? + + """ + cdef SCIP_Bool cutoff + PY_SCIP_CALL(SCIPconstructLP(self._scip, &cutoff)) + return cutoff + + def getLPObjVal(self): + """gets objective value of current LP (which is the sum of column and loose objective value)""" + + return SCIPgetLPObjval(self._scip) + + def getLPColsData(self): + """Retrieve current LP columns""" + cdef SCIP_COL** cols + cdef int ncols + + PY_SCIP_CALL(SCIPgetLPColsData(self._scip, &cols, &ncols)) + return [Column.create(cols[i]) for i in range(ncols)] + + def getLPRowsData(self): + """Retrieve current LP rows""" + cdef SCIP_ROW** rows + cdef int nrows + + PY_SCIP_CALL(SCIPgetLPRowsData(self._scip, &rows, &nrows)) + return [Row.create(rows[i]) for i in range(nrows)] + + def getNLPRows(self): + """Retrieve the number of rows currently in the LP""" + return SCIPgetNLPRows(self._scip) + + def getNLPCols(self): + """Retrieve the number of cols currently in the LP""" + return SCIPgetNLPCols(self._scip) + + def getLPBasisInd(self): + """Gets all indices of basic columns and rows: index i >= 0 corresponds to column i, index i < 0 to row -i-1""" + cdef int nrows = SCIPgetNLPRows(self._scip) + cdef int* inds = malloc(nrows * sizeof(int)) + + PY_SCIP_CALL(SCIPgetLPBasisInd(self._scip, inds)) + result = [inds[i] for i in range(nrows)] + free(inds) + return result + + def getLPBInvRow(self, row): + """gets a row from the inverse basis matrix B^-1""" + # TODO: sparsity information + cdef int nrows = SCIPgetNLPRows(self._scip) + cdef SCIP_Real* coefs = malloc(nrows * sizeof(SCIP_Real)) + + PY_SCIP_CALL(SCIPgetLPBInvRow(self._scip, row, coefs, NULL, NULL)) + result = [coefs[i] for i in range(nrows)] + free(coefs) + return result + + def getLPBInvARow(self, row): + """gets a row from B^-1 * A""" + # TODO: sparsity information + cdef int ncols = SCIPgetNLPCols(self._scip) + cdef SCIP_Real* coefs = malloc(ncols * sizeof(SCIP_Real)) + + PY_SCIP_CALL(SCIPgetLPBInvARow(self._scip, row, NULL, coefs, NULL, NULL)) + result = [coefs[i] for i in range(ncols)] + free(coefs) + return result + + def isLPSolBasic(self): + """returns whether the current LP solution is basic, i.e. is defined by a valid simplex basis""" + return SCIPisLPSolBasic(self._scip) + + #TODO: documentation!! + # LP Row Methods + def createEmptyRowSepa(self, Sepa sepa, name="row", lhs = 0.0, rhs = None, local = True, modifiable = False, removable = True): + """creates and captures an LP row without any coefficients from a separator + + :param sepa: separator that creates the row + :param name: name of row (Default value = "row") + :param lhs: left hand side of row (Default value = 0) + :param rhs: right hand side of row (Default value = None) + :param local: is row only valid locally? (Default value = True) + :param modifiable: is row modifiable during node processing (subject to column generation)? (Default value = False) + :param removable: should the row be removed from the LP due to aging or cleanup? (Default value = True) + """ + cdef SCIP_ROW* row + lhs = -SCIPinfinity(self._scip) if lhs is None else lhs + rhs = SCIPinfinity(self._scip) if rhs is None else rhs + scip_sepa = SCIPfindSepa(self._scip, str_conversion(sepa.name)) + PY_SCIP_CALL(SCIPcreateEmptyRowSepa(self._scip, &row, scip_sepa, str_conversion(name), lhs, rhs, local, modifiable, removable)) + PyRow = Row.create(row) + return PyRow + + def createEmptyRowUnspec(self, name="row", lhs = 0.0, rhs = None, local = True, modifiable = False, removable = True): + """creates and captures an LP row without any coefficients from an unspecified source + + :param name: name of row (Default value = "row") + :param lhs: left hand side of row (Default value = 0) + :param rhs: right hand side of row (Default value = None) + :param local: is row only valid locally? (Default value = True) + :param modifiable: is row modifiable during node processing (subject to column generation)? (Default value = False) + :param removable: should the row be removed from the LP due to aging or cleanup? (Default value = True) + """ + cdef SCIP_ROW* row + lhs = -SCIPinfinity(self._scip) if lhs is None else lhs + rhs = SCIPinfinity(self._scip) if rhs is None else rhs + PY_SCIP_CALL(SCIPcreateEmptyRowUnspec(self._scip, &row, str_conversion(name), lhs, rhs, local, modifiable, removable)) + PyRow = Row.create(row) + return PyRow + + def getRowActivity(self, Row row): + """returns the activity of a row in the last LP or pseudo solution""" + return SCIPgetRowActivity(self._scip, row.scip_row) + + def getRowLPActivity(self, Row row): + """returns the activity of a row in the last LP solution""" + return SCIPgetRowLPActivity(self._scip, row.scip_row) + + # TODO: do we need this? (also do we need release var??) + def releaseRow(self, Row row not None): + """decreases usage counter of LP row, and frees memory if necessary""" + PY_SCIP_CALL(SCIPreleaseRow(self._scip, &row.scip_row)) + + def cacheRowExtensions(self, Row row not None): + """informs row, that all subsequent additions of variables to the row should be cached and not directly applied; + after all additions were applied, flushRowExtensions() must be called; + while the caching of row extensions is activated, information methods of the row give invalid results; + caching should be used, if a row is build with addVarToRow() calls variable by variable to increase the performance""" + PY_SCIP_CALL(SCIPcacheRowExtensions(self._scip, row.scip_row)) + + def flushRowExtensions(self, Row row not None): + """flushes all cached row extensions after a call of cacheRowExtensions() and merges coefficients with equal columns into a single coefficient""" + PY_SCIP_CALL(SCIPflushRowExtensions(self._scip, row.scip_row)) + + def addVarToRow(self, Row row not None, Variable var not None, value): + """resolves variable to columns and adds them with the coefficient to the row""" + PY_SCIP_CALL(SCIPaddVarToRow(self._scip, row.scip_row, var.scip_var, value)) + + def printRow(self, Row row not None): + """Prints row.""" + PY_SCIP_CALL(SCIPprintRow(self._scip, row.scip_row, NULL)) + + # Cutting Plane Methods + def addPoolCut(self, Row row not None): + """if not already existing, adds row to global cut pool""" + PY_SCIP_CALL(SCIPaddPoolCut(self._scip, row.scip_row)) + + def getCutEfficacy(self, Row cut not None, Solution sol = None): + """returns efficacy of the cut with respect to the given primal solution or the current LP solution: e = -feasibility/norm""" + return SCIPgetCutEfficacy(self._scip, NULL if sol is None else sol.sol, cut.scip_row) + + def isCutEfficacious(self, Row cut not None, Solution sol = None): + """ returns whether the cut's efficacy with respect to the given primal solution or the current LP solution is greater than the minimal cut efficacy""" + return SCIPisCutEfficacious(self._scip, NULL if sol is None else sol.sol, cut.scip_row) + + def addCut(self, Row cut not None, forcecut = False): + """adds cut to separation storage and returns whether cut has been detected to be infeasible for local bounds""" + cdef SCIP_Bool infeasible + PY_SCIP_CALL(SCIPaddRow(self._scip, cut.scip_row, forcecut, &infeasible)) + return infeasible + + def getNCuts(self): + """Retrieve total number of cuts in storage""" + return SCIPgetNCuts(self._scip) + + def getNCutsApplied(self): + """Retrieve number of currently applied cuts""" + return SCIPgetNCutsApplied(self._scip) + # Constraint functions def addCons(self, cons, name='', initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, @@ -903,7 +1459,7 @@ cdef class Model: :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) """ - assert isinstance(cons, ExprCons) + assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ # replace empty name with generic one if name == '': @@ -915,8 +1471,8 @@ cdef class Model: modifiable=modifiable, dynamic=dynamic, removable=removable, stickingatnode=stickingatnode) - kwargs['lhs'] = -SCIPinfinity(self._scip) if cons.lhs is None else cons.lhs - kwargs['rhs'] = SCIPinfinity(self._scip) if cons.rhs is None else cons.rhs + kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs + kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs deg = cons.expr.degree() if deg <= 1: @@ -929,9 +1485,9 @@ cdef class Model: return self._addNonlinearCons(cons, **kwargs) def _addLinCons(self, ExprCons lincons, **kwargs): - assert isinstance(lincons, ExprCons) + assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__ - assert lincons.expr.degree() <= 1 + assert lincons.expr.degree() <= 1, "given constraint is not linear, degree == %d" % lincons.expr.degree() terms = lincons.expr.terms cdef SCIP_CONS* scip_cons @@ -944,7 +1500,7 @@ cdef class Model: for key, coeff in terms.items(): var = key[0] - PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, scip_cons, var.var, coeff)) + PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, scip_cons, var.scip_var, coeff)) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) PyCons = Constraint.create(scip_cons) @@ -954,7 +1510,7 @@ cdef class Model: def _addQuadCons(self, ExprCons quadcons, **kwargs): terms = quadcons.expr.terms - assert quadcons.expr.degree() <= 2 + assert quadcons.expr.degree() <= 2, "given constraint is not quadratic, degree == %d" % quadcons.expr.degree() cdef SCIP_CONS* scip_cons PY_SCIP_CALL(SCIPcreateConsQuadratic( @@ -969,11 +1525,11 @@ cdef class Model: for v, c in terms.items(): if len(v) == 1: # linear var = v[0] - PY_SCIP_CALL(SCIPaddLinearVarQuadratic(self._scip, scip_cons, var.var, c)) + PY_SCIP_CALL(SCIPaddLinearVarQuadratic(self._scip, scip_cons, var.scip_var, c)) else: # quadratic - assert len(v) == 2, 'term: %s' % v + assert len(v) == 2, 'term length must be 1 or 2 but it is %s' % len(v) var1, var2 = v[0], v[1] - PY_SCIP_CALL(SCIPaddBilinTermQuadratic(self._scip, scip_cons, var1.var, var2.var, c)) + PY_SCIP_CALL(SCIPaddBilinTermQuadratic(self._scip, scip_cons, var1.scip_var, var2.scip_var, c)) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) PyCons = Constraint.create(scip_cons) @@ -1020,7 +1576,7 @@ cdef class Model: PY_SCIP_CALL( SCIPexprtreeCreate(SCIPblkmem(self._scip), &exprtree, expr, len(variables), 0, NULL) ) vars = malloc(len(variables) * sizeof(SCIP_VAR*)) for idx, var in enumerate(variables): # same as varindex - vars[idx] = (var).var + vars[idx] = (var).scip_var PY_SCIP_CALL( SCIPexprtreeSetVars(exprtree, len(variables), vars) ) # create nonlinear constraint for exprtree @@ -1077,7 +1633,7 @@ cdef class Model: assert len(node[1]) == 1 pyvar = node[1][0] # for vars we store the actual var! PY_SCIP_CALL( SCIPexprCreate(SCIPblkmem(self._scip), &scipexprs[i], opidx, varpos) ) - vars[varpos] = (pyvar).var + vars[varpos] = (pyvar).scip_var varpos += 1 continue if opidx == SCIP_EXPR_CONST: @@ -1132,6 +1688,7 @@ cdef class Model: PY_SCIP_CALL( SCIPexprtreeFree(&exprtree) ) # free more memory + free(scipexprs) free(vars) return PyCons @@ -1144,7 +1701,32 @@ cdef class Model: :param coeff: coefficient of new variable """ - PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, cons.cons, var.var, coeff)) + PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, coeff)) + + def addConsNode(self, Node node, Constraint cons, Node validnode=None): + """Add a constraint to the given node + + :param Node node: node to add the constraint to + :param Constraint cons: constraint to add + :param Node validnode: more global node where cons is also valid + + """ + if isinstance(validnode, Node): + PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, validnode.scip_node)) + else: + PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, NULL)) + + def addConsLocal(self, Constraint cons, Node validnode=None): + """Add a constraint to the current node + + :param Constraint cons: constraint to add + :param Node validnode: more global node where cons is also valid + + """ + if isinstance(validnode, Node): + PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, validnode.scip_node)) + else: + PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, NULL)) def addConsSOS1(self, vars, weights=None, name="SOS1cons", initial=True, separate=True, enforce=True, check=True, @@ -1175,12 +1757,12 @@ cdef class Model: if weights is None: for v in vars: var = v - PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, scip_cons, var.var)) + PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, scip_cons, var.scip_var)) else: nvars = len(vars) for i in range(nvars): var = vars[i] - PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, scip_cons, var.var, weights[i])) + PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, scip_cons, var.scip_var, weights[i])) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) return Constraint.create(scip_cons) @@ -1214,16 +1796,133 @@ cdef class Model: if weights is None: for v in vars: var = v - PY_SCIP_CALL(SCIPappendVarSOS2(self._scip, scip_cons, var.var)) + PY_SCIP_CALL(SCIPappendVarSOS2(self._scip, scip_cons, var.scip_var)) else: nvars = len(vars) for i in range(nvars): var = vars[i] - PY_SCIP_CALL(SCIPaddVarSOS2(self._scip, scip_cons, var.var, weights[i])) + PY_SCIP_CALL(SCIPaddVarSOS2(self._scip, scip_cons, var.scip_var, weights[i])) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) return Constraint.create(scip_cons) + def addConsAnd(self, vars, resvar, name="ANDcons", + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=False, modifiable=False, dynamic=False, + removable=False, stickingatnode=False): + """Add an AND-constraint. + :param vars: list of BINARY variables to be included (operators) + :param resvar: BINARY variable (resultant) + :param name: name of the constraint (Default value = "ANDcons") + :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) + :param separate: should the constraint be separated during LP processing? (Default value = True) + :param enforce: should the constraint be enforced during node processing? (Default value = True) + :param check: should the constraint be checked for feasibility? (Default value = True) + :param propagate: should the constraint be propagated during node processing? (Default value = True) + :param local: is the constraint only valid locally? (Default value = False) + :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) + :param dynamic: is the constraint subject to aging? (Default value = False) + :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + """ + cdef SCIP_CONS* scip_cons + + nvars = len(vars) + + _vars = malloc(len(vars) * sizeof(SCIP_VAR*)) + for idx, var in enumerate(vars): + _vars[idx] = (var).scip_var + _resVar = (resvar).scip_var + + PY_SCIP_CALL(SCIPcreateConsAnd(self._scip, &scip_cons, str_conversion(name), _resVar, nvars, _vars, + initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + pyCons = Constraint.create(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + free(_vars) + + return pyCons + + def addConsOr(self, vars, resvar, name="ORcons", + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=False, modifiable=False, dynamic=False, + removable=False, stickingatnode=False): + """Add an OR-constraint. + :param vars: list of BINARY variables to be included (operators) + :param resvar: BINARY variable (resultant) + :param name: name of the constraint (Default value = "ORcons") + :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) + :param separate: should the constraint be separated during LP processing? (Default value = True) + :param enforce: should the constraint be enforced during node processing? (Default value = True) + :param check: should the constraint be checked for feasibility? (Default value = True) + :param propagate: should the constraint be propagated during node processing? (Default value = True) + :param local: is the constraint only valid locally? (Default value = False) + :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) + :param dynamic: is the constraint subject to aging? (Default value = False) + :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + """ + cdef SCIP_CONS* scip_cons + + nvars = len(vars) + + _vars = malloc(len(vars) * sizeof(SCIP_VAR*)) + for idx, var in enumerate(vars): + _vars[idx] = (var).scip_var + _resVar = (resvar).scip_var + + PY_SCIP_CALL(SCIPcreateConsOr(self._scip, &scip_cons, str_conversion(name), _resVar, nvars, _vars, + initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + pyCons = Constraint.create(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + free(_vars) + + return pyCons + + def addConsXor(self, vars, rhsvar, name="XORcons", + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=False, modifiable=False, dynamic=False, + removable=False, stickingatnode=False): + """Add a XOR-constraint. + :param vars: list of BINARY variables to be included (operators) + :param rhsvar: BOOLEAN value, explicit True, False or bool(obj) is needed (right-hand side) + :param name: name of the constraint (Default value = "XORcons") + :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) + :param separate: should the constraint be separated during LP processing? (Default value = True) + :param enforce: should the constraint be enforced during node processing? (Default value = True) + :param check: should the constraint be checked for feasibility? (Default value = True) + :param propagate: should the constraint be propagated during node processing? (Default value = True) + :param local: is the constraint only valid locally? (Default value = False) + :param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False) + :param dynamic: is the constraint subject to aging? (Default value = False) + :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) + """ + cdef SCIP_CONS* scip_cons + + nvars = len(vars) + + assert type(rhsvar) is type(bool()), "Provide BOOLEAN value as rhsvar, you gave %s." % type(rhsvar) + _vars = malloc(len(vars) * sizeof(SCIP_VAR*)) + for idx, var in enumerate(vars): + _vars[idx] = (var).scip_var + + PY_SCIP_CALL(SCIPcreateConsXor(self._scip, &scip_cons, str_conversion(name), rhsvar, nvars, _vars, + initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) + + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + pyCons = Constraint.create(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + free(_vars) + + return pyCons + def addConsCardinality(self, consvars, cardval, indvars=None, weights=None, name="CardinalityCons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, @@ -1259,13 +1958,13 @@ cdef class Model: for i, v in enumerate(consvars): var = v if indvars: - indvar = (indvars[i]).var + indvar = (indvars[i]).scip_var else: indvar = NULL if weights is None: - PY_SCIP_CALL(SCIPappendVarCardinality(self._scip, scip_cons, var.var, indvar)) + PY_SCIP_CALL(SCIPappendVarCardinality(self._scip, scip_cons, var.scip_var, indvar)) else: - PY_SCIP_CALL(SCIPaddVarCardinality(self._scip, scip_cons, var.var, indvar, weights[i])) + PY_SCIP_CALL(SCIPaddVarCardinality(self._scip, scip_cons, var.scip_var, indvar, weights[i])) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) pyCons = Constraint.create(scip_cons) @@ -1275,7 +1974,7 @@ cdef class Model: return pyCons - def addConsIndicator(self, cons, binvar=None, name="CardinalityCons", + def addConsIndicator(self, cons, binvar=None, name="IndicatorCons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): @@ -1286,7 +1985,7 @@ cdef class Model: :param cons: a linear inequality of the form "<=" :param binvar: binary indicator variable, or None if it should be created (Default value = None) - :param name: name of the constraint (Default value = "CardinalityCons") + :param name: name of the constraint (Default value = "IndicatorCons") :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) :param separate: should the constraint be separated during LP processing? (Default value = True) :param enforce: should the constraint be enforced during node processing? (Default value = True) @@ -1298,25 +1997,24 @@ cdef class Model: :param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False) """ - assert isinstance(cons, ExprCons) + assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ cdef SCIP_CONS* scip_cons cdef SCIP_VAR* _binVar - if cons.lhs is not None and cons.rhs is not None: + if cons._lhs is not None and cons._rhs is not None: raise ValueError("expected inequality that has either only a left or right hand side") if cons.expr.degree() > 1: raise ValueError("expected linear inequality, expression has degree %d" % cons.expr.degree()) - assert cons.expr.degree() <= 1 - if cons.rhs is not None: - rhs = cons.rhs + if cons._rhs is not None: + rhs = cons._rhs negate = False else: - rhs = -cons.lhs + rhs = -cons._lhs negate = True - _binVar = (binvar).var if binvar is not None else NULL + _binVar = (binvar).scip_var if binvar is not None else NULL PY_SCIP_CALL(SCIPcreateConsIndicator(self._scip, &scip_cons, str_conversion(name), _binVar, 0, NULL, NULL, rhs, initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) @@ -1326,7 +2024,7 @@ cdef class Model: var = key[0] if negate: coeff = -coeff - PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, scip_cons, var.var, coeff)) + PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, scip_cons, var.scip_var, coeff)) PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) pyCons = Constraint.create(scip_cons) @@ -1341,7 +2039,7 @@ cdef class Model: :param Constraint cons: constraint to add """ - PY_SCIP_CALL(SCIPaddCons(self._scip, cons.cons)) + PY_SCIP_CALL(SCIPaddCons(self._scip, cons.scip_cons)) Py_INCREF(cons) def addVarSOS1(self, Constraint cons, Variable var, weight): @@ -1352,7 +2050,7 @@ cdef class Model: :param weight: weight of new variable """ - PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, cons.cons, var.var, weight)) + PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, cons.scip_cons, var.scip_var, weight)) def appendVarSOS1(self, Constraint cons, Variable var): """Append variable to SOS1 constraint. @@ -1361,7 +2059,7 @@ cdef class Model: :param Variable var: variable to append """ - PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, cons.cons, var.var)) + PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, cons.scip_cons, var.scip_var)) def addVarSOS2(self, Constraint cons, Variable var, weight): """Add variable to SOS2 constraint. @@ -1371,7 +2069,7 @@ cdef class Model: :param weight: weight of new variable """ - PY_SCIP_CALL(SCIPaddVarSOS2(self._scip, cons.cons, var.var, weight)) + PY_SCIP_CALL(SCIPaddVarSOS2(self._scip, cons.scip_cons, var.scip_var, weight)) def appendVarSOS2(self, Constraint cons, Variable var): """Append variable to SOS2 constraint. @@ -1380,7 +2078,7 @@ cdef class Model: :param Variable var: variable to append """ - PY_SCIP_CALL(SCIPappendVarSOS2(self._scip, cons.cons, var.var)) + PY_SCIP_CALL(SCIPappendVarSOS2(self._scip, cons.scip_cons, var.scip_var)) def setInitial(self, Constraint cons, newInit): """Set "initial" flag of a constraint. @@ -1389,7 +2087,7 @@ cdef class Model: cons -- constraint newInit -- new initial value """ - PY_SCIP_CALL(SCIPsetConsInitial(self._scip, cons.cons, newInit)) + PY_SCIP_CALL(SCIPsetConsInitial(self._scip, cons.scip_cons, newInit)) def setRemovable(self, Constraint cons, newRem): """Set "removable" flag of a constraint. @@ -1398,7 +2096,7 @@ cdef class Model: cons -- constraint newRem -- new removable value """ - PY_SCIP_CALL(SCIPsetConsRemovable(self._scip, cons.cons, newRem)) + PY_SCIP_CALL(SCIPsetConsRemovable(self._scip, cons.scip_cons, newRem)) def setEnforced(self, Constraint cons, newEnf): """Set "enforced" flag of a constraint. @@ -1407,7 +2105,7 @@ cdef class Model: cons -- constraint newEnf -- new enforced value """ - PY_SCIP_CALL(SCIPsetConsEnforced(self._scip, cons.cons, newEnf)) + PY_SCIP_CALL(SCIPsetConsEnforced(self._scip, cons.scip_cons, newEnf)) def setCheck(self, Constraint cons, newCheck): """Set "check" flag of a constraint. @@ -1416,7 +2114,7 @@ cdef class Model: cons -- constraint newCheck -- new check value """ - PY_SCIP_CALL(SCIPsetConsChecked(self._scip, cons.cons, newCheck)) + PY_SCIP_CALL(SCIPsetConsChecked(self._scip, cons.scip_cons, newCheck)) def chgRhs(self, Constraint cons, rhs): """Change right hand side value of a constraint. @@ -1429,11 +2127,11 @@ cdef class Model: if rhs is None: rhs = SCIPinfinity(self._scip) - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - PY_SCIP_CALL(SCIPchgRhsLinear(self._scip, cons.cons, rhs)) + PY_SCIP_CALL(SCIPchgRhsLinear(self._scip, cons.scip_cons, rhs)) elif constype == 'quadratic': - PY_SCIP_CALL(SCIPchgRhsQuadratic(self._scip, cons.cons, rhs)) + PY_SCIP_CALL(SCIPchgRhsQuadratic(self._scip, cons.scip_cons, rhs)) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1448,11 +2146,11 @@ cdef class Model: if lhs is None: lhs = -SCIPinfinity(self._scip) - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - PY_SCIP_CALL(SCIPchgLhsLinear(self._scip, cons.cons, lhs)) + PY_SCIP_CALL(SCIPchgLhsLinear(self._scip, cons.scip_cons, lhs)) elif constype == 'quadratic': - PY_SCIP_CALL(SCIPchgLhsQuadratic(self._scip, cons.cons, lhs)) + PY_SCIP_CALL(SCIPchgLhsQuadratic(self._scip, cons.scip_cons, lhs)) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1462,11 +2160,11 @@ cdef class Model: :param Constraint cons: linear or quadratic constraint """ - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - return SCIPgetRhsLinear(self._scip, cons.cons) + return SCIPgetRhsLinear(self._scip, cons.scip_cons) elif constype == 'quadratic': - return SCIPgetRhsQuadratic(self._scip, cons.cons) + return SCIPgetRhsQuadratic(self._scip, cons.scip_cons) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1476,11 +2174,11 @@ cdef class Model: :param Constraint cons: linear or quadratic constraint """ - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - return SCIPgetLhsLinear(self._scip, cons.cons) + return SCIPgetLhsLinear(self._scip, cons.scip_cons) elif constype == 'quadratic': - return SCIPgetLhsQuadratic(self._scip, cons.cons) + return SCIPgetLhsQuadratic(self._scip, cons.scip_cons) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1503,11 +2201,11 @@ cdef class Model: else: scip_sol = NULL - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - activity = SCIPgetActivityLinear(self._scip, cons.cons, scip_sol) + activity = SCIPgetActivityLinear(self._scip, cons.scip_cons, scip_sol) elif constype == 'quadratic': - PY_SCIP_CALL(SCIPgetActivityQuadratic(self._scip, cons.cons, scip_sol, &activity)) + PY_SCIP_CALL(SCIPgetActivityQuadratic(self._scip, cons.scip_cons, scip_sol, &activity)) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1536,15 +2234,15 @@ cdef class Model: else: scip_sol = NULL - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if constype == 'linear': - lhs = SCIPgetLhsLinear(self._scip, cons.cons) - rhs = SCIPgetRhsLinear(self._scip, cons.cons) - activity = SCIPgetActivityLinear(self._scip, cons.cons, scip_sol) + lhs = SCIPgetLhsLinear(self._scip, cons.scip_cons) + rhs = SCIPgetRhsLinear(self._scip, cons.scip_cons) + activity = SCIPgetActivityLinear(self._scip, cons.scip_cons, scip_sol) elif constype == 'quadratic': - lhs = SCIPgetLhsQuadratic(self._scip, cons.cons) - rhs = SCIPgetRhsQuadratic(self._scip, cons.cons) - PY_SCIP_CALL(SCIPgetActivityQuadratic(self._scip, cons.cons, scip_sol, &activity)) + lhs = SCIPgetLhsQuadratic(self._scip, cons.scip_cons) + rhs = SCIPgetRhsQuadratic(self._scip, cons.scip_cons) + PY_SCIP_CALL(SCIPgetActivityQuadratic(self._scip, cons.scip_cons, scip_sol, &activity)) else: raise Warning("method cannot be called for constraints of type " + constype) @@ -1565,7 +2263,7 @@ cdef class Model: """ cdef SCIP_CONS* transcons - PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.cons, &transcons)) + PY_SCIP_CALL(SCIPgetTransformedCons(self._scip, cons.scip_cons, &transcons)) return Constraint.create(transcons) def getTermsQuadratic(self, Constraint cons): @@ -1582,15 +2280,15 @@ cdef class Model: cdef int _nquadterms cdef int _nlinvars - assert cons.isQuadratic() + assert cons.isQuadratic(), "constraint is not quadratic" bilinterms = [] quadterms = [] linterms = [] # bilinear terms - _bilinterms = SCIPgetBilinTermsQuadratic(self._scip, cons.cons) - _nbilinterms = SCIPgetNBilinTermsQuadratic(self._scip, cons.cons) + _bilinterms = SCIPgetBilinTermsQuadratic(self._scip, cons.scip_cons) + _nbilinterms = SCIPgetNBilinTermsQuadratic(self._scip, cons.scip_cons) for i in range(_nbilinterms): var1 = Variable.create(_bilinterms[i].var1) @@ -1598,17 +2296,17 @@ cdef class Model: bilinterms.append((var1,var2,_bilinterms[i].coef)) # quadratic terms - _quadterms = SCIPgetQuadVarTermsQuadratic(self._scip, cons.cons) - _nquadterms = SCIPgetNQuadVarTermsQuadratic(self._scip, cons.cons) + _quadterms = SCIPgetQuadVarTermsQuadratic(self._scip, cons.scip_cons) + _nquadterms = SCIPgetNQuadVarTermsQuadratic(self._scip, cons.scip_cons) for i in range(_nquadterms): var = Variable.create(_quadterms[i].var) quadterms.append((var,_quadterms[i].sqrcoef,_quadterms[i].lincoef)) # linear terms - _linvars = SCIPgetLinearVarsQuadratic(self._scip, cons.cons) - _lincoefs = SCIPgetCoefsLinearVarsQuadratic(self._scip, cons.cons) - _nlinvars = SCIPgetNLinearVarsQuadratic(self._scip, cons.cons) + _linvars = SCIPgetLinearVarsQuadratic(self._scip, cons.scip_cons) + _lincoefs = SCIPgetCoefsLinearVarsQuadratic(self._scip, cons.scip_cons) + _nlinvars = SCIPgetNLinearVarsQuadratic(self._scip, cons.scip_cons) for i in range(_nlinvars): var = Variable.create(_linvars[i]) @@ -1616,10 +2314,13 @@ cdef class Model: return (bilinterms, quadterms, linterms) + def setRelaxSolVal(self, Variable var, val): + """sets the value of the given variable in the global relaxation solution""" + PY_SCIP_CALL(SCIPsetRelaxSolVal(self._scip, var.scip_var, val)) + def getConss(self): """Retrieve all constraints.""" cdef SCIP_CONS** _conss - cdef SCIP_CONS* _cons cdef int _nconss conss = [] @@ -1627,13 +2328,17 @@ cdef class Model: _nconss = SCIPgetNConss(self._scip) return [Constraint.create(_conss[i]) for i in range(_nconss)] + def getNConss(self): + """Retrieve number of all constraints""" + return SCIPgetNConss(self._scip) + def delCons(self, Constraint cons): """Delete constraint from the model :param Constraint cons: constraint to be deleted """ - PY_SCIP_CALL(SCIPdelCons(self._scip, cons.cons)) + PY_SCIP_CALL(SCIPdelCons(self._scip, cons.scip_cons)) def delConsLocal(self, Constraint cons): """Delete constraint from the current node and it's children @@ -1641,7 +2346,7 @@ cdef class Model: :param Constraint cons: constraint to be deleted """ - PY_SCIP_CALL(SCIPdelConsLocal(self._scip, cons.cons)) + PY_SCIP_CALL(SCIPdelConsLocal(self._scip, cons.scip_cons)) def getValsLinear(self, Constraint cons): """Retrieve the coefficients of a linear constraint @@ -1652,54 +2357,77 @@ cdef class Model: cdef SCIP_Real* _vals cdef SCIP_VAR** _vars - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') if not constype == 'linear': raise Warning("coefficients not available for constraints of type ", constype) - _vals = SCIPgetValsLinear(self._scip, cons.cons) - _vars = SCIPgetVarsLinear(self._scip, cons.cons) + _vals = SCIPgetValsLinear(self._scip, cons.scip_cons) + _vars = SCIPgetVarsLinear(self._scip, cons.scip_cons) valsdict = {} - for i in range(SCIPgetNVarsLinear(self._scip, cons.cons)): + for i in range(SCIPgetNVarsLinear(self._scip, cons.scip_cons)): valsdict[bytes(SCIPvarGetName(_vars[i])).decode('utf-8')] = _vals[i] return valsdict + def getDualMultiplier(self, Constraint cons): + """Retrieve the dual multiplier to a linear constraint. + + :param Constraint cons: linear constraint + + """ + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == 'linear': + raise Warning("dual solution values not available for constraints of type ", constype) + if cons.isOriginal(): + transcons = self.getTransformedCons(cons) + else: + transcons = cons + return SCIPgetDualsolLinear(self._scip, transcons.scip_cons) + def getDualsolLinear(self, Constraint cons): """Retrieve the dual solution to a linear constraint. :param Constraint cons: linear constraint """ - # TODO this should ideally be handled on the SCIP side + cdef SCIP_Real dualsolval + cdef SCIP_Bool boundconstraint cdef int _nvars cdef SCIP_VAR** _vars + cdef SCIP_Real* _vals cdef SCIP_Bool _success - dual = 0.0 - constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.cons))).decode('UTF-8') - if not constype == 'linear': - raise Warning("dual solution values not available for constraints of type ", constype) + if self.version() > 6.0: + PY_SCIP_CALL( SCIPgetDualSolVal(self._scip, cons.scip_cons, &dualsolval, &boundconstraint) ) + return dualsolval + else: + dual = 0.0 - try: - _nvars = SCIPgetNVarsLinear(self._scip, cons.cons) - if cons.isOriginal(): - transcons = self.getTransformedCons(cons) - else: - transcons = cons - dual = SCIPgetDualsolLinear(self._scip, transcons.cons) - if dual == 0.0 and _nvars == 1: - _vars = SCIPgetVarsLinear(self._scip, transcons.cons) - LPsol = SCIPvarGetLPSol(_vars[0]) - rhs = SCIPgetRhsLinear(self._scip, transcons.cons) - lhs = SCIPgetLhsLinear(self._scip, transcons.cons) - if (LPsol == rhs) or (LPsol == lhs): - dual = SCIPgetVarRedcost(self._scip, _vars[0]) + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == 'linear': + raise Warning("dual solution values not available for constraints of type ", constype) - if self.getObjectiveSense() == "maximize": - dual = -dual - except: - raise Warning("no dual solution available for constraint " + cons.name) - return dual + try: + _nvars = SCIPgetNVarsLinear(self._scip, cons.scip_cons) + if cons.isOriginal(): + transcons = self.getTransformedCons(cons) + else: + transcons = cons + dual = SCIPgetDualsolLinear(self._scip, transcons.scip_cons) + if dual == 0.0 and _nvars == 1: + _vars = SCIPgetVarsLinear(self._scip, transcons.scip_cons) + _vals = SCIPgetValsLinear(self._scip, transcons.scip_cons) + activity = SCIPvarGetLPSol(_vars[0]) * _vals[0] + rhs = SCIPgetRhsLinear(self._scip, transcons.scip_cons) + lhs = SCIPgetLhsLinear(self._scip, transcons.scip_cons) + if (activity == rhs) or (activity == lhs): + dual = SCIPgetVarRedcost(self._scip, _vars[0]) + + if self.getObjectiveSense() == "maximize" and not dual == 0.0: + dual = -dual + except: + raise Warning("no dual solution available for constraint " + cons.name) + return dual def getDualfarkasLinear(self, Constraint cons): """Retrieve the dual farkas value to a linear constraint. @@ -1710,9 +2438,9 @@ cdef class Model: # TODO this should ideally be handled on the SCIP side if cons.isOriginal(): transcons = self.getTransformedCons(cons) - return SCIPgetDualfarkasLinear(self._scip, transcons.cons) + return SCIPgetDualfarkasLinear(self._scip, transcons.scip_cons) else: - return SCIPgetDualfarkasLinear(self._scip, cons.cons) + return SCIPgetDualfarkasLinear(self._scip, cons.scip_cons) def getVarRedcost(self, Variable var): """Retrieve the reduced cost of a variable. @@ -1722,7 +2450,7 @@ cdef class Model: """ redcost = None try: - redcost = SCIPgetVarRedcost(self._scip, var.var) + redcost = SCIPgetVarRedcost(self._scip, var.scip_var) if self.getObjectiveSense() == "maximize": redcost = -redcost except: @@ -1734,6 +2462,221 @@ cdef class Model: PY_SCIP_CALL(SCIPsolve(self._scip)) self._bestSol = Solution.create(SCIPgetBestSol(self._scip)) + def presolve(self): + """Presolve the problem.""" + PY_SCIP_CALL(SCIPpresolve(self._scip)) + + # Benders' decomposition methods + def initBendersDefault(self, subproblems): + """initialises the default Benders' decomposition with a dictionary of subproblems + + Keyword arguments: + subproblems -- a single Model instance or dictionary of Model instances + """ + cdef SCIP** subprobs + cdef SCIP_BENDERS* benders + + # checking whether subproblems is a dictionary + if isinstance(subproblems, dict): + isdict = True + nsubproblems = len(subproblems) + else: + isdict = False + nsubproblems = 1 + + # create array of SCIP instances for the subproblems + subprobs = malloc(nsubproblems * sizeof(SCIP*)) + + # if subproblems is a dictionary, then the dictionary is turned into a c array + if isdict: + for idx, subprob in enumerate(subproblems.values()): + subprobs[idx] = (subprob)._scip + else: + subprobs[0] = (subproblems)._scip + + # creating the default Benders' decomposition + PY_SCIP_CALL(SCIPcreateBendersDefault(self._scip, subprobs, nsubproblems)) + benders = SCIPfindBenders(self._scip, "default") + + # activating the Benders' decomposition constraint handlers + self.setBoolParam("constraints/benderslp/active", True) + self.setBoolParam("constraints/benders/active", True) + #self.setIntParam("limits/maxorigsol", 0) + + def computeBestSolSubproblems(self): + """Solves the subproblems with the best solution to the master problem. + Afterwards, the best solution from each subproblem can be queried to get + the solution to the original problem. + + If the user wants to resolve the subproblems, they must free them by + calling freeBendersSubproblems() + """ + cdef SCIP_BENDERS** _benders + cdef SCIP_Bool _infeasible + cdef int nbenders + cdef int nsubproblems + + solvecip = True + + nbenders = SCIPgetNActiveBenders(self._scip) + _benders = SCIPgetBenders(self._scip) + + # solving all subproblems from all Benders' decompositions + for i in range(nbenders): + nsubproblems = SCIPbendersGetNSubproblems(_benders[i]) + for j in range(nsubproblems): + PY_SCIP_CALL(SCIPsetupBendersSubproblem(self._scip, + _benders[i], self._bestSol.sol, j)) + PY_SCIP_CALL(SCIPsolveBendersSubproblem(self._scip, + _benders[i], self._bestSol.sol, j, &_infeasible, + SCIP_BENDERSENFOTYPE_CHECK, solvecip, NULL)) + + def freeBendersSubproblems(self): + """Calls the free subproblem function for the Benders' decomposition. + This will free all subproblems for all decompositions. + """ + cdef SCIP_BENDERS** _benders + cdef int nbenders + cdef int nsubproblems + + nbenders = SCIPgetNActiveBenders(self._scip) + _benders = SCIPgetBenders(self._scip) + + # solving all subproblems from all Benders' decompositions + for i in range(nbenders): + nsubproblems = SCIPbendersGetNSubproblems(_benders[i]) + for j in range(nsubproblems): + PY_SCIP_CALL(SCIPfreeBendersSubproblem(self._scip, _benders[i], + j)) + + def updateBendersLowerbounds(self, lowerbounds, Benders benders=None): + """"updates the subproblem lower bounds for benders using + the lowerbounds dict. If benders is None, then the default + Benders' decomposition is updated + """ + cdef SCIP_BENDERS* _benders + + assert type(lowerbounds) is dict + + if benders is None: + _benders = SCIPfindBenders(self._scip, "default") + else: + n = str_conversion(benders.name) + _benders = SCIPfindBenders(self._scip, n) + + for d in lowerbounds.keys(): + SCIPbendersUpdateSubproblemLowerbound(_benders, d, lowerbounds[d]) + + def activateBenders(self, str name, int nsubproblems): + """Activates the Benders' decomposition plugin with the input name + + Keyword arguments: + name -- the name of the Benders' decomposition plugin + nsubproblems -- the number of subproblems in the Benders' decomposition + """ + n = str_conversion(name) + cdef SCIP_BENDERS* benders + benders = SCIPfindBenders(self._scip, n) + PY_SCIP_CALL(SCIPactivateBenders(self._scip, benders, nsubproblems)) + + def addBendersSubproblem(self, str name, subproblem): + """adds a subproblem to the Benders' decomposition given by the input + name. + + Keyword arguments: + name -- the Benders' decomposition that the subproblem is added to + subproblem -- the subproblem to add to the decomposition + """ + cdef SCIP_BENDERS* benders + n = str_conversion(name) + benders = SCIPfindBenders(self._scip, n) + PY_SCIP_CALL(SCIPaddBendersSubproblem(self._scip, benders, (subproblem)._scip)) + + def getBendersVar(self, Variable var, Benders benders = None, probnumber = -1): + """Returns the variable for the subproblem or master problem + depending on the input probnumber + + Keyword arguments: + var -- the source variable for which the target variable is requested + benders -- the Benders' decomposition to which the subproblem variables belong to + probnumber -- the problem number for which the target variable belongs, -1 for master problem + """ + cdef SCIP_BENDERS* _benders + cdef SCIP_VAR* _mappedvar + + if benders is None: + _benders = SCIPfindBenders(self._scip, "default") + else: + n = str_conversion(benders.name) + _benders = SCIPfindBenders(self._scip, n) + + if probnumber == -1: + PY_SCIP_CALL(SCIPgetBendersMasterVar(self._scip, _benders, var.scip_var, &_mappedvar)) + else: + PY_SCIP_CALL(SCIPgetBendersSubproblemVar(self._scip, _benders, var.scip_var, &_mappedvar, probnumber)) + + if _mappedvar == NULL: + mappedvar = None + else: + mappedvar = Variable.create(_mappedvar) + + return mappedvar + + def setupBendersSubproblem(self, probnumber, Benders benders = None, Solution solution = None): + """ sets up the Benders' subproblem given the master problem solution + Keyword arguments: + probnumber -- the index of the problem that is to be set up + benders -- the Benders' decomposition to which the subproblem belongs to + solution -- the master problem solution that is used for the set up, if None, then the LP solution is used + """ + cdef SCIP_BENDERS* scip_benders + cdef SCIP_SOL* scip_sol + if isinstance(solution, Solution): + scip_sol = solution.sol + else: + scip_sol = NULL + + if benders is None: + scip_benders = SCIPfindBenders(self._scip, "default") + else: + n = str_conversion(benders.name) + scip_benders = SCIPfindBenders(self._scip, n) + + PY_SCIP_CALL(SCIPsetupBendersSubproblem(self._scip, scip_benders, scip_sol, probnumber)) + + def solveBendersSubproblem(self, probnumber, enfotype, solvecip, Benders benders = None, Solution solution = None): + """ solves the Benders' decomposition subproblem. The convex relaxation will be solved unless + the parameter solvecip is set to True. + + Keyword arguments: + probnumber -- the index of the problem that is to be set up + enfotype -- the enforcement type used for solving the subproblem. Either LP, RELAX, PSEUDO or CHECK + solvecip -- should the CIP of the subproblem be solved, if False, then only the convex relaxation is solved + benders -- the Benders' decomposition to which the subproblem belongs to + solution -- the master problem solution that is used for the set up, if None, then the LP solution is used + """ + + cdef SCIP_BENDERS* scip_benders + cdef SCIP_Real objective + cdef SCIP_Bool infeasible + cdef SCIP_SOL* scip_sol + + if isinstance(solution, Solution): + scip_sol = solution.sol + else: + scip_sol = NULL + + if benders is None: + scip_benders = SCIPfindBenders(self._scip, "default") + else: + n = str_conversion(benders.name) + scip_benders = SCIPfindBenders(self._scip, n) + + PY_SCIP_CALL(SCIPsolveBendersSubproblem(self._scip, scip_benders, scip_sol, + probnumber, &infeasible, enfotype, solvecip, &objective)) + + return infeasible, objective + def includeEventhdlr(self, Eventhdlr eventhdlr, name, desc): """Include an event handler. @@ -1842,7 +2785,7 @@ cdef class Model: cdef SCIP_CONSHDLR* scip_conshdlr scip_conshdlr = SCIPfindConshdlr(self._scip, str_conversion(conshdlr.name)) constraint = Constraint() - PY_SCIP_CALL(SCIPcreateCons(self._scip, &(constraint.cons), n, scip_conshdlr, constraint, + PY_SCIP_CALL(SCIPcreateCons(self._scip, &(constraint.scip_cons), n, scip_conshdlr, constraint, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) return constraint @@ -1864,7 +2807,7 @@ cdef class Model: presol.model = weakref.proxy(self) Py_INCREF(presol) - def includeSepa(self, Sepa sepa, name, desc, priority, freq, maxbounddist, usessubscip=False, delay=False): + def includeSepa(self, Sepa sepa, name, desc, priority=0, freq=10, maxbounddist=1.0, usessubscip=False, delay=False): """Include a separator :param Sepa sepa: separator @@ -1882,6 +2825,7 @@ cdef class Model: PY_SCIP_CALL(SCIPincludeSepa(self._scip, n, d, priority, freq, maxbounddist, usessubscip, delay, PySepaCopy, PySepaFree, PySepaInit, PySepaExit, PySepaInitsol, PySepaExitsol, PySepaExeclp, PySepaExecsol, sepa)) sepa.model = weakref.proxy(self) + sepa.name = name Py_INCREF(sepa) def includeProp(self, Prop prop, name, desc, presolpriority, presolmaxrounds, @@ -1941,6 +2885,25 @@ cdef class Model: heur.name = name Py_INCREF(heur) + def includeRelax(self, Relax relax, name, desc, priority=10000, freq=1): + """Include a relaxation handler. + + :param Relax relax: relaxation handler + :param name: name of relaxation handler + :param desc: description of relaxation handler + :param priority: priority of the relaxation handler (negative: after LP, non-negative: before LP, Default value = 10000) + :param freq: frequency for calling relaxation handler + + """ + nam = str_conversion(name) + des = str_conversion(desc) + PY_SCIP_CALL(SCIPincludeRelax(self._scip, nam, des, priority, freq, PyRelaxCopy, PyRelaxFree, PyRelaxInit, PyRelaxExit, + PyRelaxInitsol, PyRelaxExitsol, PyRelaxExec, relax)) + relax.model = weakref.proxy(self) + relax.name = name + + Py_INCREF(relax) + def includeBranchrule(self, Branchrule branchrule, name, desc, priority, maxdepth, maxbounddist): """Include a branching rule. @@ -1955,13 +2918,288 @@ cdef class Model: nam = str_conversion(name) des = str_conversion(desc) PY_SCIP_CALL(SCIPincludeBranchrule(self._scip, nam, des, - maxdepth, maxdepth, maxbounddist, + priority, maxdepth, maxbounddist, PyBranchruleCopy, PyBranchruleFree, PyBranchruleInit, PyBranchruleExit, PyBranchruleInitsol, PyBranchruleExitsol, PyBranchruleExeclp, PyBranchruleExecext, PyBranchruleExecps, branchrule)) branchrule.model = weakref.proxy(self) Py_INCREF(branchrule) + def includeBenders(self, Benders benders, name, desc, priority=1, cutlp=True, cutpseudo=True, cutrelax=True, + shareaux=False): + """Include a Benders' decomposition. + + Keyword arguments: + benders -- the Benders decomposition + name -- the name + desc -- the description + priority -- priority of the Benders' decomposition + cutlp -- should Benders' cuts be generated from LP solutions + cutpseudo -- should Benders' cuts be generated from pseudo solutions + cutrelax -- should Benders' cuts be generated from relaxation solutions + shareaux -- should the Benders' decomposition share the auxiliary variables of the highest priority Benders' decomposition + """ + n = str_conversion(name) + d = str_conversion(desc) + PY_SCIP_CALL(SCIPincludeBenders(self._scip, n, d, + priority, cutlp, cutrelax, cutpseudo, shareaux, + PyBendersCopy, PyBendersFree, PyBendersInit, PyBendersExit, PyBendersInitpre, + PyBendersExitpre, PyBendersInitsol, PyBendersExitsol, PyBendersGetvar, + PyBendersCreatesub, PyBendersPresubsolve, PyBendersSolvesubconvex, + PyBendersSolvesub, PyBendersPostsolve, PyBendersFreesub, + benders)) + cdef SCIP_BENDERS* scip_benders + scip_benders = SCIPfindBenders(self._scip, n) + benders.model = weakref.proxy(self) + benders.name = name + Py_INCREF(benders) + + def includeBenderscut(self, Benders benders, Benderscut benderscut, name, desc, priority=1, islpcut=True): + """ Include a Benders' decomposition cutting method + + Keyword arguments: + benders -- the Benders' decomposition that this cutting method is attached to + benderscut --- the Benders' decomposition cutting method + name -- the name + desc -- the description + priority -- priority of the Benders' decomposition + islpcut -- is this cutting method suitable for generating cuts for convex relaxations? + """ + cdef SCIP_BENDERS* _benders + + bendersname = str_conversion(benders.name) + _benders = SCIPfindBenders(self._scip, bendersname) + + n = str_conversion(name) + d = str_conversion(desc) + PY_SCIP_CALL(SCIPincludeBenderscut(self._scip, _benders, n, d, priority, islpcut, + PyBenderscutCopy, PyBenderscutFree, PyBenderscutInit, PyBenderscutExit, + PyBenderscutInitsol, PyBenderscutExitsol, PyBenderscutExec, + benderscut)) + + cdef SCIP_BENDERSCUT* scip_benderscut + scip_benderscut = SCIPfindBenderscut(_benders, n) + benderscut.model = weakref.proxy(self) + benderscut.benders = weakref.proxy(benders) + benderscut.name = name + # TODO: It might be necessary in increment the reference to benders i.e Py_INCREF(benders) + Py_INCREF(benderscut) + + + def getLPBranchCands(self): + """gets branching candidates for LP solution branching (fractional variables) along with solution values, + fractionalities, and number of branching candidates; The number of branching candidates does NOT account + for fractional implicit integer variables which should not be used for branching decisions. Fractional + implicit integer variables are stored at the positions *nlpcands to *nlpcands + *nfracimplvars - 1 + branching rules should always select the branching candidate among the first npriolpcands of the candidate list + + :return tuple (lpcands, lpcandssol, lpcadsfrac, nlpcands, npriolpcands, nfracimplvars) where + + lpcands: list of variables of LP branching candidates + lpcandssol: list of LP candidate solution values + lpcandsfrac list of LP candidate fractionalities + nlpcands: number of LP branching candidates + npriolpcands: number of candidates with maximal priority + nfracimplvars: number of fractional implicit integer variables + + """ + cdef int ncands + cdef int nlpcands + cdef int npriolpcands + cdef int nfracimplvars + + ncands = SCIPgetNLPBranchCands(self._scip) + cdef SCIP_VAR** lpcands = malloc(ncands * sizeof(SCIP_VAR*)) + cdef SCIP_Real* lpcandssol = malloc(ncands * sizeof(SCIP_Real)) + cdef SCIP_Real* lpcandsfrac = malloc(ncands * sizeof(SCIP_Real)) + + PY_SCIP_CALL(SCIPgetLPBranchCands(self._scip, &lpcands, &lpcandssol, &lpcandsfrac, + &nlpcands, &npriolpcands, &nfracimplvars)) + + return ([Variable.create(lpcands[i]) for i in range(ncands)], [lpcandssol[i] for i in range(ncands)], + [lpcandsfrac[i] for i in range(ncands)], nlpcands, npriolpcands, nfracimplvars) + + + def branchVar(self, variable): + """Branch on a non-continuous variable. + + :param variable: Variable to branch on + :return: tuple(downchild, eqchild, upchild) of Nodes of the left, middle and right child. + + """ + cdef SCIP_NODE* downchild = malloc(sizeof(SCIP_NODE)) + cdef SCIP_NODE* eqchild = malloc(sizeof(SCIP_NODE)) + cdef SCIP_NODE* upchild = malloc(sizeof(SCIP_NODE)) + + PY_SCIP_CALL(SCIPbranchVar(self._scip, (variable).scip_var, &downchild, &eqchild, &upchild)) + return Node.create(downchild), Node.create(eqchild), Node.create(upchild) + + + def branchVarVal(self, variable, value): + """Branches on variable using a value which separates the domain of the variable. + + :param variable: Variable to branch on + :param value: float, value to branch on + :return: tuple(downchild, eqchild, upchild) of Nodes of the left, middle and right child. Middle child only exists + if branch variable is integer + + """ + cdef SCIP_NODE* downchild = malloc(sizeof(SCIP_NODE)) + cdef SCIP_NODE* eqchild = malloc(sizeof(SCIP_NODE)) + cdef SCIP_NODE* upchild = malloc(sizeof(SCIP_NODE)) + + PY_SCIP_CALL(SCIPbranchVarVal(self._scip, (variable).scip_var, value, &downchild, &eqchild, &upchild)) + # TODO should the stuff be freed and how? + return Node.create(downchild), Node.create(eqchild), Node.create(upchild) + + def calcNodeselPriority(self, Variable variable, branchdir, targetvalue): + """calculates the node selection priority for moving the given variable's LP value + to the given target value; + this node selection priority can be given to the SCIPcreateChild() call + + :param variable: variable on which the branching is applied + :param branchdir: type of branching that was performed + :param targetvalue: new value of the variable in the child node + :return: node selection priority for moving the given variable's LP value to the given target value + + """ + return SCIPcalcNodeselPriority(self._scip, variable.scip_var, branchdir, targetvalue) + + def calcChildEstimate(self, Variable variable, targetvalue): + """Calculates an estimate for the objective of the best feasible solution + contained in the subtree after applying the given branching; + this estimate can be given to the SCIPcreateChild() call + + :param variable: Variable to compute the estimate for + :param targetvalue: new value of the variable in the child node + :return: objective estimate of the best solution in the subtree after applying the given branching + + """ + return SCIPcalcChildEstimate(self._scip, variable.scip_var, targetvalue) + + def createChild(self, nodeselprio, estimate): + """Create a child node of the focus node. + + :param nodeselprio: float, node selection priority of new node + :param estimate: float, estimate for(transformed) objective value of best feasible solution in subtree + :return: Node, the child which was created + + """ + cdef SCIP_NODE* child = malloc(sizeof(SCIP_NODE)) + PY_SCIP_CALL(SCIPcreateChild(self._scip, &child, nodeselprio, estimate)) + return Node.create(child) + + # Diving methods (Diving is LP related) + def startDive(self): + """Initiates LP diving + It allows the user to change the LP in several ways, solve, change again, etc, without affecting the actual LP that has. When endDive() is called, + SCIP will undo all changes done and recover the LP it had before startDive + """ + PY_SCIP_CALL(SCIPstartDive(self._scip)) + + def endDive(self): + """Quits probing and resets bounds and constraints to the focus node's environment""" + PY_SCIP_CALL(SCIPendDive(self._scip)) + + def chgVarObjDive(self, Variable var, newobj): + """changes (column) variable's objective value in current dive""" + PY_SCIP_CALL(SCIPchgVarObjDive(self._scip, var.scip_var, newobj)) + + def chgVarLbDive(self, Variable var, newbound): + """changes variable's current lb in current dive""" + PY_SCIP_CALL(SCIPchgVarLbDive(self._scip, var.scip_var, newbound)) + + def chgVarUbDive(self, Variable var, newbound): + """changes variable's current ub in current dive""" + PY_SCIP_CALL(SCIPchgVarUbDive(self._scip, var.scip_var, newbound)) + + def getVarLbDive(self, Variable var): + """returns variable's current lb in current dive""" + return SCIPgetVarLbDive(self._scip, var.scip_var) + + def getVarUbDive(self, Variable var): + """returns variable's current ub in current dive""" + return SCIPgetVarUbDive(self._scip, var.scip_var) + + def chgRowLhsDive(self, Row row, newlhs): + """changes row lhs in current dive, change will be undone after diving + ends, for permanent changes use SCIPchgRowLhs() + """ + PY_SCIP_CALL(SCIPchgRowLhsDive(self._scip, row.scip_row, newlhs)) + + def chgRowRhsDive(self, Row row, newrhs): + """changes row rhs in current dive, change will be undone after diving + ends, for permanent changes use SCIPchgRowLhs() + """ + PY_SCIP_CALL(SCIPchgRowRhsDive(self._scip, row.scip_row, newrhs)) + + def addRowDive(self, Row row): + """adds a row to the LP in current dive""" + PY_SCIP_CALL(SCIPaddRowDive(self._scip, row.scip_row)) + + def solveDiveLP(self, itlim = -1): + """solves the LP of the current dive no separation or pricing is applied + no separation or pricing is applied + :param itlim: maximal number of LP iterations to perform (Default value = -1, that is, no limit) + returns two booleans: + lperror -- if an unresolved lp error occured + cutoff -- whether the LP was infeasible or the objective limit was reached + """ + cdef SCIP_Bool lperror + cdef SCIP_Bool cutoff + + PY_SCIP_CALL(SCIPsolveDiveLP(self._scip, itlim, &lperror, &cutoff)) + return lperror, cutoff + + def inRepropagation(self): + """returns if the current node is already solved and only propagated again.""" + return SCIPinRepropagation(self._scip) + + # Probing methods (Probing is tree based) + def startProbing(self): + """Initiates probing, making methods SCIPnewProbingNode(), SCIPbacktrackProbing(), SCIPchgVarLbProbing(), + SCIPchgVarUbProbing(), SCIPfixVarProbing(), SCIPpropagateProbing(), SCIPsolveProbingLP(), etc available + """ + PY_SCIP_CALL(SCIPstartProbing(self._scip)) + + def endProbing(self): + """Quits probing and resets bounds and constraints to the focus node's environment""" + PY_SCIP_CALL(SCIPendProbing(self._scip)) + + def chgVarObjProbing(self, Variable var, newobj): + """changes (column) variable's objective value during probing mode""" + PY_SCIP_CALL(SCIPchgVarObjProbing(self._scip, var.scip_var, newobj)) + + def fixVarProbing(self, Variable var, fixedval): + """Fixes a variable at the current probing node.""" + PY_SCIP_CALL(SCIPfixVarProbing(self._scip, var.scip_var, fixedval)) + + def isObjChangedProbing(self): + """returns whether the objective function has changed during probing mode""" + return SCIPisObjChangedProbing(self._scip) + + def inProbing(self): + """returns whether we are in probing mode; probing mode is activated via startProbing() and stopped via endProbing()""" + return SCIPinProbing(self._scip) + + def solveProbingLP(self, itlim = -1): + """solves the LP at the current probing node (cannot be applied at preprocessing stage) + no separation or pricing is applied + :param itlim: maximal number of LP iterations to perform (Default value = -1, that is, no limit) + returns two booleans: + lperror -- if an unresolved lp error occured + cutoff -- whether the LP was infeasible or the objective limit was reached + """ + cdef SCIP_Bool lperror + cdef SCIP_Bool cutoff + + PY_SCIP_CALL(SCIPsolveProbingLP(self._scip, itlim, &lperror, &cutoff)) + return lperror, cutoff + + def interruptSolve(self): + """Interrupt the solving process as soon as possible.""" + PY_SCIP_CALL(SCIPinterruptSolve(self._scip)) + # Solution functions def createSol(self, Heur heur = None): @@ -2064,7 +3302,7 @@ cdef class Model: """ cdef SCIP_SOL* _sol _sol = solution.sol - PY_SCIP_CALL(SCIPsetSolVal(self._scip, _sol, var.var, val)) + PY_SCIP_CALL(SCIPsetSolVal(self._scip, _sol, var.scip_var, val)) def trySol(self, Solution solution, printreason=True, completely=False, checkbounds=True, checkintegrality=True, checklprows=True, free=True): """Check given primal solution for feasibility and try to add it to the storage. @@ -2085,6 +3323,25 @@ cdef class Model: PY_SCIP_CALL(SCIPtrySol(self._scip, solution.sol, printreason, completely, checkbounds, checkintegrality, checklprows, &stored)) return stored + def checkSol(self, Solution solution, printreason=True, completely=False, checkbounds=True, checkintegrality=True, checklprows=True, original=False): + """Check given primal solution for feasibility without adding it to the storage. + + :param Solution solution: solution to store + :param printreason: should all reasons of violations be printed? (Default value = True) + :param completely: should all violation be checked? (Default value = False) + :param checkbounds: should the bounds of the variables be checked? (Default value = True) + :param checkintegrality: has integrality to be checked? (Default value = True) + :param checklprows: have current LP rows (both local and global) to be checked? (Default value = True) + :param original: must the solution be checked against the original problem (Default value = False) + + """ + cdef SCIP_Bool feasible + if original: + PY_SCIP_CALL(SCIPcheckSolOrig(self._scip, solution.sol, &feasible, printreason, completely)) + else: + PY_SCIP_CALL(SCIPcheckSol(self._scip, solution.sol, printreason, completely, checkbounds, checkintegrality, checklprows, &feasible)) + return feasible + def addSol(self, Solution solution, free=True): """Try to add a solution to the storage. @@ -2161,7 +3418,7 @@ cdef class Model: """ if sol == None: sol = Solution.create(NULL) - return SCIPgetSolVal(self._scip, sol.sol, var.var) + return SCIPgetSolVal(self._scip, sol.sol, var.scip_var) def getVal(self, Variable var): """Retrieve the value of the best known solution. @@ -2192,7 +3449,7 @@ cdef class Model: :param Variable var: variable """ - PY_SCIP_CALL(SCIPwriteVarName(self._scip, NULL, var.var, False)) + PY_SCIP_CALL(SCIPwriteVarName(self._scip, NULL, var.scip_var, False)) def getStage(self): """Retrieve current SCIP stage""" @@ -2223,6 +3480,7 @@ cdef class Model: return "unknown" def catchEvent(self, eventtype, Eventhdlr eventhdlr): + """catches a global (not variable or row dependent) event""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -2232,6 +3490,7 @@ cdef class Model: PY_SCIP_CALL(SCIPcatchEvent(self._scip, eventtype, _eventhdlr, NULL, NULL)) def dropEvent(self, eventtype, Eventhdlr eventhdlr): + """drops a global event (stops to track event)""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) @@ -2241,40 +3500,44 @@ cdef class Model: PY_SCIP_CALL(SCIPdropEvent(self._scip, eventtype, _eventhdlr, NULL, -1)) def catchVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): + """catches an objective value or domain change event on the given transformed variable""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) _eventhdlr = SCIPfindEventhdlr(self._scip, n) else: raise Warning("event handler not found") - PY_SCIP_CALL(SCIPcatchVarEvent(self._scip, var.var, eventtype, _eventhdlr, NULL, NULL)) + PY_SCIP_CALL(SCIPcatchVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, NULL)) def dropVarEvent(self, Variable var, eventtype, Eventhdlr eventhdlr): + """drops an objective value or domain change event (stops to track event) on the given transformed variable""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) _eventhdlr = SCIPfindEventhdlr(self._scip, n) else: raise Warning("event handler not found") - PY_SCIP_CALL(SCIPdropVarEvent(self._scip, var.var, eventtype, _eventhdlr, NULL, -1)) + PY_SCIP_CALL(SCIPdropVarEvent(self._scip, var.scip_var, eventtype, _eventhdlr, NULL, -1)) def catchRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): + """catches a row coefficient, constant, or side change event on the given row""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) _eventhdlr = SCIPfindEventhdlr(self._scip, n) else: raise Warning("event handler not found") - PY_SCIP_CALL(SCIPcatchRowEvent(self._scip, row.row, eventtype, _eventhdlr, NULL, NULL)) + PY_SCIP_CALL(SCIPcatchRowEvent(self._scip, row.scip_row, eventtype, _eventhdlr, NULL, NULL)) def dropRowEvent(self, Row row, eventtype, Eventhdlr eventhdlr): + """drops a row coefficient, constant, or side change event (stops to track event) on the given row""" cdef SCIP_EVENTHDLR* _eventhdlr if isinstance(eventhdlr, Eventhdlr): n = str_conversion(eventhdlr.name) _eventhdlr = SCIPfindEventhdlr(self._scip, n) else: raise Warning("event handler not found") - PY_SCIP_CALL(SCIPdropRowEvent(self._scip, row.row, eventtype, _eventhdlr, NULL, -1)) + PY_SCIP_CALL(SCIPdropRowEvent(self._scip, row.scip_row, eventtype, _eventhdlr, NULL, -1)) # Statistic Methods @@ -2294,6 +3557,10 @@ cdef class Model: cfile = fdopen(f.fileno(), "w") PY_SCIP_CALL(SCIPprintStatistics(self._scip, cfile)) + def getNLPs(self): + """gets total number of LPs solved so far""" + return SCIPgetNLPs(self._scip) + # Verbosity Methods def hideOutput(self, quiet = True): @@ -2304,6 +3571,17 @@ cdef class Model: """ SCIPsetMessagehdlrQuiet(self._scip, quiet) + # Output Methods + + def redirectOutput(self): + """Send output to python instead of terminal.""" + + cdef SCIP_MESSAGEHDLR *myMessageHandler + + PY_SCIP_CALL(SCIPmessagehdlrCreate(&myMessageHandler, False, NULL, False, relayMessage, relayMessage, relayMessage, NULL, NULL)) + PY_SCIP_CALL(SCIPsetMessagehdlr(self._scip, myMessageHandler)) + SCIPmessageSetErrorPrinting(relayErrorMessage, NULL) + # Parameter Methods def setBoolParam(self, name, value): @@ -2354,7 +3632,7 @@ cdef class Model: """ n = str_conversion(name) - PY_SCIP_CALL(SCIPsetCharParam(self._scip, n, value)) + PY_SCIP_CALL(SCIPsetCharParam(self._scip, n, ord(value))) def setStringParam(self, name, value): """Set a string-valued parameter. @@ -2364,7 +3642,8 @@ cdef class Model: """ n = str_conversion(name) - PY_SCIP_CALL(SCIPsetStringParam(self._scip, n, value)) + v = str_conversion(value) + PY_SCIP_CALL(SCIPsetStringParam(self._scip, n, v)) def setParam(self, name, value): """Set a parameter with value in int, bool, real, long, char or str. @@ -2383,13 +3662,13 @@ cdef class Model: paramtype = SCIPparamGetType(param) if paramtype == SCIP_PARAMTYPE_BOOL: - PY_SCIP_CALL(SCIPsetBoolParam(self._scip, n, value)) + PY_SCIP_CALL(SCIPsetBoolParam(self._scip, n, bool(int(value)))) elif paramtype == SCIP_PARAMTYPE_INT: - PY_SCIP_CALL(SCIPsetIntParam(self._scip, n, value)) + PY_SCIP_CALL(SCIPsetIntParam(self._scip, n, int(value))) elif paramtype == SCIP_PARAMTYPE_LONGINT: - PY_SCIP_CALL(SCIPsetLongintParam(self._scip, n, value)) + PY_SCIP_CALL(SCIPsetLongintParam(self._scip, n, int(value))) elif paramtype == SCIP_PARAMTYPE_REAL: - PY_SCIP_CALL(SCIPsetRealParam(self._scip, n, value)) + PY_SCIP_CALL(SCIPsetRealParam(self._scip, n, float(value))) elif paramtype == SCIP_PARAMTYPE_CHAR: PY_SCIP_CALL(SCIPsetCharParam(self._scip, n, value)) elif paramtype == SCIP_PARAMTYPE_STRING: @@ -2421,7 +3700,7 @@ cdef class Model: elif paramtype == SCIP_PARAMTYPE_REAL: return SCIPparamGetReal(param) elif paramtype == SCIP_PARAMTYPE_CHAR: - return SCIPparamGetChar(param) + return chr(SCIPparamGetChar(param)) elif paramtype == SCIP_PARAMTYPE_STRING: return SCIPparamGetString(param) @@ -2524,7 +3803,7 @@ cdef class Model: else: raise Warning("unrecognized optimization sense: %s" % sense) - assert isinstance(coeffs, Expr) + assert isinstance(coeffs, Expr), "given coefficients are not Expr but %s" % coeffs.__class__.__name__ if coeffs.degree() > 1: raise ValueError("Nonlinear objective functions are not supported!") @@ -2546,7 +3825,7 @@ cdef class Model: assert len(term) == 1 var = term[0] for i in range(_nvars): - if _vars[i] == var.var: + if _vars[i] == var.scip_var: _coeffs[i] = coef PY_SCIP_CALL(SCIPchgReoptObjective(self._scip, objsense, _vars, &_coeffs[0], _nvars)) diff --git a/src/pyscipopt/sepa.pxi b/src/pyscipopt/sepa.pxi index 08cea1cc6..6e0dcd7a3 100644 --- a/src/pyscipopt/sepa.pxi +++ b/src/pyscipopt/sepa.pxi @@ -1,5 +1,6 @@ cdef class Sepa: cdef public Model model + cdef public str name def sepafree(self): pass diff --git a/tests/test_alldiff.py b/tests/test_alldiff.py index 99f046a40..ad5c2d946 100644 --- a/tests/test_alldiff.py +++ b/tests/test_alldiff.py @@ -129,9 +129,8 @@ def propagate_cons(self, cons): G.remove_edges_from(visited_edges) # compute strongly connected components of D and mark edges on the cc as useful - gscc = networkx.strongly_connected_component_subgraphs(D, copy=False) - for g in gscc: - for e in g.edges(): + for g in networkx.strongly_connected_components(D): + for e in D.subgraph(g).edges(): if G.has_edge(*e): G.remove_edge(*e) @@ -203,7 +202,7 @@ def consenfolp(self, constraints, n_useful_conss, sol_infeasible): return {"result": SCIP_RESULT.INFEASIBLE} return {"result": SCIP_RESULT.FEASIBLE} - def conslock(self, constraint, nlockspos, nlocksneg): + def conslock(self, constraint, locktype, nlockspos, nlocksneg): for var in constraint.data.vars: self.model.addVarLocks(var, nlockspos + nlocksneg , nlockspos + nlocksneg) diff --git a/tests/test_benders.py b/tests/test_benders.py new file mode 100644 index 000000000..5f4633dac --- /dev/null +++ b/tests/test_benders.py @@ -0,0 +1,113 @@ +""" +flp-benders.py: model for solving the capacitated facility location problem using Benders' decomposition + +minimize the total (weighted) travel cost from n customers +to some facilities with fixed costs and capacities. + +Copyright (c) by Joao Pedro PEDROSO and Mikio KUBO, 2012 +""" +from pyscipopt import Model, quicksum, multidict, SCIP_PARAMSETTING +import pdb + +def flp(I,J,d,M,f,c): + """flp -- model for the capacitated facility location problem + Parameters: + - I: set of customers + - J: set of facilities + - d[i]: demand for customer i + - M[j]: capacity of facility j + - f[j]: fixed cost for using a facility in point j + - c[i,j]: unit cost of servicing demand point i from facility j + Returns a model, ready to be solved. + """ + + master = Model("flp-master") + subprob = Model("flp-subprob") + + # creating the problem + y = {} + for j in J: + y[j] = master.addVar(vtype="B", name="y(%s)"%j) + + master.setObjective( + quicksum(f[j]*y[j] for j in J), + "minimize") + master.data = y + + # creating the subproblem + x,y = {},{} + for j in J: + y[j] = subprob.addVar(vtype="B", name="y(%s)"%j) + for i in I: + x[i,j] = subprob.addVar(vtype="C", name="x(%s,%s)"%(i,j)) + + for i in I: + subprob.addCons(quicksum(x[i,j] for j in J) == d[i], "Demand(%s)"%i) + + for j in M: + subprob.addCons(quicksum(x[i,j] for i in I) <= M[j]*y[j], "Capacity(%s)"%i) + + for (i,j) in x: + subprob.addCons(x[i,j] <= d[i]*y[j], "Strong(%s,%s)"%(i,j)) + + subprob.setObjective( + quicksum(c[i,j]*x[i,j] for i in I for j in J), + "minimize") + subprob.data = x,y + + return master, subprob + + +def make_data(): + I,d = multidict({1:80, 2:270, 3:250, 4:160, 5:180}) # demand + J,M,f = multidict({1:[500,1000], 2:[500,1000], 3:[500,1000]}) # capacity, fixed costs + c = {(1,1):4, (1,2):6, (1,3):9, # transportation costs + (2,1):5, (2,2):4, (2,3):7, + (3,1):6, (3,2):3, (3,3):4, + (4,1):8, (4,2):5, (4,3):3, + (5,1):10, (5,2):8, (5,3):4, + } + return I,J,d,M,f,c + + +def test_flpbenders(): + ''' + test the Benders' decomposition plugins with the facility location problem. + ''' + I,J,d,M,f,c = make_data() + master, subprob = flp(I,J,d,M,f,c) + # initializing the default Benders' decomposition with the subproblem + master.setPresolve(SCIP_PARAMSETTING.OFF) + master.setBoolParam("misc/allowdualreds", False) + master.setBoolParam("benders/copybenders", False) + master.initBendersDefault(subprob) + + # optimizing the problem using Benders' decomposition + master.optimize() + + # solving the subproblems to get the best solution + master.computeBestSolSubproblems() + + EPS = 1.e-6 + y = master.data + facilities = [j for j in y if master.getVal(y[j]) > EPS] + + x, suby = subprob.data + edges = [(i,j) for (i,j) in x if subprob.getVal(x[i,j]) > EPS] + + print("Optimal value:", master.getObjVal()) + print("Facilities at nodes:", facilities) + print("Edges:", edges) + + master.printStatistics() + + # since computeBestSolSubproblems() was called above, we need to free the + # subproblems. This must happen after the solution is extracted, otherwise + # the solution will be lost + master.freeBendersSubproblems() + + assert master.getObjVal() == 5.61e+03 + + +if __name__ == "__main__": + test_flpbenders() diff --git a/tests/test_branch_probing_lp.py b/tests/test_branch_probing_lp.py new file mode 100644 index 000000000..c7a493cdb --- /dev/null +++ b/tests/test_branch_probing_lp.py @@ -0,0 +1,96 @@ +from pyscipopt import Model, Branchrule, SCIP_RESULT, quicksum + + +class MyBranching(Branchrule): + + def __init__(self, model, cont): + self.model = model + self.cont = cont + self.count = 0 + self.was_called_val = False + self.was_called_int = False + + def branchexeclp(self, allowaddcons): + self.count += 1 + if self.count >= 2: + return {"result": SCIP_RESULT.DIDNOTRUN} + assert allowaddcons + + assert not self.model.inRepropagation() + assert not self.model.inProbing() + self.model.startProbing() + assert not self.model.isObjChangedProbing() + self.model.fixVarProbing(self.cont, 2.0) + self.model.constructLP() + self.model.solveProbingLP() + self.model.getLPObjVal() + self.model.endProbing() + + self.integral = self.model.getLPBranchCands()[0][0] + + if self.count == 1: + down, eq, up = self.model.branchVarVal(self.cont, 1.3) + self.model.chgVarLbNode(down, self.cont, -1.5) + self.model.chgVarUbNode(up, self.cont, 3.0) + self.was_called_val = True + down2, eq2, up2 = self.model.branchVar(self.integral) + self.was_called_int = True + self.model.createChild(6, 7) + return {"result": SCIP_RESULT.BRANCHED} + + + + +m = Model() +m.setIntParam("presolving/maxrounds", 0) +#m.setLongintParam("lp/rootiterlim", 3) +m.setRealParam("limits/time", 60) + +x0 = m.addVar(lb=-2, ub=4) +r1 = m.addVar() +r2 = m.addVar() +y0 = m.addVar(lb=3) +t = m.addVar(lb=None) +l = m.addVar(vtype="I", lb=-9, ub=18) +u = m.addVar(vtype="I", lb=-3, ub=99) + +more_vars = [] +for i in range(1000): + more_vars.append(m.addVar(vtype="I", lb= -12, ub=40)) + m.addCons(quicksum(v for v in more_vars) <= (40 - i) * quicksum(v for v in more_vars[::2])) + +for i in range(1000): + more_vars.append(m.addVar(vtype="I", lb= -52, ub=10)) + m.addCons(quicksum(v for v in more_vars[50::2]) <= (40 - i) * quicksum(v for v in more_vars[405::2])) + + + +m.addCons(r1 >= x0) +m.addCons(r2 >= -x0) +m.addCons(y0 == r1 +r2) +#m.addCons(t * l + l * u >= 4) +m.addCons(t + l + 7* u <= 300) +m.addCons(t >= quicksum(v for v in more_vars[::3]) - 10 * more_vars[5] + 5* more_vars[9]) +m.addCons(more_vars[3] >= l + 2) +m.addCons(7 <= quicksum(v for v in more_vars[::4]) - x0) +m.addCons(quicksum(v for v in more_vars[::2]) + l <= quicksum(v for v in more_vars[::4])) + + +m.setObjective(t - quicksum(j*v for j, v in enumerate(more_vars[20:-40]))) +#m.addCons(t >= r1 * (r1 - x0) + r2 * (r2 + x0)) + +my_branchrule = MyBranching(m, x0) +m.includeBranchrule(my_branchrule, "test branch", "test branching and probing and lp functions", + priority=10000000, maxdepth=3, maxbounddist=1) + +m.optimize() + +print("x0", m.getVal(x0)) +print("r1", m.getVal(r1)) +print("r2", m.getVal(r2)) +print("y0", m.getVal(y0)) +print("t", m.getVal(t)) + +assert my_branchrule.was_called_val +assert my_branchrule.was_called_int + diff --git a/tests/test_conshdlr.py b/tests/test_conshdlr.py index 39b858c44..45c42852f 100644 --- a/tests/test_conshdlr.py +++ b/tests/test_conshdlr.py @@ -64,7 +64,7 @@ def conscheck(self, constraints, solution, checkintegrality, checklprows, printr assert id(constraint) in ids return {"result": SCIP_RESULT.FEASIBLE} - def conslock(self, constraint, nlockspos, nlocksneg): + def conslock(self, constraint, locktype, nlockspos, nlocksneg): calls.add("conslock") assert id(constraint) in ids diff --git a/tests/test_event.py b/tests/test_event.py index 478a2e56a..cb050271b 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -16,6 +16,10 @@ def eventexit(self): def eventexec(self, event): calls.append('eventexec') + event.getNewBound() + event.getOldBound() + assert event.getNode().getNumber() == 1 + def test_event(): # create solver instance diff --git a/tests/test_expr.py b/tests/test_expr.py index a9dca9698..81fef8a48 100644 --- a/tests/test_expr.py +++ b/tests/test_expr.py @@ -51,7 +51,7 @@ def test_upgrade(model): assert isinstance(log(expr), GenExpr) assert isinstance(exp(expr), GenExpr) - with pytest.raises(ValueError): + with pytest.raises(ZeroDivisionError): expr /= 0.0 def test_genexpr_op_expr(model): diff --git a/tests/test_gomory.py b/tests/test_gomory.py new file mode 100644 index 000000000..b6f209b3c --- /dev/null +++ b/tests/test_gomory.py @@ -0,0 +1,345 @@ +import pytest + +from pyscipopt import Model, Sepa, SCIP_RESULT, SCIP_PARAMSETTING +from pyscipopt.scip import is_memory_freed + +class GMI(Sepa): + + def __init__(self): + self.ncuts = 0 + + def getGMIFromRow(self, cols, rows, binvrow, binvarow, primsol): + """ Given the row (binvarow, binvrow) of the tableau, computes gomory cut + + :param primsol: is the rhs of the tableau row. + :param cols: are the variables + :param rows: are the slack variables + :param binvrow: components of the tableau row associated to the basis inverse + :param binvarow: components of the tableau row associated to the basis inverse * A + + The GMI is given by + sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + where J_I are the integer non-basic variables and J_C are the continuous. + f_0 is the fractional part of primsol + a_j is the j-th coefficient of the row and f_j its fractional part + Note: we create -% <= -f_0 !! + Note: this formula is valid for a problem of the form Ax = b, x>= 0. Since we do not have + such problem structure in general, we have to (implicitely) transform whatever we are given + to that form. Specifically, non-basic variables at their lower bound are shifted so that the lower + bound is 0 and non-basic at their upper bound are complemented. + """ + + # initialize + cutcoefs = [0] * len(cols) + cutrhs = 0 + + # get scip + scip = self.model + + # Compute cut fractionality f0 and f0/(1-f0) + f0 = scip.frac(primsol) + ratiof0compl = f0/(1-f0) + + # rhs of the cut is the fractional part of the LP solution for the basic variable + cutrhs = -f0 + + # Generate cut coefficients for the original variables + for c in range(len(cols)): + col = cols[c] + assert col is not None # is this the equivalent of col != NULL? does it even make sense to have this assert? + status = col.getBasisStatus() + + # Get simplex tableau coefficient + if status == "lower": + # Take coefficient if nonbasic at lower bound + rowelem = binvarow[c] + elif status == "upper": + # Flip coefficient if nonbasic at upper bound: x --> u - x + rowelem = -binvarow[c] + else: + # variable is nonbasic free at zero -> cut coefficient is zero, skip OR + # variable is basic, skip + assert status == "zero" or status == "basic" + continue + + # Integer variables + if col.isIntegral(): + # warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive) + # However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true, so we ignore + # the coefficient (see below) + cutelem = scip.frac(rowelem) + + if cutelem > f0: + # sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + cutelem = -((1.0 - cutelem) * ratiof0compl) + else: + # sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + cutelem = -cutelem + else: + # Continuous variables + if rowelem < 0.0: + # -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + cutelem = rowelem * ratiof0compl + else: + # sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + cutelem = -rowelem + + # cut is define when variables are in [0, infty). Translate to general bounds + if not scip.isZero(cutelem): + if col.getBasisStatus() == "upper": + cutelem = -cutelem + cutrhs += cutelem * col.getUb() + else: + cutrhs += cutelem * col.getLb() + # Add coefficient to cut in dense form + cutcoefs[col.getLPPos()] = cutelem + + # Generate cut coefficients for the slack variables; skip basic ones + for c in range(len(rows)): + row = rows[c] + assert row != None + status = row.getBasisStatus() + + # free slack variable shouldn't appear + assert status != "zero" + + # Get simplex tableau coefficient + if status == "lower": + # Take coefficient if nonbasic at lower bound + rowelem = binvrow[row.getLPPos()] + # But if this is a >= or ranged constraint at the lower bound, we have to flip the row element + if not scip.isInfinity(-row.getLhs()): + rowelem = -rowelem + elif status == "upper": + # Take element if nonbasic at upper bound - see notes at beginning of file: only nonpositive slack variables + # can be nonbasic at upper, therefore they should be flipped twice and we can take the element directly. + rowelem = binvrow[row.getLPPos()] + else: + assert status == "basic" + continue + + # if row is integral we can strengthen the cut coefficient + if row.isIntegral() and not row.isModifiable(): + # warning: because of numerics cutelem < 0 is possible (though the fractional part is, mathematically, always positive) + # However, when cutelem < 0 it is also very close to 0, enough that isZero(cutelem) is true (see later) + cutelem = scip.frac(rowelem) + + if cutelem > f0: + # sum((1-f_j)*f_0/(1 - f_0) x_j, j in J_I s.t. f_j > f_0) + + cutelem = -((1.0 - cutelem) * ratiof0compl) + else: + # sum(f_j x_j , j in J_I s.t. f_j <= f_0) + + cutelem = -cutelem + else: + # Continuous variables + if rowelem < 0.0: + # -sum(a_j*f_0/(1-f_0) x_j , j in J_C s.t. a_j < 0) >= f_0. + cutelem = rowelem * ratiof0compl + else: + # sum(a_j x_j, , j in J_C s.t. a_j >= 0) - + cutelem = -rowelem + + # cut is define in original variables, so we replace slack by its definition + if not scip.isZero(cutelem): + # get lhs/rhs + rlhs = row.getLhs() + rrhs = row.getRhs() + assert scip.isLE(rlhs, rrhs) + assert not scip.isInfinity(rlhs) or not scip.isInfinity(rrhs) + + # If the slack variable is fixed, we can ignore this cut coefficient + if scip.isFeasZero(rrhs - rlhs): + continue + + # Unflip slack variable and adjust rhs if necessary: row at lower means the slack variable is at its upper bound. + # Since SCIP adds +1 slacks, this can only happen when constraints have a finite lhs + if row.getBasisStatus() == "lower": + assert not scip.isInfinity(-rlhs) + cutelem = -cutelem + + rowcols = row.getCols() + rowvals = row.getVals() + + assert len(rowcols) == len(rowvals) + + # Eliminate slack variable: rowcols is sorted: [columns in LP, columns not in LP] + for i in range(row.getNLPNonz()): + cutcoefs[rowcols[i].getLPPos()] -= cutelem * rowvals[i] + + act = scip.getRowLPActivity(row) + rhsslack = rrhs - act + if scip.isFeasZero(rhsslack): + assert row.getBasisStatus() == "upper" # cutelem != 0 and row active at upper bound -> slack at lower, row at upper + cutrhs -= cutelem * (rrhs - row.getConstant()) + else: + assert scip.isFeasZero(act - rlhs) + cutrhs -= cutelem * (rlhs - row.getConstant()) + + return cutcoefs, cutrhs + + def sepaexeclp(self): + result = SCIP_RESULT.DIDNOTRUN + scip = self.model + + if not scip.isLPSolBasic(): + return {"result": result} + + #TODO: add SCIPgetNLPBranchCands + # get var data ---> this is the same as getVars! + vars = scip.getVars(transformed = True) + + # get LP data + cols = scip.getLPColsData() + rows = scip.getLPRowsData() + + # exit if LP is trivial + if len(cols) == 0 or len(rows) == 0: + return {"result": result} + + result = SCIP_RESULT.DIDNOTFIND + + # get basis indices + basisind = scip.getLPBasisInd() + + # For all basic columns (not slacks) belonging to integer variables, try to generate a gomory cut + for i in range(len(rows)): + tryrow = False + c = basisind[i] + #primsol = SCIP_INVALID + + #print("Row %d/%d basic index: %d"%(i, len(rows),c)) + #print("Row %d with value %f"%(i, cols[c].getPrimsol() if c >=0 else scip.getRowActivity(rows[-c-1]))) + if c >= 0: + assert c < len(cols) + var = cols[c].getVar() + + if var.vtype() != "CONTINUOUS": + primsol = cols[c].getPrimsol() + assert scip.getSolVal(None, var) == primsol + + #print("var ", var," is not continuous. primsol = ", primsol) + if 0.005 <= scip.frac(primsol) <= 1 - 0.005: + #print("####trying gomory cut for col <%s> [%g] row %i"%(var, primsol, i)) + tryrow = True + + # generate the cut! + if tryrow: + # get the row of B^-1 for this basic integer variable with fractional solution value + binvrow = scip.getLPBInvRow(i) + + # get the tableau row for this basic integer variable with fractional solution value + binvarow = scip.getLPBInvARow(i) + + # get cut's coefficients + cutcoefs, cutrhs = self.getGMIFromRow(cols, rows, binvrow, binvarow, primsol) + + ######################################### + #### This code is for testing only!! #### + # the first two cuts are -x1 + 2y <= 0 and -x2 +2y <= 0 + # the next two (when both previous ones are added) are -2x1 + 6y <=0 and -2x2 + 6y <=0 + assert scip.isZero(cutcoefs[0]) or scip.isZero(cutcoefs[1]) + if self.ncuts < 2: + assert scip.isZero(cutcoefs[0] + 1) or scip.isZero(cutcoefs[1] + 1) + assert scip.isZero(cutcoefs[2] - 2) + else: + assert scip.isZero(cutcoefs[0] + 2) or scip.isZero(cutcoefs[1] + 2) + assert scip.isZero(cutcoefs[2] - 6) + ########################################## + + # add cut + # TODO: here it might make sense just to have a function `addCut` just like `addCons`. Or maybe better `createCut` + # so that then one can ask stuff about it, like its efficacy, etc. This function would receive all coefficients + # and basically do what we do here: cacheRowExtension etc up to releaseRow + cut = scip.createEmptyRowSepa(self, "gmi%d_x%d"%(self.ncuts,c if c >= 0 else -c-1), lhs = None, rhs = cutrhs) + scip.cacheRowExtensions(cut) + + for j in range(len(cutcoefs)): + if scip.isZero(cutcoefs[j]): # maybe here we need isFeasZero + continue + #print("var : ", cols[j].getVar(), " coef: ", cutcoefs[j]) + #print("cut.lhs : ", cut.getLhs(), " cut.rhs: ", cut.getRhs()) + scip.addVarToRow(cut, cols[j].getVar(), cutcoefs[j]) + + if cut.getNNonz() == 0: + assert scip.isFeasNegative(cutrhs) + #print("Gomory cut is infeasible: 0 <= ", cutrhs) + return {"result": SCIP_RESULT.CUTOFF} + + + # Only take efficacious cuts, except for cuts with one non-zero coefficient (= bound changes) + # the latter cuts will be handeled internally in sepastore. + if cut.getNNonz() == 1 or scip.isCutEfficacious(cut): + #print(" -> gomory cut for <%s>: rhs=%f, eff=%f"%(cols[c].getVar() if c >= 0 else rows[-c-1].getName(), + # cutrhs, scip.getCutEfficacy(cut))) + + #SCIPdebugMessage(" -> found gomory cut <%s>: act=%f, rhs=%f, norm=%f, eff=%f, min=%f, max=%f (range=%f)\n", + # cutname, SCIPgetRowLPActivity(scip, cut), SCIProwGetRhs(cut), SCIProwGetNorm(cut), + # SCIPgetCutEfficacy(scip, NULL, cut), + # SCIPgetRowMinCoef(scip, cut), SCIPgetRowMaxCoef(scip, cut), + # SCIPgetRowMaxCoef(scip, cut)/SCIPgetRowMinCoef(scip, cut)) + + # flush all changes before adding the cut + scip.flushRowExtensions(cut) + + infeasible = scip.addCut(cut, forcecut=True) + self.ncuts += 1 + + if infeasible: + result = SCIP_RESULT.CUTOFF + else: + result = SCIP_RESULT.SEPARATED + scip.releaseRow(cut) + + return {"result": result} + +def model(): + # create solver instance + s = Model() + + # include separator + sepa = GMI() + s.includeSepa(sepa, "python_gmi", "generates gomory mixed integer cuts", priority = 1000, freq = 1) + + # turn off presolve + s.setPresolve(SCIP_PARAMSETTING.OFF) + # turn off heuristics + s.setHeuristics(SCIP_PARAMSETTING.OFF) + # turn off propagation + s.setIntParam("propagating/maxrounds", 0) + s.setIntParam("propagating/maxroundsroot", 0) + # turn off some cuts + s.setIntParam("separating/strongcg/freq", -1) + s.setIntParam("separating/gomory/freq", -1) + s.setIntParam("separating/aggregation/freq", -1) + s.setIntParam("separating/mcf/freq", -1) + s.setIntParam("separating/closecuts/freq", -1) + s.setIntParam("separating/clique/freq", -1) + s.setIntParam("separating/zerohalf/freq", -1) + + # only two rounds of cuts + s.setIntParam("separating/maxroundsroot", 2) + + return s + +# we use Cook Kannan and Schrijver's example +def test_CKS(): + s = model() + # add variables + x1 = s.addVar("x1", vtype='I') + x2 = s.addVar("x2", vtype='I') + y = s.addVar("y", obj=-1, vtype='C') + + # add constraint + s.addCons(-x1 + y <= 0) + s.addCons( - x2 + y <= 0) + s.addCons( x1 + x2 + y <= 2) + + # solve problem + s.optimize() + s.printStatistics() + +if __name__ == "__main__": + test_CKS() diff --git a/tests/test_logical.py b/tests/test_logical.py new file mode 100644 index 000000000..07aec63de --- /dev/null +++ b/tests/test_logical.py @@ -0,0 +1,168 @@ +from pyscipopt import Model +from pyscipopt import quicksum + +try: + import pytest + itertools = pytest.importorskip("itertools") +except ImportError: + import itertools +product = itertools.product + + +# Testing AND/OR/XOR constraints +# check whether any error is raised +# see http://scip.zib.de/doc-5.0.1/html/cons__and_8c.php +# for resultant and operators definition +# CAVEAT: ONLY binary variables are allowed +# Integer and continous variables behave unexpectedly (due to SCIP?) +# TBI: automatic assertion of expected resultant VS optimal resultant +# (visual inspection at the moment) + +verbose = True # py.test ignores this + + +# AUXILIARY FUNCTIONS +def setModel(vtype="B", name=None, imax=2): + """initialize model and its variables. + imax (int): number of operators""" + if name is None: + name = "model" + m = Model(name) + m.hideOutput() + i = 0 + r = m.addVar("r", vtype) + while i < imax: + m.addVar("v%s" % i, vtype) + i += 1 + return m + + +def getVarByName(m, name): + try: + return [v for v in m.getVars() if name == v.name][0] + except IndexError: + return None + + +def getAllVarsByName(m, name): + try: + return [v for v in m.getVars() if name in v.name] + except IndexError: + return [] + + +def setConss(m, vtype="B", val=0, imax=1): + """set numeric constraints to the operators. + val (int): number to which the operators are constraint + imax (int): number of operators affected by the constraint""" + i = 0 + while i < imax: + vi = getVarByName(m, "v%s" % i) + m.addCons(vi == val, vtype) + i += 1 + return + + +def printOutput(m): + """print status and values of variables AFTER optimization.""" + status = m.getStatus() + r = getVarByName(m, "r") + rstr = "%d" % round(m.getVal(r)) + vs = getAllVarsByName(m, "v") + vsstr = "".join(["%d" % round(m.getVal(v)) for v in vs]) + print("Status: %s, resultant: %s, operators: %s" % (status, rstr, vsstr)) + + +# MAIN FUNCTIONS +def main_variable(model, logical, sense="min"): + """r is the BINARY resultant variable + v are BINARY operators + cf. http://scip.zib.de/doc-5.0.1/html/cons__and_8h.php""" + try: + r = getVarByName(model, "r") + vs = getAllVarsByName(model, "v") + # addConsAnd/Or method (Xor: TBI, custom) ### + method_name = "addCons%s" % logical.capitalize() + if method_name == "addConsXor": + n = model.addVar("n", "I") + model.addCons(r+quicksum(vs) == 2*n) + else: + try: + _model_addConsLogical = getattr(model, method_name) + _model_addConsLogical(vs, r) + except AttributeError as e: + raise AttributeError("%s not implemented" % method_name) + model.setObjective(r, sense="%simize" % sense) + model.optimize() + assert model.getStatus() == "optimal" + if verbose: + printOutput(model) + return True + except Exception as e: + if verbose: + print("%s: %s" % (e.__class__.__name__, e)) + return False + + +def main_boolean(model, logical, value=False): + """r is the BOOLEAN rhs (NOT a variable!) + v are BINARY operators + cf. http://scip.zib.de/doc-5.0.1/html/cons__xor_8h.php""" + try: + r = value + vs = getAllVarsByName(model, "v") + # addConsXor method (And/Or: TBI) ### + method_name = "addCons%s" % logical.capitalize() + try: + _model_addConsLogical = getattr(model, method_name) + _model_addConsLogical(vs, r) + except AttributeError as e: + raise AttributeError("%s not implemented" % method_name) + model.optimize() + assert model.getStatus() == "optimal" + if verbose: + printOutput(model) + return True + except Exception as e: + if verbose: + print("%s: %s" % (e.__class__.__name__, e)) + return False + + +# TEST FUNCTIONS +@pytest.mark.parametrize("nconss", [1, 2, "all"]) +@pytest.mark.parametrize("vconss", [0, 1]) +@pytest.mark.parametrize("sense", ["min", "max"]) +@pytest.mark.parametrize("logical", ["and", "or", "xor"]) +@pytest.mark.parametrize("noperators", [2, 20, 51, 100]) +@pytest.mark.parametrize("vtype", ["B"]) +def test_variable(noperators, vtype, logical, sense, vconss, nconss): + if nconss == "all": + nconss = noperators + if vtype in ["I", "C"]: + pytest.skip("unsupported vtype \"%s\" may raise errors or unexpected results" % vtype) + m = setModel(vtype, logical, noperators) + setConss(m, vtype, vconss, nconss) + success = main_variable(m, logical, sense) + assert(success), "Status is not optimal" + + +@pytest.mark.parametrize("nconss", [1, 2, "all"]) +@pytest.mark.parametrize("vconss", [0, 1]) +@pytest.mark.parametrize("value", [False, True]) +@pytest.mark.parametrize("logical", ["xor", "and", "or"]) +@pytest.mark.parametrize("noperators", [2, 20, 51, 100]) +@pytest.mark.parametrize("vtype", ["B"]) +def test_boolean(noperators, vtype, logical, value, vconss, nconss): + if nconss == "all": + nconss = noperators + if vtype in ["I", "C"]: + pytest.skip("unsupported vtype \"%s\" may raise errors or unexpected results" % vtype) + if logical in ["and", "or"]: + pytest.skip("unsupported logical: %s" % vtype) + if logical == "xor" and nconss == noperators and noperators % 2 & vconss != value: + pytest.xfail("addConsXor cannot be %s if an %s number of variables are all constraint to %s" % (value, noperators, vconss)) + m = setModel(vtype, logical, noperators) + setConss(m, vtype, vconss, nconss) + success = main_boolean(m, logical, value) + assert(success), "Test is not successful" diff --git a/tests/test_nonlinear.py b/tests/test_nonlinear.py index aaedeb9c7..2cbd6b9c3 100644 --- a/tests/test_nonlinear.py +++ b/tests/test_nonlinear.py @@ -1,7 +1,6 @@ import pytest -from pyscipopt import Model, quicksum -from pyscipopt.scip import Expr, ExprCons, sqrt +from pyscipopt import Model, quicksum, sqrt # test string with polynomial formulation (uses only Expr) def test_string_poly(): @@ -137,9 +136,9 @@ def test_circle(): m.optimize() bestsol = m.getBestSol() - assert abs(m.getSolVal(bestsol, r) - 5.2543) < 1.0e-4 - assert abs(m.getSolVal(bestsol, a) - 6.1242) < 1.0e-4 - assert abs(m.getSolVal(bestsol, b) - 5.4702) < 1.0e-4 + assert abs(m.getSolVal(bestsol, r) - 5.2543) < 1.0e-3 + assert abs(m.getSolVal(bestsol, a) - 6.1242) < 1.0e-3 + assert abs(m.getSolVal(bestsol, b) - 5.4702) < 1.0e-3 # test gastrans: see example in /examples/CallableLibrary/src/gastrans.c # of course there is a more pythonic/elegant way of implementing this, probably diff --git a/tests/test_quickprod.py b/tests/test_quickprod.py new file mode 100644 index 000000000..04b26e2c6 --- /dev/null +++ b/tests/test_quickprod.py @@ -0,0 +1,42 @@ +from pyscipopt import Model, quickprod +from pyscipopt.scip import CONST +from operator import mul +import functools + +def test_quickprod_model(): + m = Model("quickprod") + x = m.addVar("x") + y = m.addVar("y") + z = m.addVar("z") + c = 2.3 + + q = quickprod([x,y,z,c]) == 0.0 + s = functools.reduce(mul,[x,y,z,c],1) == 0.0 + + assert(q.expr.terms == s.expr.terms) + +def test_quickprod(): + empty = quickprod(1 for i in []) + assert len(empty.terms) == 1 + assert CONST in empty.terms + +def test_largequadratic(): + # inspired from performance issue on + # http://stackoverflow.com/questions/38434300 + + m = Model("dense_quadratic") + dim = 20 + x = [m.addVar("x_%d" % i) for i in range(dim)] + expr = quickprod((i+j+1)*x[i]*x[j] + for i in range(dim) + for j in range(dim)) + cons = expr <= 1.0 + # upper triangle, diagonal + assert cons.expr.degree() == 2*dim*dim + m.addCons(cons) + # TODO: what can we test beyond the lack of crashes? + +if __name__ == "__main__": + test_quickprod() + test_quickprod_model() + test_largequadratic() diff --git a/tests/test_tsp.py b/tests/test_tsp.py index 350b180ff..5e17a06d2 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -55,7 +55,7 @@ def consenfolp(self, constraints, n_useful_conss, sol_infeasible): else: return {"result": SCIP_RESULT.FEASIBLE} - def conslock(self, constraint, nlockspos, nlocksneg): + def conslock(self, constraint, locktype, nlockspos, nlocksneg): pass diff --git a/tests/test_variablebounds.py b/tests/test_variablebounds.py new file mode 100644 index 000000000..85ee87e1a --- /dev/null +++ b/tests/test_variablebounds.py @@ -0,0 +1,48 @@ +from pyscipopt import Model + + +m = Model() + +x0 = m.addVar(lb=-5, ub=8) +r1 = m.addVar() +r2 = m.addVar() +y0 = m.addVar(lb=3) +t = m.addVar(lb=None) +z = m.addVar() + +m.chgVarLbGlobal(x0, -2) +m.chgVarUbGlobal(x0, 4) + +infeas, tightened = m.tightenVarLb(x0, -5) +assert not infeas +assert not tightened +infeas, tightened = m.tightenVarLbGlobal(x0, -1) +assert not infeas +assert tightened +infeas, tightened = m.tightenVarUb(x0, 3) +assert not infeas +assert tightened +infeas, tightened = m.tightenVarUbGlobal(x0, 9) +assert not infeas +assert not tightened +infeas, fixed = m.fixVar(z, 7) +assert not infeas +assert fixed +assert m.delVar(z) + +m.addCons(r1 >= x0) +m.addCons(r2 >= -x0) +m.addCons(y0 == r1 +r2) + +m.setObjective(t) +m.addCons(t >= r1 * (r1 - x0) + r2 * (r2 + x0)) + + +m.optimize() + +print("x0", m.getVal(x0)) +print("r1", m.getVal(r1)) +print("r2", m.getVal(r2)) +print("y0", m.getVal(y0)) +print("t", m.getVal(t)) +