diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f917ec92..d52f9b2a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added recipe for getting local constraints - More support for AND-Constraints - Added support for knapsack constraints - Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests diff --git a/src/pyscipopt/recipes/getLocalConss.py b/src/pyscipopt/recipes/getLocalConss.py new file mode 100644 index 000000000..0c3d29646 --- /dev/null +++ b/src/pyscipopt/recipes/getLocalConss.py @@ -0,0 +1,50 @@ +from pyscipopt import Model, Constraint + +def getLocalConss(model: Model, node = None) -> list[Constraint]: + """ + Returns local constraints. + + Parameters + ---------- + model : Model + The model from which to retrieve the local constraints. + node : Node, optional + The node from which to retrieve the local constraints. If not provided, the current node is used. + + Returns + ------- + list[Constraint] + A list of local constraints. First entry are global constraints, second entry are all the added constraints. + """ + + if not node: + assert model.getStageName() in ["INITPRESOLVE", "PRESOLVING", "EXITPRESOLVE", "SOLVING"], "Model cannot be called in stage %s." % model.getStageName() + cur_node = model.getCurrentNode() + else: + cur_node = node + + added_conss = [] + while cur_node is not None: + added_conss = cur_node.getAddedConss() + added_conss + cur_node = cur_node.getParent() + + return [model.getConss(), added_conss] + +def getNLocalConss(model: Model, node = None) -> list[int]: + """ + Returns the number of local constraints of a node. + + Parameters + ---------- + model : Model + The model from which to retrieve the number of local constraints. + node : Node, optional + The node from which to retrieve the number of local constraints. If not provided, the current node is used. + + Returns + ------- + list[int] + A list of the number of local constraints. First entry is the number of global constraints, second entry is the number of all the added constraints. + """ + local_conss = getLocalConss(model, node) + return [len(local_conss[0]), len(local_conss[1])] \ No newline at end of file diff --git a/src/pyscipopt/recipes/nonlinear.py b/src/pyscipopt/recipes/nonlinear.py index d4f485786..41c53b8af 100644 --- a/src/pyscipopt/recipes/nonlinear.py +++ b/src/pyscipopt/recipes/nonlinear.py @@ -1,6 +1,5 @@ from pyscipopt import Model - def set_nonlinear_objective(model: Model, expr, sense="minimize"): """ Takes a nonlinear expression and performs an epigraph reformulation. diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b198b221d..49e7007be 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -699,6 +699,7 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPwriteOrigProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames) SCIP_RETCODE SCIPwriteTransProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames) SCIP_RETCODE SCIPwriteLP(SCIP* scip, const char*) + SCIP_RETCODE SCIPwriteMIP(SCIP * scip, const char * filename, SCIP_Bool genericnames, SCIP_Bool origobj, SCIP_Bool lazyconss) SCIP_STATUS SCIPgetStatus(SCIP* scip) SCIP_Real SCIPepsilon(SCIP* scip) SCIP_Real SCIPfeastol(SCIP* scip) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 7c7216c83..f26f18c52 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -9392,6 +9392,29 @@ cdef class Model: PY_SCIP_CALL( SCIPwriteLP(self._scip, absfile) ) locale.setlocale(locale.LC_NUMERIC,user_locale) + + def writeMIP(self, filename, genericnames=False, origobj=False, lazyconss=False): + """ + Writes MIP relaxation of the current branch-and-bound node to a file + + Parameters + ---------- + filename : str + name of the output file + genericnames : bool, optional + should generic names like x_i and row_j be used in order to avoid troubles with reserved symbols? (Default value = False) + origobj : bool, optional + should the original objective function be used (Default value = False) + lazyconss : bool, optional + output removable rows as lazy constraints? (Default value = False) + """ + user_locale = locale.getlocale(category=locale.LC_NUMERIC) + locale.setlocale(locale.LC_NUMERIC, "C") + + absfile = str_conversion(abspath(filename)) + PY_SCIP_CALL(SCIPwriteMIP(self._scip, absfile, genericnames, origobj, lazyconss)) + + locale.setlocale(locale.LC_NUMERIC,user_locale) def createSol(self, Heur heur = None, initlp=False): """ diff --git a/tests/test_recipe_getLocalConss.py b/tests/test_recipe_getLocalConss.py new file mode 100644 index 000000000..5eb3e0e46 --- /dev/null +++ b/tests/test_recipe_getLocalConss.py @@ -0,0 +1,38 @@ +from pyscipopt import Model, SCIP_EVENTTYPE +from pyscipopt.recipes.getLocalConss import * +from helpers.utils import random_mip_1 + +def localconss(model, event): + local_conss = getLocalConss(model) + assert len(local_conss[1]) == getNLocalConss(model)[1] + assert len(local_conss[0]) == len(model.getConss()) + assert local_conss[0] == model.getConss() + + vars = model.getVars() + if model.getCurrentNode().getNumber() == 1: + pass + + elif model.getCurrentNode().getNumber() == 2: + model.data["local_cons1"] = model.addCons(vars[0] + vars[1] <= 1, name="c1", local=True) + assert getNLocalConss(model)[1] == 1 + assert getLocalConss(model)[1][0] == model.data["local_cons1"] + + elif model.getCurrentNode().getNumber() == 4: + local_conss = getLocalConss(model) + model.data["local_cons2"] = model.addCons(vars[1] + vars[2] <= 1, name="c2", local=True) + model.data["local_cons3"] = model.addCons(vars[2] + vars[3] <= 1, name="c3", local=True) + assert getNLocalConss(model)[1] == 3 + assert getLocalConss(model)[1][0] == model.data["local_cons1"] + assert getLocalConss(model)[1][1] == model.data["local_cons2"] + assert getLocalConss(model)[1][2] == model.data["local_cons3"] + + elif model.getCurrentNode().getParent().getNumber() not in [2,4]: + assert getLocalConss(model) == [model.getConss(), []] + +def test_getLocalConss(): + model = random_mip_1(node_lim=4) + model.data = {} + + model.attachEventHandlerCallback(localconss, [SCIP_EVENTTYPE.NODEFOCUSED]) + model.optimize() + assert len(model.data) == 3 \ No newline at end of file