From 9f900b0eb9ca115aa1709935d39f91535b586cf5 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 26 Apr 2025 20:09:44 +0100 Subject: [PATCH 1/4] Add support for SOS1 constraints --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 13 +++ src/pyscipopt/scip.pxi | 218 +++++++++++++++++++++++++++++++++++------ 3 files changed, 202 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dadd7965..34c6ab543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added support for SOS1-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..5a04ded04 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1550,6 +1550,19 @@ cdef extern from "scip/cons_sos1.h": SCIP_CONS* cons, SCIP_VAR* var) + int SCIPgetNVarsSOS1(SCIP* scip, SCIP_CONS* cons) + + SCIP_VAR** SCIPgetVarsSOS1(SCIP* scip, SCIP_CONS* cons) + + SCIP_Real* SCIPgetWeightsSOS1(SCIP* scip, SCIP_CONS* cons) + + int SCIPgetNSOS1Vars(SCIP_CONSHDLR* conshdlr) + + SCIP_RETCODE SCIPmakeSOS1Feasible(SCIP* scip, + SCIP_CONSHDLR* conshdlr, + SCIP_SOL* solution, + SCIP_Bool* changed, + SCIP_Bool* success) cdef extern from "scip/cons_sos2.h": SCIP_RETCODE SCIPcreateConsSOS2(SCIP* scip, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 3b847ef60..fe22bdf80 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1671,6 +1671,44 @@ cdef class Variable(Expr): else: mayround = SCIPvarMayRoundUp(self.scip_var) return mayround + + def varIsSOS1(self, Conshdlr conshdlr, Variable var): + """ + Returns whether variable is part of the SOS1 conflict graph + + Parameters + ---------- + conshdlr : Conshdlr + SOS1 constraint handler + var : Variable + variable to check + + Returns + ------- + bool + True if variable is part of the SOS1 conflict graph, False otherwise + + """ + return SCIPvarIsSOS1(conshdlr.scip_conshdlr, var.scip_var) + + def varGetNodeSOS1(self, Conshdlr conshdlr, Variable var): + """ + Returns node of variable in the conflict graph or -1 if variable is not part of the SOS1 conflict graph + + Parameters + ---------- + conshdlr : Conshdlr + SOS1 constraint handler + var : Variable + variable + + Returns + ------- + int + node of the variable in the SOS1 conflict graph + + """ + return SCIPvarGetNodeSOS1(conshdlr.scip_conshdlr, var.scip_var) class MatrixVariable(MatrixExpr): @@ -5853,6 +5891,156 @@ cdef class Model: return Constraint.create(scip_cons) + def addVarSOS1(self, Constraint cons, Variable var, weight): + """ + Add variable to SOS1 constraint. + + Parameters + ---------- + cons : Constraint + SOS1 constraint + var : Variable + new variable + weight : weight + weight of new variable + + """ + PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, cons.scip_cons, var.scip_var, weight)) + + def appendVarSOS1(self, Constraint cons, Variable var): + """ + Append variable to SOS1 constraint. + + Parameters + ---------- + cons : Constraint + SOS1 constraint + var : Variable + variable to append + + """ + PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, cons.scip_cons, var.scip_var)) + + def getNVarsSOS1(self, Constraint cons): + """ + Get number of variables in SOS1 constraint. + + Parameters + ---------- + cons : Constraint + SOS1 constraint + + Returns + ------- + int + number of variables in SOS1 constraint + + """ + return SCIPgetNVarsSOS1(self._scip, cons.scip_cons) + + def getVarsSOS1(self, Constraint cons): + """ + Get variables in SOS1 constraint. + + Parameters + ---------- + cons : Constraint + SOS1 constraint + + Returns + ------- + list of Variable + list of variables in SOS1 constraint + + """ + cdef SCIP_VAR** _vars + cdef int nvars + cdef int i + cdef Variable var + cdef list vars = [] + cdef size_t ptr + nvars = SCIPgetNVarsSOS1(self._scip, cons.scip_cons) + _vars = SCIPgetVarsSOS1(self._scip, cons.scip_cons) + for i in range(nvars): + ptr = (_vars[i]) + # check whether the corresponding variable exists already + if ptr in self._modelvars: + vars.append(self._modelvars[ptr]) + else: + # create a new variable + var = Variable.create(_vars[i]) + assert var.ptr() == ptr + self._modelvars[ptr] = var + vars.append(var) + return vars + + def getWeightsSOS1(self, Constraint cons): + """ + Get array of weights in SOS1 constraint (or NULL if not existent). + + Parameters + ---------- + cons : Constraint + SOS1 constraint + + Returns + ------- + list of float + list of weights in SOS1 constraint + + """ + cdef SCIP_VAR** vars + cdef SCIP_Real* weights + cdef int i + + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == 'SOS1': + raise Warning("Weights not available for constraints of type ", constype) + + vals = SCIPgetValsSOS1(self._scip, cons.scip_cons) + vars = SCIPgetVarsSOS1(self._scip, cons.scip_cons) + + valsdict = {} + for i in range(SCIPgetNVarsSOS1(self._scip, cons.scip_cons)): + valsdict[bytes(SCIPvarGetName(vars[i])).decode('utf-8')] = vals[i] + + return valsdict + + def getNSOS1Vars(self, Conshdlr conshdlr): + """ + Gets number of problem variables that are part of the SOS1 conflict graph + + Parameters + ---------- + conshdlr : Conshdlr + constraint handler + + Returns + ------- + int + number of SOS1 variables in constraint handler + + """ + return SCIPgetNSOS1Vars(conshdlr.scip_conshdlr) + + def makeSOS1Feasible(self, Conshdlr conshdlr, Solution sol): + """ + Makes the SOS1 conflict graph feasible + + Parameters + ---------- + conshdlr : Conshdlr + constraint handler + sol : Solution + solution to be made feasible + + """ + cdef SCIP_Bool changed + cdef SCIP_Bool success + PY_SCIP_CALL(SCIPmakeSOS1Feasible(self._scip, conshdlr.scip_conshdlr, sol.scip_sol, &changed, &success)) + if not success: + raise Warning("SOS1 conflict graph could not be made feasible") + def addConsSOS2(self, vars, weights=None, name="SOS2cons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, @@ -6317,36 +6505,6 @@ cdef class Model: PY_SCIP_CALL(SCIPaddCons(self._scip, cons.scip_cons)) Py_INCREF(cons) - def addVarSOS1(self, Constraint cons, Variable var, weight): - """ - Add variable to SOS1 constraint. - - Parameters - ---------- - cons : Constraint - SOS1 constraint - var : Variable - new variable - weight : weight - weight of new variable - - """ - PY_SCIP_CALL(SCIPaddVarSOS1(self._scip, cons.scip_cons, var.scip_var, weight)) - - def appendVarSOS1(self, Constraint cons, Variable var): - """ - Append variable to SOS1 constraint. - - Parameters - ---------- - cons : Constraint - SOS1 constraint - var : Variable - variable to append - - """ - PY_SCIP_CALL(SCIPappendVarSOS1(self._scip, cons.scip_cons, var.scip_var)) - def addVarSOS2(self, Constraint cons, Variable var, weight): """ Add variable to SOS2 constraint. From beb89301b8bf7936bbf12068d99f4a262e790767 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Sat, 26 Apr 2025 20:12:05 +0100 Subject: [PATCH 2/4] Corrected docstring --- src/pyscipopt/scip.pxi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index fe22bdf80..5ea6f0cb0 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1693,7 +1693,7 @@ cdef class Variable(Expr): def varGetNodeSOS1(self, Conshdlr conshdlr, Variable var): """ - Returns node of variable in the conflict graph or -1 if variable is not part of the SOS1 conflict graph + Returns SOS1 index of variable or -1 if variable is not part of the SOS1 conflict graph Parameters ---------- @@ -1705,7 +1705,6 @@ cdef class Variable(Expr): Returns ------- int - node of the variable in the SOS1 conflict graph """ return SCIPvarGetNodeSOS1(conshdlr.scip_conshdlr, var.scip_var) From c7af8c268cc62d27d60576cb70dfb6e277866e5e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Mon, 12 May 2025 21:13:48 +0100 Subject: [PATCH 3/4] tiny assert --- src/pyscipopt/scip.pxi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 5ea6f0cb0..512dc89a9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6036,6 +6036,8 @@ cdef class Model: """ cdef SCIP_Bool changed cdef SCIP_Bool success + + assert bytes(SCIPconshdlrGetName(conshdlr.scip_conshdlr)).decode('UTF-8') == "SOS1" PY_SCIP_CALL(SCIPmakeSOS1Feasible(self._scip, conshdlr.scip_conshdlr, sol.scip_sol, &changed, &success)) if not success: raise Warning("SOS1 conflict graph could not be made feasible") From 6fe5d58bf60e7b96d8af8e3f4c3bf4e9468a01eb Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 29 Jul 2025 06:21:45 +0100 Subject: [PATCH 4/4] getWeightsSOS1 --- src/pyscipopt/scip.pxd | 2 -- src/pyscipopt/scip.pxi | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 25b6159de..dbc85da3f 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -1594,8 +1594,6 @@ cdef extern from "scip/cons_sos1.h": SCIP_Real* SCIPgetWeightsSOS1(SCIP* scip, SCIP_CONS* cons) - int SCIPgetNSOS1Vars(SCIP_CONSHDLR* conshdlr) - SCIP_RETCODE SCIPmakeSOS1Feasible(SCIP* scip, SCIP_CONSHDLR* conshdlr, SCIP_SOL* solution, diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index b75d80825..e2ecb4ea9 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6508,6 +6508,40 @@ cdef class Model: return Constraint.create(scip_cons) + def getWeightsSOS1(self, Constraint cons): + """ + Retrieve the coefficients of an SOS1 constraint + + Parameters + ---------- + cons : Constraint + SOS1 constraint to get the coefficients of + + Returns + ------- + dict of str to float + + """ + cdef SCIP_VAR** vars + cdef SCIP_Longint* vals + cdef int nvars + cdef int i + + constype = bytes(SCIPconshdlrGetName(SCIPconsGetHdlr(cons.scip_cons))).decode('UTF-8') + if not constype == 'SOS1': + raise Warning("weights not available for constraints of type ", constype) + + nvars = SCIPgetNVarsSOS1(self._scip, cons.scip_cons) + vals = SCIPgetWeightsSOS1(self._scip, cons.scip_cons) + vars = SCIPgetVarsSOS1(self._scip, cons.scip_cons) + + valsdict = {} + for i in range(nvars): + var_name = bytes(SCIPvarGetName(vars[i])).decode('utf-8') + valsdict[var_name] = vals[i] + + return valsdict + def addConsSOS2(self, vars, weights=None, name="", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False,