Skip to content

Add support for knapsack constraints #975

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

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 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 support for knapsack constraints
- Added getLinearConsIndicator
- Added SCIP_LPPARAM, setIntParam, setRealParam, getIntParam, getRealParam, isOptimal, getObjVal, getRedcost for lpi
- Added isFeasPositive
Expand Down
33 changes: 33 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,39 @@ cdef extern from "scip/cons_linear.h":
SCIP_Real* SCIPgetValsLinear(SCIP* scip, SCIP_CONS* cons)
SCIP_ROW* SCIPgetRowLinear(SCIP* scip, SCIP_CONS* cons)

cdef extern from "scip/cons_knapsack.h"
SCIP_RETCODE SCIPcreateConsKnapsack(SCIP* scip,
SCIP_CONS** cons,
char* name,
int nvars,
SCIP_VAR** vars,
SCIP_Real* vals,
SCIP_Real lhs,
SCIP_Real rhs,
SCIP_Bool initial,
SCIP_Bool separate,
SCIP_Bool enforce,
SCIP_Bool check,
SCIP_Bool propagate,
SCIP_Bool local,
SCIP_Bool modifiable,
SCIP_Bool dynamic,
SCIP_Bool removable,
SCIP_Bool stickingatnode)
SCIP_RETCODE SCIPaddCoefKnapsack(SCIP* scip,
SCIP_CONS* cons,
SCIP_VAR* var,
SCIP_Real val)

SCIP_Real SCIPgetDualsolKnapsack(SCIP* scip, SCIP_CONS* cons)
SCIP_Real SCIPgetDualfarkasKnapsack(SCIP* scip, SCIP_CONS* cons)
SCIP_RETCODE SCIPchgCapacityKnapsack(SCIP* scip, SCIP_CONS* cons, SCIP_Real rhs)
SCIP_Real SCIPgetCapacityKnapsack(SCIP* scip, SCIP_CONS* cons)
SCIP_RETCODE SCIPaddCoefKnapsack(SCIP* scip, SCIP_CONS* cons, SCIP_VAR*, SCIP_Real val)
SCIP_VAR** SCIPgetVarsKnapsack(SCIP* scip, SCIP_CONS* cons)
int SCIPgetNVarsKnapsack(SCIP* scip, SCIP_CONS* cons)
SCIP_Real* SCIPgetWeightsKnapsack(SCIP* scip, SCIP_CONS* cons)

cdef extern from "scip/cons_nonlinear.h":
SCIP_EXPR* SCIPgetExprNonlinear(SCIP_CONS* cons)
SCIP_RETCODE SCIPcreateConsNonlinear(SCIP* scip,
Expand Down
174 changes: 167 additions & 7 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -2057,6 +2057,19 @@ cdef class Constraint:
"""
constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8')
return constype == 'linear'

def isKnapsack(self):
"""
Returns True if constraint is a knapsack constraint.
This is a special case of a linear constraint.

Returns
-------
bool

"""
constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(self.scip_cons))).decode('UTF-8')
return constype == 'knapsack'

def isNonlinear(self):
"""
Expand Down Expand Up @@ -4889,6 +4902,47 @@ cdef class Model:

return PyCons

def _createConsKnapsack(self, ExprCons knapsackcons, **kwargs):
"""
The function for creating a knapsack constraint, but not adding it to the Model.
Please do not use this function directly, but rather use createConsFromExpr

Parameters
----------
knapsackcons : ExprCons
kwargs : dict, optional

Returns
-------
Constraint

"""
assert isinstance(knapsackcons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__

cdef int nvars = len(terms.items())
cdef SCIP_VAR** vars_array = <SCIP_VAR**> malloc(nvars * sizeof(SCIP_VAR*))
cdef SCIP_Real* weights_array = <SCIP_Real*> malloc(nvars * sizeof(SCIP_Real))
cdef SCIP_CONS* scip_cons
cdef SCIP_Real coeff
cdef int i

for i, (key, weight) in enumerate(terms.items()):
vars_array[i] = <SCIP_VAR*>(<Variable>key[0]).scip_var
weights_array[i] = <SCIP_Real>weight

PY_SCIP_CALL(SCIPcreateConsKnapsack(
self._scip, &scip_cons, str_conversion(kwargs['name']), nvars, vars_array, weights_array,
kwargs['rhs'], kwargs['initial'], kwargs['separate'], kwargs['enforce'],
kwargs['check'], kwargs['propagate'], kwargs['local'], kwargs['modifiable'],
kwargs['dynamic'], kwargs['removable'], kwargs['stickingatnode']))

PyCons = Constraint.create(scip_cons)

free(vars_array)
free(coeffs_array)

return PyCons

def _createConsQuadratic(self, ExprCons quadcons, **kwargs):
"""
The function for creating a quadratic constraint, but not adding it to the Model.
Expand Down Expand Up @@ -5154,7 +5208,7 @@ cdef class Model:
def createConsFromExpr(self, cons, name='', initial=True, separate=True,
enforce=True, check=True, propagate=True, local=False,
modifiable=False, dynamic=False, removable=False,
stickingatnode=False):
stickingatnode=False, knapsack=False):
"""
Create a linear or nonlinear constraint without adding it to the SCIP problem.
This is useful for creating disjunction constraints without also enforcing the individual constituents.
Expand Down Expand Up @@ -5188,6 +5242,8 @@ cdef class Model:
stickingatnode : bool, optional
should the constraint always be kept at the node where it was added,
even if it may be moved to a more global node? (Default value = False)
knapsack : bool, optional
should the constraint be treated as a knapsack constraint? (Default value = False)

Returns
-------
Expand All @@ -5203,7 +5259,12 @@ cdef class Model:
propagate=propagate, local=local,
modifiable=modifiable, dynamic=dynamic,
removable=removable,
stickingatnode=stickingatnode)
stickingatnode=stickingatnode,
knapsack=knapsack)

