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

Check warning on line 1 in src/pyscipopt/recipes/nonlinear.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/nonlinear.py#L1

Added line #L1 was not covered by tests

def set_nonlinear_objective(model: Model, expr, sense="minimize"):

Check warning on line 3 in src/pyscipopt/recipes/nonlinear.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/nonlinear.py#L3

Added line #L3 was not covered by tests
"""
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()

Check warning on line 15 in src/pyscipopt/recipes/nonlinear.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/nonlinear.py#L8-L15

Added lines #L8 - L15 were not covered by tests
else:
raise Warning("unrecognized optimization sense: %s" % sense)

Check warning on line 17 in src/pyscipopt/recipes/nonlinear.py

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/recipes/nonlinear.py#L17

Added line #L17 was not covered by tests

22 changes: 12 additions & 10 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 262 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand Down Expand Up @@ -1315,24 +1315,26 @@
"""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'):

Check warning on line 1318 in src/pyscipopt/scip.pxi

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/scip.pxi#L1318

Added line #L1318 was not covered by tests
"""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

Check warning on line 1334 in src/pyscipopt/scip.pxi

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/scip.pxi#L1332-L1334

Added lines #L1332 - L1334 were not covered by tests

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")

Check warning on line 1337 in src/pyscipopt/scip.pxi

View check run for this annotation

Codecov / codecov/patch

src/pyscipopt/scip.pxi#L1337

Added line #L1337 was not covered by tests

if clear:
# clear existing objective function
Expand All @@ -1342,10 +1344,10 @@
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
Loading