Skip to content

Add possibility for nonlinear objective function #781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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