From a146f5201c4c31d5d7cb76a46f08204b5c1fdb3a Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 11 Jan 2024 13:53:49 +0000 Subject: [PATCH 01/11] addPiecewiseLinearCons method and test --- src/pyscipopt/scip.pxi | 31 ++++++++++++++++++++++++++++++- tests/test_cons.py | 12 ++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 0b2332d88..833f23182 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2602,7 +2602,7 @@ cdef class Model: 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) + return Constraint.create(scip_cons) def addConsAnd(self, vars, resvar, name="ANDcons", initial=True, separate=True, enforce=True, check=True, @@ -2834,6 +2834,35 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) return pyCons + + def addPiecewiseLinearCons(self, X, Y, a, b): + """add piecewise relation with multiple selection formulation + + :param a: array with x-coordinates of the points in the piecewise linear relation + :param b: array with y-coordinate of the points in the piecewise linear relation + """ + K = len(a)-1 + w,z = {},{} + for k in range(K): + w[k] = self.addVar(lb=-self.infinity()) + z[k] = self.addVar(vtype="B") + + #X = self.addVar(lb=a[0], ub=a[K]) + #Y = self.addVar(lb=-self.infinity()) + + for k in range(K): + self.addCons(w[k] >= a[k]*z[k]) + self.addCons(w[k] <= a[k+1]*z[k]) + + self.addConsSOS1([z[k] for k in range(K)]) + self.addCons(X == quicksum(w[k] for k in range(K))) + + c = [float(b[k+1]-b[k])/(a[k+1]-a[k]) for k in range(K)] + d = [b[k]-c[k]*a[k] for k in range(K)] + + new_cons = self.addCons(Y == quicksum(d[k]*z[k] + c[k]*w[k] for k in range(K))) + + return new_cons def getSlackVarIndicator(self, Constraint cons): """Get slack variable of an indicator constraint. diff --git a/tests/test_cons.py b/tests/test_cons.py index c3f1f1978..00d5d1ae8 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -153,6 +153,18 @@ def test_printCons(): m.printCons(c) +def test_addPiecewiseLinearCons(): + m = Model() + + xpoints = [1,3,5] + ypoints = [1,2,4] + x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) + y = m.addVar(lb=-m.infinity(), obj=-3) + m.addPiecewiseLinearCons(x,y,xpoints,ypoints) + + m.optimize() + assert m.isEQ(m.getObjVal(), -2) + @pytest.mark.skip(reason="TODO: test getValsLinear()") def test_getValsLinear(): assert True From 8eae73af02715af92f109546a54480e33dc18af2 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 11 Jan 2024 13:54:20 +0000 Subject: [PATCH 02/11] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e408517d..423a8977a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added method for adding piecewise linear constraints - Added methods for getting the names of the current stage and of an event ### Fixed - Fixed outdated time.clock call in gcp.py From 1a32614154e737980cca7bd080adfe5a2c353173 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 15 Jan 2024 11:50:09 +0000 Subject: [PATCH 03/11] Fix recommendations --- src/pyscipopt/scip.pxi | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 833f23182..dd5b33962 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2836,29 +2836,34 @@ cdef class Model: return pyCons def addPiecewiseLinearCons(self, X, Y, a, b): - """add piecewise relation with multiple selection formulation + """add constraint of the form y = f(x), where f is a piecewise linear function + :param X: x variable + :param Y: y variable :param a: array with x-coordinates of the points in the piecewise linear relation :param b: array with y-coordinate of the points in the piecewise linear relation + + Disclaimer: For the moment, can only model 2d piecewise linear functions + Adapted from https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished/piecewise.py """ + assert len(a) == len(b), "Must have the same number of x and y-coordinates" + K = len(a)-1 w,z = {},{} for k in range(K): w[k] = self.addVar(lb=-self.infinity()) z[k] = self.addVar(vtype="B") - #X = self.addVar(lb=a[0], ub=a[K]) - #Y = self.addVar(lb=-self.infinity()) - for k in range(K): self.addCons(w[k] >= a[k]*z[k]) self.addCons(w[k] <= a[k+1]*z[k]) - self.addConsSOS1([z[k] for k in range(K)]) + self.addCons(quicksum(z[k] for k in range(K)) == 1) + self.addCons(X == quicksum(w[k] for k in range(K))) - c = [float(b[k+1]-b[k])/(a[k+1]-a[k]) for k in range(K)] - d = [b[k]-c[k]*a[k] for k in range(K)] + c = [float(b[k+1]-b[k]) / (a[k+1]-a[k]) for k in range(K)] + d = [b[k] - c[k]*a[k] for k in range(K)] new_cons = self.addCons(Y == quicksum(d[k]*z[k] + c[k]*w[k] for k in range(K))) From 0d5355ba7996332bb86452955700f424a56a9019 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Sun, 25 Feb 2024 13:30:22 +0100 Subject: [PATCH 04/11] Add structure to put subpackages --- setup.py | 4 ++-- src/pyscipopt/formulations/__init__.py | 0 src/pyscipopt/formulations/piecewise.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/pyscipopt/formulations/__init__.py create mode 100644 src/pyscipopt/formulations/piecewise.py diff --git a/setup.py b/setup.py index b90151601..41587c9c0 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, Extension +from setuptools import find_packages, setup, Extension import os, platform, sys, re # look for environment variable that specifies path to SCIP @@ -129,7 +129,7 @@ "Topic :: Scientific/Engineering :: Mathematics", ], ext_modules=extensions, - packages=["pyscipopt"], + packages=find_packages(where="src"), package_dir={"pyscipopt": packagedir}, package_data={"pyscipopt": ["scip.pyx", "scip.pxd", "*.pxi"]}, ) diff --git a/src/pyscipopt/formulations/__init__.py b/src/pyscipopt/formulations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyscipopt/formulations/piecewise.py b/src/pyscipopt/formulations/piecewise.py new file mode 100644 index 000000000..e69de29bb From 88a5d2a08781d4c518927525dd76d9c7acfe4482 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 10:29:22 +0200 Subject: [PATCH 05/11] Rename subpackage & move test --- .../{formulations => recipes}/__init__.py | 0 .../{formulations => recipes}/piecewise.py | 0 tests/test_cons.py | 12 ------------ tests/test_piecewise.py | 15 +++++++++++++++ 4 files changed, 15 insertions(+), 12 deletions(-) rename src/pyscipopt/{formulations => recipes}/__init__.py (100%) rename src/pyscipopt/{formulations => recipes}/piecewise.py (100%) create mode 100644 tests/test_piecewise.py diff --git a/src/pyscipopt/formulations/__init__.py b/src/pyscipopt/recipes/__init__.py similarity index 100% rename from src/pyscipopt/formulations/__init__.py rename to src/pyscipopt/recipes/__init__.py diff --git a/src/pyscipopt/formulations/piecewise.py b/src/pyscipopt/recipes/piecewise.py similarity index 100% rename from src/pyscipopt/formulations/piecewise.py rename to src/pyscipopt/recipes/piecewise.py diff --git a/tests/test_cons.py b/tests/test_cons.py index 00d5d1ae8..c3f1f1978 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -153,18 +153,6 @@ def test_printCons(): m.printCons(c) -def test_addPiecewiseLinearCons(): - m = Model() - - xpoints = [1,3,5] - ypoints = [1,2,4] - x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) - y = m.addVar(lb=-m.infinity(), obj=-3) - m.addPiecewiseLinearCons(x,y,xpoints,ypoints) - - m.optimize() - assert m.isEQ(m.getObjVal(), -2) - @pytest.mark.skip(reason="TODO: test getValsLinear()") def test_getValsLinear(): assert True diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py new file mode 100644 index 000000000..e88058ee1 --- /dev/null +++ b/tests/test_piecewise.py @@ -0,0 +1,15 @@ + +from pyscipopt import Model + + +def test_addPiecewiseLinearCons(): + m = Model() + + xpoints = [1, 3, 5] + ypoints = [1, 2, 4] + x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) + y = m.addVar(lb=-m.infinity(), obj=-3) + m.addPiecewiseLinearCons(x, y, xpoints, ypoints) + + m.optimize() + assert m.isEQ(m.getObjVal(), -2) From 7696cab95123fc24aedf973526a6db8b20a03142 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 10:32:36 +0200 Subject: [PATCH 06/11] Add another test --- tests/test_piecewise.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py index e88058ee1..57ec041ba 100644 --- a/tests/test_piecewise.py +++ b/tests/test_piecewise.py @@ -13,3 +13,18 @@ def test_addPiecewiseLinearCons(): m.optimize() assert m.isEQ(m.getObjVal(), -2) + + +def test_addPiecewiseLinearCons2(): + m = Model() + + xpoints = [1, 3, 5] + ypoints = [1, 2, 4] + x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) + y = m.addVar(lb=-m.infinity(), obj=-3) + m.addPiecewiseLinearCons(x, y, xpoints, ypoints) + + m.setMaximize() + + m.optimize() + assert m.isEQ(m.getObjVal(), 0) From 9ad111e7c487e4494c5324417afb26f32960ea19 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 10:55:09 +0200 Subject: [PATCH 07/11] Move piecewise function to its own module --- src/pyscipopt/__init__.py | 2 ++ src/pyscipopt/recipes/piecewise.py | 36 ++++++++++++++++++++++++++++++ src/pyscipopt/scip.pxi | 34 ---------------------------- tests/test_piecewise.py | 10 ++++----- 4 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index 8221fb130..949d70adc 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -9,6 +9,8 @@ # export user-relevant objects: from pyscipopt.Multidict import multidict from pyscipopt.scip import Model +from pyscipopt.scip import Variable +from pyscipopt.scip import Constraint from pyscipopt.scip import Benders from pyscipopt.scip import Benderscut from pyscipopt.scip import Branchrule diff --git a/src/pyscipopt/recipes/piecewise.py b/src/pyscipopt/recipes/piecewise.py index e69de29bb..7540414da 100644 --- a/src/pyscipopt/recipes/piecewise.py +++ b/src/pyscipopt/recipes/piecewise.py @@ -0,0 +1,36 @@ + +from pyscipopt import Model, quicksum, Variable, Constraint + +def add_piecewise_linear_cons(model: Model, X: Variable, Y: Variable, a: list[float], b: list[float]) -> Constraint: + """add constraint of the form y = f(x), where f is a piecewise linear function + + :param X: x variable + :param Y: y variable + :param a: array with x-coordinates of the points in the piecewise linear relation + :param b: array with y-coordinate of the points in the piecewise linear relation + + Disclaimer: For the moment, can only model 2d piecewise linear functions + Adapted from https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished/piecewise.py + """ + assert len(a) == len(b), "Must have the same number of x and y-coordinates" + + K = len(a)-1 + w,z = {},{} + for k in range(K): + w[k] = model.addVar(lb=-model.infinity()) + z[k] = model.addVar(vtype="B") + + for k in range(K): + model.addCons(w[k] >= a[k]*z[k]) + model.addCons(w[k] <= a[k+1]*z[k]) + + model.addCons(quicksum(z[k] for k in range(K)) == 1) + + model.addCons(X == quicksum(w[k] for k in range(K))) + + c = [float(b[k+1]-b[k]) / (a[k+1]-a[k]) for k in range(K)] + d = [b[k] - c[k]*a[k] for k in range(K)] + + new_cons = model.addCons(Y == quicksum(d[k]*z[k] + c[k]*w[k] for k in range(K))) + + return new_cons diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index dd5b33962..ab0753bed 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2835,40 +2835,6 @@ cdef class Model: return pyCons - def addPiecewiseLinearCons(self, X, Y, a, b): - """add constraint of the form y = f(x), where f is a piecewise linear function - - :param X: x variable - :param Y: y variable - :param a: array with x-coordinates of the points in the piecewise linear relation - :param b: array with y-coordinate of the points in the piecewise linear relation - - Disclaimer: For the moment, can only model 2d piecewise linear functions - Adapted from https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished/piecewise.py - """ - assert len(a) == len(b), "Must have the same number of x and y-coordinates" - - K = len(a)-1 - w,z = {},{} - for k in range(K): - w[k] = self.addVar(lb=-self.infinity()) - z[k] = self.addVar(vtype="B") - - for k in range(K): - self.addCons(w[k] >= a[k]*z[k]) - self.addCons(w[k] <= a[k+1]*z[k]) - - self.addCons(quicksum(z[k] for k in range(K)) == 1) - - self.addCons(X == quicksum(w[k] for k in range(K))) - - c = [float(b[k+1]-b[k]) / (a[k+1]-a[k]) for k in range(K)] - d = [b[k] - c[k]*a[k] for k in range(K)] - - new_cons = self.addCons(Y == quicksum(d[k]*z[k] + c[k]*w[k] for k in range(K))) - - return new_cons - def getSlackVarIndicator(self, Constraint cons): """Get slack variable of an indicator constraint. diff --git a/tests/test_piecewise.py b/tests/test_piecewise.py index 57ec041ba..b2aa11be4 100644 --- a/tests/test_piecewise.py +++ b/tests/test_piecewise.py @@ -1,28 +1,28 @@ from pyscipopt import Model +from pyscipopt.recipes.piecewise import add_piecewise_linear_cons - -def test_addPiecewiseLinearCons(): +def test_add_piecewise_linear_cons(): m = Model() xpoints = [1, 3, 5] ypoints = [1, 2, 4] x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) y = m.addVar(lb=-m.infinity(), obj=-3) - m.addPiecewiseLinearCons(x, y, xpoints, ypoints) + add_piecewise_linear_cons(m, x, y, xpoints, ypoints) m.optimize() assert m.isEQ(m.getObjVal(), -2) -def test_addPiecewiseLinearCons2(): +def test_add_piecewise_linear_cons2(): m = Model() xpoints = [1, 3, 5] ypoints = [1, 2, 4] x = m.addVar(lb=xpoints[0], ub=xpoints[-1], obj=2) y = m.addVar(lb=-m.infinity(), obj=-3) - m.addPiecewiseLinearCons(x, y, xpoints, ypoints) + add_piecewise_linear_cons(m, x, y, xpoints, ypoints) m.setMaximize() From 65bdedf86536a9821698198bccdf2e54a98b58e1 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 11:09:24 +0200 Subject: [PATCH 08/11] Add readme for recipes subpackage --- src/pyscipopt/recipes/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/pyscipopt/recipes/README.md diff --git a/src/pyscipopt/recipes/README.md b/src/pyscipopt/recipes/README.md new file mode 100644 index 000000000..9942db0c3 --- /dev/null +++ b/src/pyscipopt/recipes/README.md @@ -0,0 +1,3 @@ +# Recipes sub-package + +This sub-package provides a set of functions for common usecases for pyscipopt. This sub-package is for all functions that don't necessarily reflect the core functionality of SCIP, but are useful for working with the solver. The functions implemented in this sub-package might not be the most efficient way to solve/formulate a problem but would provide a good starting point. From 0f04a769a6c041f3ea10e41c364ca0cccdaf54d9 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 11:12:53 +0200 Subject: [PATCH 09/11] Update readme to mention recipes sub-package --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index deedffd47..f3644dc2f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Building and solving a model There are several [examples](https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished) and [tutorials](https://github.com/scipopt/PySCIPOpt/blob/master/examples/tutorial). These display some functionality of the -interface and can serve as an entry point for writing more complex code. +interface and can serve as an entry point for writing more complex code. Some of the common usecases are also avaialble in the [recipes](https://github.com/scipopt/PySCIPOpt/blob/master/src/pyscipopt/recipes) sub-package. You might also want to have a look at this article about PySCIPOpt: . The following steps are always required when using the interface: From 54318359fc16ce9d9f0758feea4706aa8856f1ba Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 11:14:42 +0200 Subject: [PATCH 10/11] Fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3644dc2f..d30ebdbb5 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Building and solving a model There are several [examples](https://github.com/scipopt/PySCIPOpt/blob/master/examples/finished) and [tutorials](https://github.com/scipopt/PySCIPOpt/blob/master/examples/tutorial). These display some functionality of the -interface and can serve as an entry point for writing more complex code. Some of the common usecases are also avaialble in the [recipes](https://github.com/scipopt/PySCIPOpt/blob/master/src/pyscipopt/recipes) sub-package. +interface and can serve as an entry point for writing more complex code. Some of the common usecases are also available in the [recipes](https://github.com/scipopt/PySCIPOpt/blob/master/src/pyscipopt/recipes) sub-package. You might also want to have a look at this article about PySCIPOpt: . The following steps are always required when using the interface: From 3809c618f1f229d6f3bb52566d518fc9b5556dd7 Mon Sep 17 00:00:00 2001 From: Mohammed Ghannam Date: Mon, 1 Apr 2024 11:17:59 +0200 Subject: [PATCH 11/11] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f18b37e..83bc76e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - Added method for adding piecewise linear constraints - Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation +- Add recipes sub-package ### Fixed ### Changed ### Removed