if kwargs["knapsack"]:
return self._createConsKnapsack(cons, **kwargs)

kwargs['lhs'] = -SCIPinfinity(self._scip) if cons._lhs is None else cons._lhs
kwargs['rhs'] = SCIPinfinity(self._scip) if cons._rhs is None else cons._rhs

Expand All @@ -5221,7 +5282,7 @@ cdef class Model:
def addCons(self, cons, name='', initial=True, separate=True,
enforce=True, check=True, propagate=True, local=False,
modifiable=False, dynamic=False, removable=False,
stickingatnode=False):
stickingatnode=False, knapsack=False):
"""
Add a linear or nonlinear constraint.

Expand Down Expand Up @@ -5252,6 +5313,8 @@ cdef class Model:
stickingatnode : bool, optional
should the constraints always be kept at the node where it was added,
even if it may be moved to a more global node? (Default value = False)
knapsack : bool, optional
should the constraint be treated as a knapsack constraint? (Default value = False)

Returns
-------
Expand All @@ -5268,7 +5331,8 @@ cdef class Model:
propagate=propagate, local=local,
modifiable=modifiable, dynamic=dynamic,
removable=removable,
stickingatnode=stickingatnode)
stickingatnode=stickingatnode,
knapsack=knapsack)
# we have to pass this back to a SCIP_CONS*
# object to create a new python constraint & handle constraint release
# correctly. Otherwise, segfaults when trying to query information
Expand Down Expand Up @@ -6446,6 +6510,8 @@ cdef class Model:
PY_SCIP_CALL(SCIPchgRhsLinear(self._scip, cons.scip_cons, rhs))
elif constype == 'nonlinear':
PY_SCIP_CALL(SCIPchgRhsNonlinear(self._scip, cons.scip_cons, rhs))
elif constype == "knapsack":
PY_SCIP_CALL(SCIPchgCapacityKnapsack(self._scip, cons.scip_cons, rhs))
else:
raise Warning("method cannot be called for constraints of type " + constype)

Expand Down Expand Up @@ -6475,7 +6541,7 @@ cdef class Model:

def getRhs(self, Constraint cons):
"""
Retrieve right-hand side value of a constraint.
Retrieve right-hand side value of a linear constraint.

Parameters
----------
Expand All @@ -6492,6 +6558,8 @@ cdef class Model:
return SCIPgetRhsLinear(self._scip, cons.scip_cons)
elif constype == 'nonlinear':
return SCIPgetRhsNonlinear(cons.scip_cons)
elif constype == "knapsack":
return SCIPgetCapacityKnapsack(self._scip, cons.scip_cons)
else:
raise Warning("method cannot be called for constraints of type " + constype)

Expand Down Expand Up @@ -6570,6 +6638,23 @@ cdef class Model:

PY_SCIP_CALL( SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, value) )

def addCoefKnapsack(self, Constraint cons, Variable var, weight):
"""
Adds coefficient to knapsack constraint (if it is not zero)

Parameters
----------
cons : Constraint
knapsack constraint
var : Variable
variable of constraint entry
weight : float
coefficient of constraint entry

"""

PY_SCIP_CALL( SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, value) )

def getActivity(self, Constraint cons, Solution sol = None):
"""
Retrieve activity of given constraint.
Expand Down Expand Up @@ -6954,7 +7039,7 @@ cdef class Model:

"""
PY_SCIP_CALL(SCIPdelConsLocal(self._scip, cons.scip_cons))

