From 8a9af4f60ed2fab5d01479cb01fe70c46c8a4f25 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 16 Apr 2025 23:46:27 +0100 Subject: [PATCH 01/14] Add support for knapsack constraints --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 33 ++++++++ src/pyscipopt/scip.pxi | 177 +++++++++++++++++++++++++++++++++++++++-- tests/test_cons.py | 36 ++++++++- 4 files changed, 238 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18718ae76..db0b88057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index f53421164..25d0e8e47 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -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, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 3b847ef60..8c0559e0f 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -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): """ @@ -4889,6 +4902,50 @@ 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 = 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 + + assert kwargs["rhs"] >= 0 and isinstance(kwargs["rhs"], int), "knapsack constraint capacity must be non-negative integer" + for i, (key, weight) in enumerate(terms.items()): + assert weight >= 0 and isinstance(weight, int), "weight must be non-negative integer" + assert key[0].vtype() == 'B', "knapsack constraint only supports binary variables" + 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) + + return PyCons + def _createConsQuadratic(self, ExprCons quadcons, **kwargs): """ The function for creating a quadratic constraint, but not adding it to the Model. @@ -5154,7 +5211,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. @@ -5188,6 +5245,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 ------- @@ -5203,7 +5262,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 @@ -5221,7 +5285,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. @@ -5252,6 +5316,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 ------- @@ -5268,7 +5334,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 @@ -6446,6 +6513,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) @@ -6475,7 +6544,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 ---------- @@ -6492,6 +6561,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) @@ -6570,6 +6641,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. @@ -6954,7 +7042,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 @@ -6986,10 +7074,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 ---------- @@ -7031,6 +7150,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. @@ -7068,6 +7210,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 ab53e9a24..069ed059d 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.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(): From c1c371bbdfea2b945d92a65bca296dfae96415ec Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 16 Apr 2025 23:54:37 +0100 Subject: [PATCH 02/14] remove useless asserts --- src/pyscipopt/scip.pxi | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 8c0559e0f..847899894 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -4926,10 +4926,7 @@ cdef class Model: cdef SCIP_Real coeff cdef int i - assert kwargs["rhs"] >= 0 and isinstance(kwargs["rhs"], int), "knapsack constraint capacity must be non-negative integer" for i, (key, weight) in enumerate(terms.items()): - assert weight >= 0 and isinstance(weight, int), "weight must be non-negative integer" - assert key[0].vtype() == 'B', "knapsack constraint only supports binary variables" vars_array[i] = (key[0]).scip_var weights_array[i] = weight From fd0290943703b9cb6bba513edc0aa37144eb202d Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 17 Apr 2025 10:38:44 +0100 Subject: [PATCH 03/14] Standardize knapsack with other constypes --- src/pyscipopt/scip.pxi | 181 +++++++++++++++++++++++------------------ tests/test_cons.py | 2 +- 2 files changed, 101 insertions(+), 82 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 847899894..446d4b543 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -4902,47 +4902,6 @@ 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 = 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 - - 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) - - return PyCons - def _createConsQuadratic(self, ExprCons quadcons, **kwargs): """ The function for creating a quadratic constraint, but not adding it to the Model. @@ -5208,7 +5167,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, knapsack=False): + stickingatnode=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. @@ -5242,8 +5201,6 @@ 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 ------- @@ -5259,11 +5216,8 @@ cdef class Model: propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, - stickingatnode=stickingatnode, - knapsack=knapsack) - - if kwargs["knapsack"]: - return self._createConsKnapsack(cons, **kwargs) + 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 @@ -5282,7 +5236,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, knapsack=False): + stickingatnode=False): """ Add a linear or nonlinear constraint. @@ -5313,8 +5267,6 @@ 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 ------- @@ -5331,8 +5283,8 @@ cdef class Model: propagate=propagate, local=local, modifiable=modifiable, dynamic=dynamic, removable=removable, - stickingatnode=stickingatnode, - knapsack=knapsack) + 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 @@ -5840,28 +5792,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)+"knapsack" + + 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) - def addConsSOS1(self, vars, weights=None, name="SOS1cons", + free(vars_array) + free(coeffs_array) + + 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 @@ -5869,7 +5870,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 @@ -5903,6 +5904,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)+"SOS1" + if weights is None: for v in vars: var = v @@ -5917,7 +5921,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): @@ -5931,7 +5935,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 @@ -5965,6 +5969,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)+"SOS2" + if weights is None: for v in vars: var = v @@ -5979,7 +5986,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): @@ -5993,7 +6000,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 @@ -6030,6 +6037,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1)+"AND" + PY_SCIP_CALL(SCIPcreateConsAnd(self._scip, &scip_cons, str_conversion(name), _resvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6041,7 +6051,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): @@ -6055,7 +6065,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 @@ -6092,6 +6102,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1)+"OR" + PY_SCIP_CALL(SCIPcreateConsOr(self._scip, &scip_cons, str_conversion(name), _resvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6103,7 +6116,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): @@ -6117,7 +6130,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 @@ -6153,6 +6166,9 @@ cdef class Model: for i, var in enumerate(vars): _vars[i] = (var).scip_var + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1)+"XOR" + PY_SCIP_CALL(SCIPcreateConsXor(self._scip, &scip_cons, str_conversion(name), rhsvar, nvars, _vars, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) @@ -6164,7 +6180,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): @@ -6185,7 +6201,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 @@ -6223,6 +6239,9 @@ cdef class Model: if weights is None: weights = list(range(1, len(consvars) + 1)) + if name == '': + name = 'c'+str(SCIPgetNConss(self._scip)+1)+"Cardinality" + for i, v in enumerate(consvars): var = v if indvars: @@ -6295,7 +6314,7 @@ cdef class Model: raise ValueError("expected inequality that has either only a left or right hand side") if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1) + name = 'c'+str(SCIPgetNConss(self._scip)+1)+"Indicator" if cons.expr.degree() > 1: raise ValueError("expected linear inequality, expression has degree %d" % cons.expr.degree()) diff --git a/tests/test_cons.py b/tests/test_cons.py index 069ed059d..13e5e90ad 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -222,7 +222,7 @@ def test_cons_knapsack(): 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) + knapsack_cons = m.addConsKnapsack(4*x + 2*y <= 10) assert knapsack_cons.getConshdlrName() == "knapsack" assert knapsack_cons.isKnapsack() From 8dc24f89bf8c10b0017fa573fd61627b4af3a701 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 22 Apr 2025 18:21:13 +0100 Subject: [PATCH 04/14] Remove handler name from default constraint name --- src/pyscipopt/scip.pxi | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 446d4b543..572fac684 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5837,7 +5837,7 @@ cdef class Model: cdef int i if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"knapsack" + name = 'c'+str(SCIPgetNConss(self._scip)+1) for i, (key, weight) in enumerate(terms.items()): vars_array[i] = (key[0]).scip_var @@ -5905,7 +5905,7 @@ cdef class Model: initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"SOS1" + name = 'c'+str(SCIPgetNConss(self._scip)+1) if weights is None: for v in vars: @@ -5970,7 +5970,7 @@ cdef class Model: initial, separate, enforce, check, propagate, local, dynamic, removable, stickingatnode)) if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"SOS2" + name = 'c'+str(SCIPgetNConss(self._scip)+1) if weights is None: for v in vars: @@ -6038,7 +6038,7 @@ cdef class Model: _vars[i] = (var).scip_var if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"AND" + 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)) @@ -6103,7 +6103,7 @@ cdef class Model: _vars[i] = (var).scip_var if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"OR" + 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)) @@ -6167,7 +6167,7 @@ cdef class Model: _vars[i] = (var).scip_var if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"XOR" + 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)) @@ -6240,7 +6240,7 @@ cdef class Model: weights = list(range(1, len(consvars) + 1)) if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"Cardinality" + name = 'c'+str(SCIPgetNConss(self._scip)+1) for i, v in enumerate(consvars): var = v @@ -6314,7 +6314,7 @@ cdef class Model: raise ValueError("expected inequality that has either only a left or right hand side") if name == '': - name = 'c'+str(SCIPgetNConss(self._scip)+1)+"Indicator" + name = 'c'+str(SCIPgetNConss(self._scip)+1) if cons.expr.degree() > 1: raise ValueError("expected linear inequality, expression has degree %d" % cons.expr.degree()) From 9c7659ce5dbd4675acbf7732086311e7287eaa3c Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 7 May 2025 22:38:24 +0100 Subject: [PATCH 05/14] Remove redeclaration --- src/pyscipopt/scip.pxd | 1 - src/pyscipopt/scip.pxi | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 177329a60..6e28124c3 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1502,7 +1502,6 @@ cdef extern from "scip/cons_knapsack.h" 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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a9a9c4549..f83bcd131 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6645,7 +6645,7 @@ cdef class Model: def getRhs(self, Constraint cons): """ - Retrieve right-hand side value of a linear constraint. + Retrieve right-hand side value of a constraint. Parameters ---------- From 55d50ccc8169a1da722ab00c50116ff36741e1b9 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 11:38:35 +0100 Subject: [PATCH 06/14] minor bugfixing --- src/pyscipopt/scip.pxi | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index f83bcd131..e96d217b5 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5900,9 +5900,7 @@ cdef class Model: 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 int nvars = len(vars) 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 @@ -5917,15 +5915,14 @@ cdef class Model: 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'])) + self._scip, &scip_cons, str_conversion(name), nvars, vars_array, weights_array, + 0, rhs, initial, separate, enforce, check, propagate, local, modifiable, + dynamic, removable, stickingatnode)) PyCons = Constraint.create(scip_cons) free(vars_array) - free(coeffs_array) + free(weights_array) return PyCons @@ -6757,7 +6754,7 @@ cdef class Model: """ - PY_SCIP_CALL( SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, value) ) + PY_SCIP_CALL( SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, weight) ) def getActivity(self, Constraint cons, Solution sol = None): """ From ba7321c576f9cedd834a5b26864d360f9c22b22e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 15:13:00 +0100 Subject: [PATCH 07/14] Fix stupid bug --- src/pyscipopt/scip.pxd | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 6e28124c3..bc7982947 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1474,15 +1474,14 @@ 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" +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_Real* weights, + SCIP_Real capacity, SCIP_Bool initial, SCIP_Bool separate, SCIP_Bool enforce, From 34a6b4fb9b64c32d5448a9f5b40c35c962cc5868 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 16:33:24 +0100 Subject: [PATCH 08/14] Everything seems alright now --- src/pyscipopt/scip.pxd | 10 +++--- src/pyscipopt/scip.pxi | 76 +++++++++++++++++++++++++++--------------- tests/test_cons.py | 13 ++++---- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index bc7982947..f00f9a664 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1480,8 +1480,8 @@ cdef extern from "scip/cons_knapsack.h": char* name, int nvars, SCIP_VAR** vars, - SCIP_Real* weights, - SCIP_Real capacity, + SCIP_Longint* weights, + SCIP_Longint capacity, SCIP_Bool initial, SCIP_Bool separate, SCIP_Bool enforce, @@ -1495,15 +1495,15 @@ cdef extern from "scip/cons_knapsack.h": SCIP_RETCODE SCIPaddCoefKnapsack(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* var, - SCIP_Real val) + SCIP_Longint 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_Longint SCIPgetCapacityKnapsack(SCIP* scip, SCIP_CONS* cons) SCIP_VAR** SCIPgetVarsKnapsack(SCIP* scip, SCIP_CONS* cons) int SCIPgetNVarsKnapsack(SCIP* scip, SCIP_CONS* cons) - SCIP_Real* SCIPgetWeightsKnapsack(SCIP* scip, SCIP_CONS* cons) + SCIP_Longint* SCIPgetWeightsKnapsack(SCIP* scip, SCIP_CONS* cons) cdef extern from "scip/cons_nonlinear.h": SCIP_EXPR* SCIPgetExprNonlinear(SCIP_CONS* cons) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e96d217b5..a56bc064b 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5814,7 +5814,7 @@ cdef class Model: def addConsCoeff(self, Constraint cons, Variable var, coeff): """ - Add coefficient to the linear constraint (if non-zero). + Add coefficient to the constraint (if non-zero). Parameters ---------- @@ -5826,7 +5826,14 @@ cdef class Model: coefficient of new variable """ - PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, coeff)) + + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if constype == 'linear': + PY_SCIP_CALL(SCIPaddCoefLinear(self._scip, cons.scip_cons, var.scip_var, coeff)) + elif constype == 'knapsack': + PY_SCIP_CALL(SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, coeff)) + else: + raise NotImplementedError("Adding coefficients to %s constraints is not implemented." % constype) def addConsNode(self, Node node, Constraint cons, Node validnode=None): """ @@ -5866,17 +5873,21 @@ cdef class Model: PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, NULL)) Py_INCREF(cons) - def addConsKnapsack(self, vars, weights=None, name="", + def addConsKnapsack(self, vars, weights, capacity, 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 + vars : list of Variable + list of variables to be included + weights : list of int + list of weights + capacity: int + capacity of the knapsack name : str, optional - the name of the constraint, generic name if empty (Default value = "") + 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 @@ -5889,42 +5900,47 @@ cdef class Model: 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, + 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) + + Returns + ------- + Constraint + The newly created knapsack constraint """ cdef int nvars = len(vars) + cdef int i cdef SCIP_VAR** vars_array = malloc(nvars * sizeof(SCIP_VAR*)) - cdef SCIP_Real* weights_array = malloc(nvars * sizeof(SCIP_Real)) + cdef SCIP_Longint* 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 + for i in range(nvars): + vars_array[i] = (vars[i]).scip_var + weights_array[i] = weights[i] PY_SCIP_CALL(SCIPcreateConsKnapsack( self._scip, &scip_cons, str_conversion(name), nvars, vars_array, weights_array, - 0, rhs, initial, separate, enforce, check, propagate, local, modifiable, + capacity, initial, separate, enforce, check, propagate, local, modifiable, dynamic, removable, stickingatnode)) - PyCons = Constraint.create(scip_cons) - free(vars_array) free(weights_array) - return PyCons + PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) + + pyCons = Constraint.create(scip_cons) + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + return pyCons def addConsSOS1(self, vars, weights=None, name="", initial=True, separate=True, enforce=True, check=True, @@ -6754,6 +6770,7 @@ cdef class Model: """ + print(weight) PY_SCIP_CALL( SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, weight) ) def getActivity(self, Constraint cons, Solution sol = None): @@ -7187,19 +7204,26 @@ cdef class Model: """ cdef SCIP_VAR** vars - cdef SCIP_Real* vals + cdef SCIP_Longint* vals + cdef int nvars 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) + nvars = SCIPgetNVarsKnapsack(self._scip, cons.scip_cons) 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] + for i in range(nvars): + var_name = bytes(SCIPvarGetName(vars[i])).decode('utf-8') + valsdict[var_name] = vals[i] + + print(f"Number of variables: {nvars}") + for i in range(nvars): + print(f"Variable: {bytes(SCIPvarGetName(vars[i])).decode('utf-8')}, Weight: {vals[i]}") return valsdict @@ -7266,9 +7290,9 @@ cdef class Model: 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 + else: + transcons = self.getTransformedCons(cons) return SCIPgetDualsolKnapsack(self._scip, transcons.scip_cons) def getDualMultiplier(self, Constraint cons): @@ -7325,10 +7349,10 @@ cdef class Model: """ # TODO this should ideally be handled on the SCIP side if cons.isOriginal(): + return SCIPgetDualfarkasKnapsack(self._scip, cons.scip_cons) + else: 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 13e5e90ad..d5a2db363 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -218,18 +218,18 @@ def test_addConsDisjunction_expr_init(): 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) + x = m.addVar("x", vtype="B", obj=-1) + y = m.addVar("y", vtype="B", obj=0) + z = m.addVar("z", vtype="B", obj=2) - knapsack_cons = m.addConsKnapsack(4*x + 2*y <= 10) + knapsack_cons = m.addConsKnapsack([x,y], [4,2], 10) assert knapsack_cons.getConshdlrName() == "knapsack" assert knapsack_cons.isKnapsack() m.chgRhs(knapsack_cons, 5) - assert knapsack_cons.getRhs() == 5 + assert m.getRhs(knapsack_cons) == 5 m.addCoefKnapsack(knapsack_cons, z, 3) weights = m.getWeightsKnapsack(knapsack_cons) @@ -239,6 +239,7 @@ def test_cons_knapsack(): m.optimize() assert m.getDualsolKnapsack(knapsack_cons) == 0 + assert m.getDualfarkasKnapsack(knapsack_cons) == 0 def test_getValsLinear(): m = Model() @@ -248,7 +249,7 @@ def test_getValsLinear(): c1 = m.addCons(2*x + y <= 5) c2 = m.addCons(x + 4*z <= 5) - assert m.getValsLinear(c1) == [2,1] + assert m.getValsLinear(c1) == {'x': 2, 'y': 1} m.optimize() # just to check if constraint transformation matters From 7b3abfa49750edbb777cd717c13816735a5c673f Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 16:41:36 +0100 Subject: [PATCH 09/14] copilot --- src/pyscipopt/scip.pxi | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a56bc064b..a28a57ff7 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6770,7 +6770,6 @@ cdef class Model: """ - print(weight) PY_SCIP_CALL( SCIPaddCoefKnapsack(self._scip, cons.scip_cons, var.scip_var, weight) ) def getActivity(self, Constraint cons, Solution sol = None): @@ -7221,10 +7220,6 @@ cdef class Model: var_name = bytes(SCIPvarGetName(vars[i])).decode('utf-8') valsdict[var_name] = vals[i] - print(f"Number of variables: {nvars}") - for i in range(nvars): - print(f"Variable: {bytes(SCIPvarGetName(vars[i])).decode('utf-8')}, Weight: {vals[i]}") - return valsdict def getRowLinear(self, Constraint cons): From 8ef7edeabce85997f5f1b5cf6b2d779f4fe0bcfa Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 16:42:16 +0100 Subject: [PATCH 10/14] typo --- tests/test_cons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cons.py b/tests/test_cons.py index d5a2db363..120675c31 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -253,7 +253,7 @@ def test_getValsLinear(): m.optimize() # just to check if constraint transformation matters - assert m.getValsLinear(c2) == [1,4] + assert m.getValsLinear(c2) == {'x': 1, 'y': 4} @pytest.mark.skip(reason="TODO: test getRowLinear()") def test_getRowLinear(): From 13232ab411c95a51849bee7e3fbe82abaaacacb5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 10 May 2025 16:50:36 +0100 Subject: [PATCH 11/14] all good now --- tests/test_cons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cons.py b/tests/test_cons.py index 120675c31..de3ac866e 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -253,7 +253,7 @@ def test_getValsLinear(): m.optimize() # just to check if constraint transformation matters - assert m.getValsLinear(c2) == {'x': 1, 'y': 4} + assert m.getValsLinear(c2) == {'x': 1, 'z': 4} @pytest.mark.skip(reason="TODO: test getRowLinear()") def test_getRowLinear(): From a4278391b13be4917b91eaa3cccaf0c4d8ec6c70 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 12 May 2025 11:35:34 +0100 Subject: [PATCH 12/14] remove knapsack getrhs --- src/pyscipopt/scip.pxi | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index a28a57ff7..e558fcf15 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6675,8 +6675,6 @@ 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) From f66235e4cc2109721e16610583394effafa5e24b Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 12 May 2025 17:03:34 +0100 Subject: [PATCH 13/14] Add assert --- src/pyscipopt/scip.pxi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e558fcf15..313500ec6 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -5920,6 +5920,8 @@ cdef class Model: cdef SCIP_Longint* weights_array = malloc(nvars * sizeof(SCIP_Real)) cdef SCIP_CONS* scip_cons + assert nvars == len(weights), "Number of variables and weights must be the same." + if name == '': name = 'c'+str(SCIPgetNConss(self._scip)+1) From 701acdb0f556cf54cfb7a4930584dff98b46b385 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 12 May 2025 17:55:58 +0100 Subject: [PATCH 14/14] remove get/setRhs --- src/pyscipopt/scip.pxi | 41 +++++++++++++++++++++++++++++++++++++++-- tests/test_cons.py | 8 +++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 313500ec6..970e6f039 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6629,11 +6629,29 @@ 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) + def chgCapacityKnapsack(self, Constraint cons, capacity): + """ + Change capacity of a knapsack constraint. + + Parameters + ---------- + cons : Constraint + knapsack constraint to change the capacity from + capacity : float or None + new capacity (set to None for +infinity) + + """ + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + assert constype == 'knapsack', "method cannot be called for constraints of type " + constype + + if capacity is None: + capacity = SCIPinfinity(self._scip) + + PY_SCIP_CALL(SCIPchgCapacityKnapsack(self._scip, cons.scip_cons, capacity)) + def chgLhs(self, Constraint cons, lhs): """ Change left-hand side value of a constraint. @@ -6680,6 +6698,25 @@ cdef class Model: else: raise Warning("method cannot be called for constraints of type " + constype) + def getCapacityKnapsack(self, Constraint cons): + """ + Retrieve capacity of a knapsack constraint. + + Parameters + ---------- + cons : Constraint + knapsack constraint to get the capacity from + + Returns + ------- + float + + """ + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + assert constype == 'knapsack', "method cannot be called for constraints of type " + constype + + return SCIPgetCapacityKnapsack(self._scip, cons.scip_cons) + def getLhs(self, Constraint cons): """ Retrieve left-hand side value of a constraint. diff --git a/tests/test_cons.py b/tests/test_cons.py index de3ac866e..a3c663ee0 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -223,13 +223,15 @@ def test_cons_knapsack(): z = m.addVar("z", vtype="B", obj=2) knapsack_cons = m.addConsKnapsack([x,y], [4,2], 10) - assert knapsack_cons.getConshdlrName() == "knapsack" assert knapsack_cons.isKnapsack() - m.chgRhs(knapsack_cons, 5) + assert m.getConsNVars(knapsack_cons) == 2 + assert m.getConsVars(knapsack_cons) == [x, y] + + m.chgCapacityKnapsack(knapsack_cons, 5) - assert m.getRhs(knapsack_cons) == 5 + assert m.getCapacityKnapsack(knapsack_cons) == 5 m.addCoefKnapsack(knapsack_cons, z, 3) weights = m.getWeightsKnapsack(knapsack_cons)