From 3cbfc849540d9651454898eac4389d78b379fa07 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 26 Apr 2025 18:52:43 +0100 Subject: [PATCH 1/2] Add support for indicator constraints --- src/pyscipopt/scip.pxd | 32 ++++++- src/pyscipopt/scip.pxi | 188 +++++++++++++++++++++++++++++++++++++++++ tests/test_cons.py | 2 +- 3 files changed, 218 insertions(+), 4 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index f53421164..32ad0d39a 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1851,14 +1851,40 @@ cdef extern from "scip/cons_indicator.h": SCIP_Bool dynamic, SCIP_Bool removable, SCIP_Bool stickingatnode) - SCIP_RETCODE SCIPaddVarIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* var, SCIP_Real val) - - SCIP_VAR* SCIPgetSlackVarIndicator(SCIP_CONS* cons) SCIP_CONS* SCIPgetLinearConsIndicator(SCIP_CONS* cons) + SCIP_RETCODE SCIPsetLinearConsIndicator(SCIP* scip, + SCIP_CONS* cons, + SCIP_CONS* lincons) + SCIP_RETCODE SCIPsetBinaryVarIndicator(SCIP* scip, + SCIP_CONS* cons, + SCIP_VAR* binvar) + SCIP_RETCODE SCIPgetActiveOneIndicator(SCIP_Cons* cons, + SCIP_VAR* binvar) + SCIP_VAR* SCIPgetBinaryVarIndicator(SCIP_CONS* cons) + SCIP_VAR* SCIPgetSlackVarIndicator(SCIP_CONS* cons) + SCIP_RETCODE SCIPsetSlackVarIndicator(SCIP* scip, + SCIP_CONS* cons, + SCIP_VAR* slackvar) + SCIP_Bool SCIPisActiveOneIndicator(SCIP* scip, + SCIP_CONS* cons, + SCIP_VAR* binvar) + SCIP_Bool SCIPmakeIndicatorFeasible(SCIP* scip, + SCIP_CONS* cons, + SCIP_SOL* sol, + SCIP_Bool* changed) + SCIP_Bool SCIPmakeIndicatorsFeasible(SCIP* scip, + SCIP_CONSHDLR* conshdlr, + SCIP_SOL* sol) + SCIP_RETCODE SCIPaddLinearConsIndicator(SCIP* scip, + SCIP_CONSHDLR* conshdlr, + SCIP_CONS* cons) + SCIP_RETCODE SCIPaddRowIndicator(SCIP* scip, + SCIP_CONSHDLR* conshdlr, + SCIP_ROW* row) cdef extern from "scip/misc.h": SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 3b847ef60..480dac646 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6267,6 +6267,22 @@ cdef class Model: return pyCons + def addVarIndicator(self, Constraint cons, Variable var, coeff): + """ + Add variable to the inequality of the indicator constraint. + + Parameters + ---------- + cons : Constraint + indicator constraint + var : Variable + new variable + coeff : float + coefficient of new variable + + """ + PY_SCIP_CALL(SCIPaddVarIndicator(self._scip, cons.scip_cons, var.scip_var, coeff)) + def getLinearConsIndicator(self, Constraint cons): """ Get the linear constraint corresponding to the indicator constraint. @@ -6285,6 +6301,77 @@ cdef class Model: if lincons == NULL: return None return Constraint.create(lincons) + + def setLinearConsIndicator(self, Constraint cons, Constraint lincons): + """ + Sets the linear constraint corresponding to the indicator constraint (may be None). + + Parameters + ---------- + cons : Constraint + The indicator constraint + lincons : Constraint + The linear constraint + + """ + PY_SCIP_CALL(SCIPsetLinearConsIndicator(self._scip, cons.scip_cons, lincons.scip_cons)) + + def setBinaryVarIndicator(self, Constraint cons, Variable binvar): + """ + Set the binary variable of the indicator constraint. + + Parameters + ---------- + cons : Constraint + The indicator constraint + binvar : Variable + The binary variable + + """ + PY_SCIP_CALL(SCIPsetBinaryVarIndicator(self._scip, cons.scip_cons, binvar.scip_var)) + + def getActiveOnIndicator(self, Constraint cons): + """ + Gets activation value of an indicator constraint, True for active on 1, False for active on 0. + + Parameters + ---------- + cons : Constraint + The indicator constraint + + Returns + ------- + bool + + """ + cdef SCIP_Bool activeone + PY_SCIP_CALL(SCIPgetActiveOneIndicator(cons.scip_cons, &activeone)) + return activeone + + def getBinaryVarIndicator(self, Constraint cons): + """ + Gets binary variable corresponding to indicator constraint. Returns the negative of the original binary variable if activeone was set to false + + Parameters + ---------- + cons : Constraint + The indicator constraint + + Returns + ------- + Variable + + """ + cdef SCIP_VAR* _var = SCIPgetBinaryVarIndicator(cons.scip_cons) + ptr = (_var) + + if pts not in self._modelvars: + # create a new variable + var = Variable.create(_var) + assert var.ptr() == ptr + self._modelvars[ptr] = var + + return self._modelvars[ptr] def getSlackVarIndicator(self, Constraint cons): """ @@ -6304,6 +6391,107 @@ cdef class Model: cdef SCIP_VAR* var = SCIPgetSlackVarIndicator(cons.scip_cons) return Variable.create(var) + def setSlackVarUb(self, Constraint cons, ub): + """ + Set the upper bound of the slack variable of an indicator constraint. + Use with care if you know that the maximal violation of the corresponding constraint is at most ub. + This bound might be improved automatically during the solution process. + + Parameters + ---------- + cons : Constraint + The indicator constraint + ub : Variable + Upper bound for slack variable + + """ + PY_SCIP_CALL(SCIPsetSlackVarUb(self._scip, cons.scip_cons, ub)) + + def isViolatedIndicator(self, Constraint cons, Solution sol): + """ + Check if the indicator constraint is violated. + + Parameters + ---------- + cons : Constraint + The indicator constraint + sol : Solution + The solution to check, or None for the current solution + + Returns + ------- + bool + + """ + if sol is None: + return SCIPisViolatedIndicator(self._scip, cons.scip_cons, NULL) + else: + return SCIPisViolatedIndicator(self._scip, cons.scip_cons, sol.scip_sol) + + def makeIndicatorFeasible(self, Constraint cons, Solution sol): + """ + Based on values of other variables, computes slack and binary variable to turn constraint feasible. + Returns whether the solution was changed. + For a longer explanation, see SCIP documentation: https://www.scipopt.org/doc-9.2.2/html/group__CONSHDLRS.php#gac50cc1a3e02089a32c8ee04f9d9eb5a8 + + Parameters + ---------- + cons : Constraint + The indicator constraint + sol : Solution + The solution. + + """ + cdef SCIP_Bool changed + PY_SCIP_CALL(SCIPmakeIndicatorFeasible(self._scip, cons.scip_cons, sol.scip_sol, &changed)) + return changed + + def makeIndicatorsFeasible(self, Conshdlr conshdlr, Solution sol): + """ + Based on values of other variables, computes slack and binary variable to turn all constraints feasible + + Parameters + ---------- + conshdlr : Conshdlr + The indicator constraint handler + sol : Solution + The solution. + + """ + cdef SCIP_Bool changed + PY_SCIP_CALL(SCIPmakeIndicatorsFeasible(self._scip, conshdlr.scip_conshdlr, sol.scip_sol, &changed)) + return changed + + def addLinearConsIndicator(self, Conshdlr conshdlr, Constraint lincons): + """ + Adds additional linear constraint that is not connected with an indicator constraint, but can be used for separation + + Parameters + ---------- + conshdlr : Conshdlr + The indicator constraint handler + lincons : Constraint + The linear constraint + + """ + PY_SCIP_CALL(SCIPaddLinearConsIndicator(self._scip, conshdlr.scip_conshdlr, lincons.scip_cons)) + + def addRowIndicator(self, Conshdlr conshdlr, Row rowcons): + """ + Adds additional globally valid row constraint that is not connected with an indicator constraint, but can be used for separation + + Note: The row is directly added to the alternative polyhedron and is not stored. + + Parameters + ---------- + conshdlr : Conshdlr + The indicator constraint handler + rowcons : Constraint + The row to add + + """ + PY_SCIP_CALL(SCIPaddRowIndicator(self._scip, conshdlr.scip_conshdlr, row.scip_row)) + def addPyCons(self, Constraint cons): """ Adds a customly created cons. diff --git a/tests/test_cons.py b/tests/test_cons.py index ab53e9a24..bdd87f7d4 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -104,7 +104,7 @@ def test_cons_indicator(): x = m.addVar(lb=0, obj=1) binvar = m.addVar(vtype="B", lb=1) - c1 = m.addConsIndicator(x >= 1, binvar) + c1 = m.addConsIndicator(x >= 1, binvar) assert c1.name == "c1" From da849b1b58893bbe6ea4ffa6782db84c9e2d45e1 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 26 Apr 2025 19:22:34 +0100 Subject: [PATCH 2/2] better spacing in pxd --- src/pyscipopt/scip.pxd | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 32ad0d39a..933b41089 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1851,37 +1851,50 @@ cdef extern from "scip/cons_indicator.h": SCIP_Bool dynamic, SCIP_Bool removable, SCIP_Bool stickingatnode) + SCIP_RETCODE SCIPaddVarIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* var, SCIP_Real val) + SCIP_CONS* SCIPgetLinearConsIndicator(SCIP_CONS* cons) + SCIP_RETCODE SCIPsetLinearConsIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_CONS* lincons) + SCIP_RETCODE SCIPsetBinaryVarIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* binvar) + SCIP_RETCODE SCIPgetActiveOneIndicator(SCIP_Cons* cons, SCIP_VAR* binvar) + SCIP_VAR* SCIPgetBinaryVarIndicator(SCIP_CONS* cons) + SCIP_VAR* SCIPgetSlackVarIndicator(SCIP_CONS* cons) + SCIP_RETCODE SCIPsetSlackVarIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* slackvar) + SCIP_Bool SCIPisActiveOneIndicator(SCIP* scip, SCIP_CONS* cons, SCIP_VAR* binvar) + SCIP_Bool SCIPmakeIndicatorFeasible(SCIP* scip, SCIP_CONS* cons, SCIP_SOL* sol, SCIP_Bool* changed) + SCIP_Bool SCIPmakeIndicatorsFeasible(SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_SOL* sol) + SCIP_RETCODE SCIPaddLinearConsIndicator(SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_CONS* cons) + SCIP_RETCODE SCIPaddRowIndicator(SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_ROW* row)