Skip to content

Commit

Permalink
Add possibility for nonlinear objective function (#781)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Mohammed Ghannam <[email protected]>
  • Loading branch information
3 people authored Apr 1, 2024
1 parent 30148ab commit e8ec238
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Added
- Added recipe for nonlinear objective functions
- Added method for adding piecewise linear constraints
- Add SCIP function SCIPgetTreesizeEstimation and wrapper getTreesizeEstimation
- New test for model setLogFile
Expand Down
18 changes: 18 additions & 0 deletions src/pyscipopt/recipes/nonlinear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pyscipopt import Model

def set_nonlinear_objective(model: Model, expr, sense="minimize"):
"""
Takes a nonlinear expression and performs an epigraph reformulation.
"""

assert expr.degree() > 1, "For linear objectives, please use the setObjective method."
new_obj = model.addVar(lb=-float("inf"),obj=1)
if sense == "minimize":
model.addCons(expr <= new_obj)
model.setMinimize()
elif sense == "maximize":
model.addCons(expr >= new_obj)
model.setMaximize()
else:
raise Warning("unrecognized optimization sense: %s" % sense)

22 changes: 12 additions & 10 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -1315,24 +1315,26 @@ cdef class Model:
"""returns current limit on objective function."""
return SCIPgetObjlimit(self._scip)

def setObjective(self, coeffs, sense = 'minimize', clear = 'true'):
def setObjective(self, expr, sense = 'minimize', clear = 'true'):
"""Establish the objective function as a linear expression.
:param coeffs: the coefficients
:param expr: the objective function SCIP Expr, or constant value
:param sense: the objective sense (Default value = 'minimize')
:param clear: set all other variables objective coefficient to zero (Default value = 'true')
"""

cdef SCIP_VAR** _vars
cdef int _nvars

# 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 not isinstance(expr, Expr):
print(expr)
assert(_is_number(expr)), "given coefficients are neither Expr or number but %s" % expr.__class__.__name__
expr = Expr() + expr

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

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

if coeffs[CONST] != 0.0:
self.addObjoffset(coeffs[CONST])
if expr[CONST] != 0.0:
self.addObjoffset(expr[CONST])

for term, coef in coeffs.terms.items():
for term, coef in expr.terms.items():
# avoid CONST term of Expr
if term != CONST:
assert len(term) == 1
Expand Down
1 change: 0 additions & 1 deletion tests/test_cons.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ def test_printCons():

m.printCons(c)


@pytest.mark.skip(reason="TODO: test getValsLinear()")
def test_getValsLinear():
assert True
Expand Down
7 changes: 1 addition & 6 deletions tests/test_linexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,4 @@ def test_objective(model):

# setting affine objective
m.setObjective(x + y + 1)
assert m.getObjoffset() == 1

# setting nonlinear objective
with pytest.raises(ValueError):
m.setObjective(x ** 2 - y * z)

assert m.getObjoffset() == 1
10 changes: 10 additions & 0 deletions tests/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ def test_getStage():
assert m.getStage() == SCIP_STAGE.SOLVED
assert m.getStageName() == "SOLVED"

def test_getObjective():
m = Model()
m.addVar(obj=2, name="x1")
m.addVar(obj=3, name="x2")

assert str(m.getObjective()) == "Expr({Term(x1): 2.0, Term(x2): 3.0})"


def test_getTreesizeEstimation():
m = Model()

Expand Down Expand Up @@ -349,3 +357,5 @@ def test_locale():

with open("model.cip") as file:
assert "1,1" not in file.read()

locale.setlocale(locale.LC_NUMERIC,"")
3 changes: 2 additions & 1 deletion tests/test_nonlinear.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
import random

from pyscipopt import Model, quicksum, sqrt
from pyscipopt import Model, quicksum, sqrt, exp, log, sin

# test string with polynomial formulation (uses only Expr)
def test_string_poly():
Expand Down
52 changes: 52 additions & 0 deletions tests/test_recipe_nonlinear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from pyscipopt import Model, exp, log, sqrt, sin
from pyscipopt.recipes.nonlinear import set_nonlinear_objective

def test_nonlinear_objective():
model = Model()

v = model.addVar()
w = model.addVar()
x = model.addVar()
y = model.addVar()
z = model.addVar()

obj = 0
obj += exp(v)
obj += log(w)
obj += sqrt(x)
obj += sin(y)
obj += z**3 * y

model.addCons(v + w + x + y + z <= 1)
set_nonlinear_objective(model, obj, sense='maximize')

model2 = Model()

a = model2.addVar()
b = model2.addVar()
c = model2.addVar()
d = model2.addVar()
e = model2.addVar()

obj2 = 0
obj2 += exp(a)
obj2 += log(b)
obj2 += sqrt(c)
obj2 += sin(d)
obj2 += e**3 * d

model2.addCons(a + b + c + d + e <= 1)

t = model2.addVar(lb=-float("inf"),obj=1)
model2.addCons(t <= obj2)
model2.setMaximize()

obj_expr = model.getObjective()
assert obj_expr.degree() == 1

model.setParam("numerics/epsilon", 10**(-5)) # bigger eps due to nonlinearities
model2.setParam("numerics/epsilon", 10**(-5))

model.optimize()
model2.optimize()
assert model.isEQ(model.getObjVal(), model2.getObjVal())
1 change: 0 additions & 1 deletion tests/test_piecewise.py → tests/test_recipe_piecewise.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def test_add_piecewise_linear_cons():
m.optimize()
assert m.isEQ(m.getObjVal(), -2)


def test_add_piecewise_linear_cons2():
m = Model()

Expand Down

0 comments on commit e8ec238

Please sign in to comment.