Skip to content

Commit e8ec238

Browse files
Joao-DionisioOpt-Muccammghannam
authored
Add possibility for nonlinear objective function (#781)
* addPiecewiseLinearCons method and test * Update CHANGELOG * Fix recommendations * Add possibility for nonlinear objective * Add nonlinear objective test * Modify other tests * Update CHANGELOG * Add untested changes to fix test and assert * Move to recipes folder * Update CHANGELOG * Fix segfault * Reset locale to standard * Fix test * Small refactoring --------- Co-authored-by: Mark Turner <turner@zib.de> Co-authored-by: Mohammed Ghannam <mohammad.m.ghannam@gmail.com>
1 parent 30148ab commit e8ec238

9 files changed

+96
-19
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- Added recipe for nonlinear objective functions
56
- Added method for adding piecewise linear constraints
67
- Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation
78
- New test for model setLogFile

src/pyscipopt/recipes/nonlinear.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from pyscipopt import Model
2+
3+
def set_nonlinear_objective(model: Model, expr, sense="minimize"):
4+
"""
5+
Takes a nonlinear expression and performs an epigraph reformulation.
6+
"""
7+
8+
assert expr.degree() > 1, "For linear objectives, please use the setObjective method."
9+
new_obj = model.addVar(lb=-float("inf"),obj=1)
10+
if sense == "minimize":
11+
model.addCons(expr <= new_obj)
12+
model.setMinimize()
13+
elif sense == "maximize":
14+
model.addCons(expr >= new_obj)
15+
model.setMaximize()
16+
else:
17+
raise Warning("unrecognized optimization sense: %s" % sense)
18+

src/pyscipopt/scip.pxi

+12-10
Original file line numberDiff line numberDiff line change
@@ -1315,24 +1315,26 @@ cdef class Model:
13151315
"""returns current limit on objective function."""
13161316
return SCIPgetObjlimit(self._scip)
13171317

1318-
def setObjective(self, coeffs, sense = 'minimize', clear = 'true'):
1318+
def setObjective(self, expr, sense = 'minimize', clear = 'true'):
13191319
"""Establish the objective function as a linear expression.
13201320
1321-
:param coeffs: the coefficients
1321+
:param expr: the objective function SCIP Expr, or constant value
13221322
:param sense: the objective sense (Default value = 'minimize')
13231323
:param clear: set all other variables objective coefficient to zero (Default value = 'true')
13241324
13251325
"""
1326+
13261327
cdef SCIP_VAR** _vars
13271328
cdef int _nvars
13281329

13291330
# turn the constant value into an Expr instance for further processing
1330-
if not isinstance(coeffs, Expr):
1331-
assert(_is_number(coeffs)), "given coefficients are neither Expr or number but %s" % coeffs.__class__.__name__
1332-
coeffs = Expr() + coeffs
1331+
if not isinstance(expr, Expr):
1332+
print(expr)
1333+
assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__
1334+
expr = Expr() + expr
13331335

1334-
if coeffs.degree() > 1:
1335-
raise ValueError("Nonlinear objective functions are not supported!")
1336+
if expr.degree() > 1:
1337+
raise ValueError("SCIP does not support nonlinear objective functions. Consider using set_nonlinear_objective in the pyscipopt.recipe.nonlinear")
13361338

13371339
if clear:
13381340
# clear existing objective function
@@ -1342,10 +1344,10 @@ cdef class Model:
13421344
for i in range(_nvars):
13431345
PY_SCIP_CALL(SCIPchgVarObj(self._scip, _vars[i], 0.0))
13441346

1345-
if coeffs[CONST] != 0.0:
1346-
self.addObjoffset(coeffs[CONST])
1347+
if expr[CONST] != 0.0:
1348+
self.addObjoffset(expr[CONST])
13471349

1348-
for term, coef in coeffs.terms.items():
1350+
for term, coef in expr.terms.items():
13491351
# avoid CONST term of Expr
13501352
if term != CONST:
13511353
assert len(term) == 1

tests/test_cons.py

-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ def test_printCons():
152152

153153
m.printCons(c)
154154

155-
156155
@pytest.mark.skip(reason="TODO: test getValsLinear()")
157156
def test_getValsLinear():
158157
assert True

tests/test_linexpr.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,4 @@ def test_objective(model):
215215

216216
# setting affine objective
217217
m.setObjective(x + y + 1)
218-
assert m.getObjoffset() == 1
219-
220-
# setting nonlinear objective
221-
with pytest.raises(ValueError):
222-
m.setObjective(x ** 2 - y * z)
223-
218+
assert m.getObjoffset() == 1

tests/test_model.py

+10
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ def test_getStage():
288288
assert m.getStage() == SCIP_STAGE.SOLVED
289289
assert m.getStageName() == "SOLVED"
290290

291+
def test_getObjective():
292+
m = Model()
293+
m.addVar(obj=2, name="x1")
294+
m.addVar(obj=3, name="x2")
295+
296+
assert str(m.getObjective()) == "Expr({Term(x1): 2.0, Term(x2): 3.0})"
297+
298+
291299
def test_getTreesizeEstimation():
292300
m = Model()
293301

@@ -349,3 +357,5 @@ def test_locale():
349357

350358
with open("model.cip") as file:
351359
assert "1,1" not in file.read()
360+
361+
locale.setlocale(locale.LC_NUMERIC,"")

tests/test_nonlinear.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
2+
import random
23

3-
from pyscipopt import Model, quicksum, sqrt
4+
from pyscipopt import Model, quicksum, sqrt, exp, log, sin
45

56
# test string with polynomial formulation (uses only Expr)
67
def test_string_poly():

tests/test_recipe_nonlinear.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pyscipopt import Model, exp, log, sqrt, sin
2+
from pyscipopt.recipes.nonlinear import set_nonlinear_objective
3+
4+
def test_nonlinear_objective():
5+
model = Model()
6+
7+
v = model.addVar()
8+
w = model.addVar()
9+
x = model.addVar()
10+
y = model.addVar()
11+
z = model.addVar()
12+
13+
obj = 0
14+
obj += exp(v)
15+
obj += log(w)
16+
obj += sqrt(x)
17+
obj += sin(y)
18+
obj += z**3 * y
19+
20+
model.addCons(v + w + x + y + z <= 1)
21+
set_nonlinear_objective(model, obj, sense='maximize')
22+
23+
model2 = Model()
24+
25+
a = model2.addVar()
26+
b = model2.addVar()
27+
c = model2.addVar()
28+
d = model2.addVar()
29+
e = model2.addVar()
30+
31+
obj2 = 0
32+
obj2 += exp(a)
33+
obj2 += log(b)
34+
obj2 += sqrt(c)
35+
obj2 += sin(d)
36+
obj2 += e**3 * d
37+
38+
model2.addCons(a + b + c + d + e <= 1)
39+
40+
t = model2.addVar(lb=-float("inf"),obj=1)
41+
model2.addCons(t <= obj2)
42+
model2.setMaximize()
43+
44+
obj_expr = model.getObjective()
45+
assert obj_expr.degree() == 1
46+
47+
model.setParam("numerics/epsilon", 10**(-5)) # bigger eps due to nonlinearities
48+
model2.setParam("numerics/epsilon", 10**(-5))
49+
50+
model.optimize()
51+
model2.optimize()
52+
assert model.isEQ(model.getObjVal(), model2.getObjVal())

tests/test_piecewise.py renamed to tests/test_recipe_piecewise.py

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ def test_add_piecewise_linear_cons():
1414
m.optimize()
1515
assert m.isEQ(m.getObjVal(), -2)
1616

17-
1817
def test_add_piecewise_linear_cons2():
1918
m = Model()
2019

0 commit comments

Comments
 (0)