diff --git a/CHANGELOG.md b/CHANGELOG.md index 3390a6d9..ca44c7d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added support for knapsack constraints - Wrapped SCIPgetChildren and added getChildren and test (also test getOpenNodes) - Wrapped SCIPgetLeaves, SCIPgetNLeaves, and added getLeaves, getNLeaves and test - Wrapped SCIPgetSiblings, SCIPgetNSiblings, and added getSiblings, getNSiblings and test diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 2180d05f..177329a6 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1474,6 +1474,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, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index eba75495..a9a9c454 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -2079,6 +2079,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): """ @@ -5276,7 +5289,9 @@ cdef class Model: propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, - stickingatnode=stickingatnode) + stickingatnode=stickingatnode + ) + 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 @@ -5341,7 +5356,8 @@ cdef class Model: propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, - stickingatnode=stickingatnode) + stickingatnode=stickingatnode + ) # 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 @@ -5849,28 +5865,77 @@ cdef class Model: else: PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, NULL)) Py_INCREF(cons) + + def addConsKnapsack(self, vars, weights=None, name="", + initial=True, separate=True, enforce=True, check=True, + modifiable=False, propagate=True, local=False, dynamic=False, + removable=False, stickingatnode=False): + """ + Parameters + ---------- + cons : ExprCons + The expression constraint that is not yet an actual constraint + name : str, optional + the name of the constraint, generic name if empty (Default value = "") + initial : bool, optional + should the LP relaxation of constraint be in the initial LP? (Default value = True) + separate : bool, optional + should the constraint be separated during LP processing? (Default value = True) + enforce : bool, optional + should the constraint be enforced during node processing? (Default value = True) + check : bool, optional + should the constraint be checked for feasibility? (Default value = True) + propagate : bool, optional + should the constraint be propagated during node processing? (Default value = True) + local : bool, optional + is the constraint only valid locally? (Default value = False) + modifiable : bool, optional + is the constraint modifiable (subject to column generation)? (Default value = False) + dynamic : bool, optional + is the constraint subject to aging? (Default value = False) + removable : bool, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + 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) + """ + + 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 = malloc(nvars * sizeof(SCIP_VAR*)) + cdef SCIP_Real* weights_array = malloc(nvars * sizeof(SCIP_Real)) + cdef SCIP_CONS* scip_cons + cdef SCIP_Real coeff + cdef int i + + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + + for i, (key, weight) in enumerate(terms.items()): + vars_array[i] = (key[0]).scip_var + weights_array[i] = 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) - def addConsSOS1(self, vars, weights=None, name="SOS1cons", + return PyCons + + def addConsSOS1(self, vars, weights=None, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): """ Add an SOS1 constraint. - :param vars: list of variables to be included - :param weights: list of weights (Default value = None) - :param name: name of the constraint (Default value = "SOS1cons") - :param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True) - :param separate: should the constraint be separated during LP processing? (Default value = True) - :param enforce: should the constraint be enforced during node processing? (Default value = True) - :param check: should the constraint be checked for feasibility? (Default value = True) - :param propagate: should the constraint be propagated during node processing? (Default value = True) - :param local: is the constraint only valid locally? (Default value = False) - :param dynamic: is the constraint subject to aging? (Default value = False) - :param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) - :param stickingatnode: 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) - - Parameters ---------- vars : list of Variable @@ -5878,7 +5943,7 @@ cdef class Model: weights : list of float or None, optional list of weights (Default value = None) name : str, optional - name of the constraint (Default value = "SOS1cons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -5912,6 +5977,9 @@ cdef class Model: PY_SCIP_CALL(SCIPcreateConsSOS1(self._scip, &scip_cons, str_conversion(name), 0, NULL, NULL, initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + if weights is None: for v in vars: var = v @@ -5926,7 +5994,7 @@ cdef class Model: return Constraint.create(scip_cons) - def addConsSOS2(self, vars, weights=None, name="SOS2cons", + def addConsSOS2(self, vars, weights=None, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): @@ -5940,7 +6008,7 @@ cdef class Model: weights : list of float or None, optional list of weights (Default value = None) name : str, optional - name of the constraint (Default value = "SOS2cons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -5974,6 +6042,9 @@ cdef class Model: PY_SCIP_CALL(SCIPcreateConsSOS2(self._scip, &scip_cons, str_conversion(name), 0, NULL, NULL, initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + if weights is None: for v in vars: var = v @@ -5988,7 +6059,7 @@ cdef class Model: return Constraint.create(scip_cons) - def addConsAnd(self, vars, resvar, name="ANDcons", + def addConsAnd(self, vars, resvar, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -6002,7 +6073,7 @@ cdef class Model: resvar : Variable BINARY variable (resultant) name : str, optional - name of the constraint (Default value = "ANDcons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -6039,6 +6110,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + PY_SCIP_CALL(SCIPcreateConsAnd(self._scip, &scip_cons, str_conversion(name), _resvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6050,7 +6124,7 @@ cdef class Model: return pyCons - def addConsOr(self, vars, resvar, name="ORcons", + def addConsOr(self, vars, resvar, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -6064,7 +6138,7 @@ cdef class Model: resvar : Variable BINARY variable (resultant) name : str, optional - name of the constraint (Default value = "ORcons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -6101,6 +6175,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + PY_SCIP_CALL(SCIPcreateConsOr(self._scip, &scip_cons, str_conversion(name), _resvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6112,7 +6189,7 @@ cdef class Model: return pyCons - def addConsXor(self, vars, rhsvar, name="XORcons", + def addConsXor(self, vars, rhsvar, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False): @@ -6126,7 +6203,7 @@ cdef class Model: rhsvar : bool BOOLEAN value, explicit True, False or bool(obj) is needed (right-hand side) name : str, optional - name of the constraint (Default value = "XORcons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -6162,6 +6239,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + PY_SCIP_CALL(SCIPcreateConsXor(self._scip, &scip_cons, str_conversion(name), rhsvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6173,7 +6253,7 @@ cdef class Model: return pyCons - def addConsCardinality(self, consvars, cardval, indvars=None, weights=None, name="CardinalityCons", + def addConsCardinality(self, consvars, cardval, indvars=None, weights=None, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, removable=False, stickingatnode=False): @@ -6194,7 +6274,7 @@ cdef class Model: weights determining the variable order, or None if variables should be ordered in the same way they were added to the constraint (Default value = None) name : str, optional - name of the constraint (Default value = "CardinalityCons") + name of the constraint (Default value = "") initial : bool, optional should the LP relaxation of constraint be in the initial LP? (Default value = True) separate : bool, optional @@ -6232,6 +6312,9 @@ cdef class Model: if weights is None: weights = list(range(1, len(consvars) + 1)) + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1) + for i, v in enumerate(consvars): var = v if indvars: @@ -6531,6 +6614,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) @@ -6560,7 +6645,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 ---------- @@ -6577,6 +6662,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) @@ -6655,6 +6742,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. @@ -7039,7 +7143,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 @@ -7071,10 +7175,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 ---------- @@ -7116,6 +7251,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 = 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. @@ -7153,6 +7311,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 = self.getTransformedCons(cons) + return SCIPgetDualfarkasKnapsack(self._scip, transcons.scip_cons) + else: + return SCIPgetDualfarkasKnapsack(self._scip, cons.scip_cons) def getVarRedcost(self, Variable var): """ diff --git a/tests/test_cons.py b/tests/test_cons.py index ab53e9a2..13e5e90a 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -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.addConsKnapsack(4*x + 2*y <= 10) + + 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():