diff --git a/CHANGELOG.md b/CHANGELOG.md index 069b4c6c1..b5ee70dad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,16 @@ ## Unreleased ### Added +- Add SCIP functions SCIPconsGetNVars, SCIPconsGetVars - Add SCIP functions SCIPchgCoefLinear, SCIPaddCoefLinear and SCIPdelCoefLinear - Add SCIP function SCIPgetSolTime and wrapper getSolTime +- Add convenience methods relax and getVarDict ### Fixed - Pricer plugin fundamental callbacks now raise an error if not implemented - Brachrule plugin fundamental callbacks now raise an error if not implemented - Fixed segmentation fault when accessing the Solution class directly - Changed getSols so that it prints solutions in terms of the original variables +- Fixed error message in _checkStage ### Changed - Improved error message when using < or > instead of <= or >= ### Removed diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 139f4487f..a1c313d90 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -754,6 +754,8 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPreleaseCons(SCIP* scip, SCIP_CONS** cons) SCIP_RETCODE SCIPtransformCons(SCIP* scip, SCIP_CONS* cons, SCIP_CONS** transcons) SCIP_RETCODE SCIPgetTransformedCons(SCIP* scip, SCIP_CONS* cons, SCIP_CONS** transcons) + SCIP_RETCODE SCIPgetConsVars(SCIP* scip, SCIP_CONS* cons, SCIP_VAR** vars, int varssize, SCIP_Bool* success) + SCIP_RETCODE SCIPgetConsNVars(SCIP* scip, SCIP_CONS* cons, int* nvars, SCIP_Bool* success) SCIP_CONS** SCIPgetConss(SCIP* scip) const char* SCIPconsGetName(SCIP_CONS* cons) int SCIPgetNConss(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 99b01df7e..81ffd24e5 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -584,8 +584,8 @@ cdef class Solution: def _checkStage(self, method): if method in ["SCIPgetSolVal", "getSolObjVal"]: - if self.sol == NULL and not SCIPgetStage(self.scip) == SCIP_STAGE_SOLVING: - raise Warning(f"{method} can only be called in stage SOLVING with a valid solution (current stage: {SCIPgetStage(self.scip)})") + if self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING: + raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})") cdef class BoundChange: @@ -1338,7 +1338,6 @@ cdef class Model: else: return SCIPgetTransObjoffset(self._scip) - def setObjIntegral(self): """informs SCIP that the objective value is always integral in every feasible solution Note: This function should be used to inform SCIP that the objective function is integral, helping to improve the @@ -1615,7 +1614,6 @@ cdef class Model: ub = SCIPinfinity(self._scip) PY_SCIP_CALL(SCIPchgVarUb(self._scip, var.scip_var, ub)) - def chgVarLbGlobal(self, Variable var, lb): """Changes the global lower bound of the specified variable. @@ -1729,6 +1727,13 @@ cdef class Model: def getNBinVars(self): """gets number of binary active problem variables""" return SCIPgetNBinVars(self._scip) + + def getVarDict(self): + """gets dictionary with variables names as keys and current variable values as items""" + var_dict = {} + for var in self.getVars(): + var_dict[var.name] = self.getVal(var) + return var_dict def updateNodeLowerbound(self, Node node, lb): """if given value is larger than the node's lower bound (in transformed problem), @@ -1739,6 +1744,14 @@ cdef class Model: """ PY_SCIP_CALL(SCIPupdateNodeLowerbound(self._scip, node.scip_node, lb)) + + def relax(self): + """Relaxes the integrality restrictions of the model""" + if self.getStage() != SCIP_STAGE_PROBLEM: + raise Warning("method can only be called in stage PROBLEM") + + for var in self.getVars(): + self.chgVarType(var, "C") # Node methods def getBestChild(self): @@ -2126,6 +2139,52 @@ cdef class Model: return constraints + def getConsNVars(self, Constraint constraint): + """ + Gets number of variables in a constraint. + + :param constraint: Constraint to get the number of variables from. + """ + cdef int nvars + cdef SCIP_Bool success + + PY_SCIP_CALL(SCIPgetConsNVars(self._scip, constraint.scip_cons, &nvars, &success)) + + if not success: + conshdlr = SCIPconsGetHdlr(constraint.scip_cons) + conshdrlname = SCIPconshdlrGetName(conshdlr) + raise TypeError("The constraint handler %s does not have this functionality." % conshdrlname) + + return nvars + + def getConsVars(self, Constraint constraint): + """ + Gets variables in a constraint. + + :param constraint: Constraint to get the variables from. + """ + cdef SCIP_Bool success + cdef int _nvars + + SCIPgetConsNVars(self._scip, constraint.scip_cons, &_nvars, &success) + + cdef SCIP_VAR** _vars = malloc(_nvars * sizeof(SCIP_VAR*)) + SCIPgetConsVars(self._scip, constraint.scip_cons, _vars, _nvars*sizeof(SCIP_VAR), &success) + + vars = [] + 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 printCons(self, Constraint constraint): return PY_SCIP_CALL(SCIPprintCons(self._scip, constraint.scip_cons, NULL)) @@ -3993,7 +4052,7 @@ cdef class Model: return ([Variable.create(pseudocands[i]) for i in range(npseudocands)], npseudocands, npriopseudocands) - def branchVar(self, variable): + def branchVar(self, Variable variable): """Branch on a non-continuous variable. :param variable: Variable to branch on @@ -5031,4 +5090,4 @@ def is_memory_freed(): return BMSgetMemoryUsed() == 0 def print_memory_in_use(): - BMScheckEmptyMemory() + BMScheckEmptyMemory() \ No newline at end of file diff --git a/tests/test_cons.py b/tests/test_cons.py new file mode 100644 index 000000000..57300f887 --- /dev/null +++ b/tests/test_cons.py @@ -0,0 +1,27 @@ +from pyscipopt import Model, quicksum +import random + + +def test_getConsNVars(): + n_vars = random.randint(100,1000) + m = Model() + x = {} + for i in range(n_vars): + x[i] = m.addVar("%i"%i) + + c = m.addCons(quicksum(x[i] for i in x) <= 10) + assert m.getConsNVars(c) == n_vars + + m.optimize() + assert m.getConsNVars(c) == n_vars + +def test_getConsVars(): + n_vars = random.randint(100,1000) + m = Model() + x = {} + for i in range(n_vars): + x[i] = m.addVar("%i"%i) + + c = m.addCons(quicksum(x[i] for i in x) <= 1) + assert m.getConsVars(c) == [x[i] for i in x] + \ No newline at end of file diff --git a/tests/test_memory.py b/tests/test_memory.py index 13735687d..b0b824713 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -1,5 +1,5 @@ import pytest -from pyscipopt.scip import Model, is_memory_freed +from pyscipopt.scip import Model, is_memory_freed, print_memory_in_use from util import is_optimized_mode def test_not_freed(): @@ -15,6 +15,9 @@ def test_freed(): del m assert is_memory_freed() +def test_print_memory_in_use(): + print_memory_in_use() + if __name__ == "__main__": test_not_freed() - test_freed() + test_freed() \ No newline at end of file diff --git a/tests/test_model.py b/tests/test_model.py index f86fac33b..8223627d3 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -183,11 +183,37 @@ def test_model_ptr(): with pytest.raises(ValueError): Model.from_ptr("some gibberish", take_ownership=False) - -if __name__ == "__main__": - test_model() - test_solve_concurrent() - test_multiple_cons_simple() - test_multiple_cons_names() - test_multiple_cons_params() - test_model_ptr() +def test_model_relax(): + model = Model() + x = {} + for i in range(5): + x[i] = model.addVar(lb = -i, ub = i, vtype="C") + for i in range(10,15): + x[i] = model.addVar(lb = -i, ub = i, vtype="I") + for i in range(20,25): + x[i] = model.addVar(vtype="B") + + model.relax() + for v in x.values(): + var_lb = v.getLbGlobal() + var_ub = v.getUbGlobal() + assert v.getLbGlobal() == var_lb + assert v.getUbGlobal() == var_ub + assert v.vtype() == "CONTINUOUS" + +def test_getVarsDict(): + model = Model() + x = {} + for i in range(5): + x[i] = model.addVar(lb = -i, ub = i, vtype="C") + for i in range(10,15): + x[i] = model.addVar(lb = -i, ub = i, vtype="I") + for i in range(20,25): + x[i] = model.addVar(vtype="B") + + model.hideOutput() + model.optimize() + var_dict = model.getVarDict() + for v in x.values(): + assert v.name in var_dict + assert model.getVal(v) == var_dict[v.name]