def getValsLinear(self, Constraint cons):
"""
Retrieve the coefficients of a linear constraint
Expand Down Expand Up @@ -6986,10 +7071,41 @@ cdef class Model:

return valsdict

def getWeightsKnapsack(self, Constraint cons):
"""
Retrieve the coefficients of a knapsack constraint

Parameters
----------
cons : Constraint
knapsack constraint to get the coefficients of

Returns
-------
dict of str to float

"""
cdef SCIP_VAR** vars
cdef SCIP_Real* vals
cdef int i

constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8')
if not constype == 'knapsack':
raise Warning("weights not available for constraints of type ", constype)

vals = SCIPgetWeightsKnapsack(self._scip, cons.scip_cons)
vars = SCIPgetVarsKnapsack(self._scip, cons.scip_cons)

valsdict = {}
for i in range(SCIPgetNVarsKnapsack(self._scip, cons.scip_cons)):
valsdict[bytes(SCIPvarGetName(vars[i])).decode('utf-8')] = vals[i]

return valsdict

def getRowLinear(self, Constraint cons):
"""
Retrieve the linear relaxation of the given linear constraint as a row.
may return NULL if no LP row was yet created; the user must not modify the row!
May return NULL if no LP row was yet created; the user must not modify the row!

Parameters
----------
Expand Down Expand Up @@ -7031,6 +7147,29 @@ cdef class Model:
transcons = cons
return SCIPgetDualsolLinear(self._scip, transcons.scip_cons)

def getDualsolKnapsack(self, Constraint cons):
"""
Retrieve the dual solution to a knapsack constraint.

Parameters
----------
cons : Constraint
knapsack constraint

Returns
-------
float

"""
constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8')
if not constype == 'knapsack':
raise Warning("dual solution values not available for constraints of type ", constype)
if cons.isOriginal():
transcons = <Constraint>self.getTransformedCons(cons)
else:
transcons = cons
return SCIPgetDualsolKnapsack(self._scip, transcons.scip_cons)

def getDualMultiplier(self, Constraint cons):
"""
DEPRECATED: Retrieve the dual solution to a linear constraint.
Expand Down Expand Up @@ -7068,6 +7207,27 @@ cdef class Model:
return SCIPgetDualfarkasLinear(self._scip, transcons.scip_cons)
else:
return SCIPgetDualfarkasLinear(self._scip, cons.scip_cons)

def getDualfarkasKnapsack(self, Constraint cons):
"""
Retrieve the dual farkas value to a knapsack constraint.

Parameters
----------
cons : Constraint
knapsack constraint

Returns
-------
float

"""
# TODO this should ideally be handled on the SCIP side
if cons.isOriginal():
transcons = <Constraint>self.getTransformedCons(cons)
return SCIPgetDualfarkasKnapsack(self._scip, transcons.scip_cons)
else:
return SCIPgetDualfarkasKnapsack(self._scip, cons.scip_cons)

def getVarRedcost(self, Variable var):
"""
Expand Down
36 changes: 34 additions & 2 deletions tests/test_cons.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,43 @@ def test_addConsDisjunction_expr_init():
assert m.isEQ(m.getVal(y), 5)
assert m.isEQ(m.getVal(o), 6)

def test_cons_knapsack():
m = Model()
x = m.addVar("x", lb=0, ub=2, obj=-1)
y = m.addVar("y", lb=0, ub=4, obj=0)
z = m.addVar("z", lb=0, ub=5, obj=2)

knapsack_cons = m.addCons(4*x + 2*y <= 10, knapsack=True)

assert knapsack_cons.getConshdlrName() == "knapsack"
assert knapsack_cons.isKnapsack()

m.chgRhs(knapsack_cons, 5)

assert knapsack_cons.getRhs() == 5

m.addCoefKnapsack(knapsack_cons, z, 3)
weights = m.getWeightsKnapsack(knapsack_cons)
assert weights["x"] == 4
assert weights["y"] == 2
assert weights["z"] == 3

m.optimize()
assert m.getDualsolKnapsack(knapsack_cons) == 0

@pytest.mark.skip(reason="TODO: test getValsLinear()")
def test_getValsLinear():
assert True
m = Model()
x = m.addVar("x", lb=0, ub=2, obj=-1)
y = m.addVar("y", lb=0, ub=4, obj=0)
z = m.addVar("z", lb=0, ub=5, obj=2)

c1 = m.addCons(2*x + y <= 5)
c2 = m.addCons(x + 4*z <= 5)
assert m.getValsLinear(c1) == [2,1]

m.optimize() # just to check if constraint transformation matters

assert m.getValsLinear(c2) == [1,4]

@pytest.mark.skip(reason="TODO: test getRowLinear()")
def test_getRowLinear():
Expand Down
Loading