diff --git a/CHANGELOG.md b/CHANGELOG.md index 52df46228..aade45fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fixed lotsizing_lazy example ### Changed - changed default value of enablepricing flag to True +- changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint ### Removed ## 6.0.0 - 2025.xx.yy diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index da23028f9..3ecc17b90 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6013,7 +6013,7 @@ cdef class Model: Parameters ---------- cons : ExprCons - The expression constraint that is not yet an actual constraint + the constraint expression to add to the model (e.g., x + y <= 5) name : str, optional the name of the constraint, generic name if empty (Default value = "") initial : bool, optional @@ -6691,7 +6691,10 @@ cdef class Model: else: raise NotImplementedError("Adding coefficients to %s constraints is not implemented." % constype) - def addConsNode(self, Node node, Constraint cons, Node validnode=None): + def addConsNode(self, Node node, ExprCons cons, Node validnode=None, name='', + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=True, dynamic=False, removable=True, + stickingatnode=True): """ Add a constraint to the given node. @@ -6699,35 +6702,120 @@ cdef class Model: ---------- node : Node node at which the constraint will be added - cons : Constraint - the constraint to add to the node + cons : ExprCons + the constraint expression to add to the node (e.g., x + y <= 5) validnode : Node or None, optional more global node where cons is also valid. (Default=None) + name : str, optional + 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 + 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 = True) + 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 = True) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added? (Default value = True) + + Returns + ------- + Constraint + The added Constraint object. """ + assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ + + cdef SCIP_CONS* scip_cons + + kwargs = dict(name=name, initial=initial, separate=separate, + enforce=enforce, check=check, propagate=propagate, + local=local, modifiable=False, dynamic=dynamic, + removable=removable, stickingatnode=stickingatnode) + pycons_initial = self.createConsFromExpr(cons, **kwargs) + scip_cons = (pycons_initial).scip_cons + if isinstance(validnode, Node): - PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, validnode.scip_node)) + PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, scip_cons, validnode.scip_node)) else: - PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, NULL)) - Py_INCREF(cons) + PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, scip_cons, NULL)) + + pycons = Constraint.create(scip_cons) + pycons.data = (pycons_initial).data + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + return pycons - def addConsLocal(self, Constraint cons, Node validnode=None): + def addConsLocal(self, ExprCons cons, Node validnode=None, name='', + initial=True, separate=True, enforce=True, check=True, + propagate=True, local=True, dynamic=False, removable=True, + stickingatnode=True): """ Add a constraint to the current node. Parameters ---------- - cons : Constraint - the constraint to add to the current node + cons : ExprCons + the constraint expression to add to the current node (e.g., x + y <= 5) validnode : Node or None, optional more global node where cons is also valid. (Default=None) + name : str, optional + 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 + 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 = True) + 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 = True) + stickingatnode : bool, optional + should the constraint always be kept at the node where it was added? (Default value = True) + + Returns + ------- + Constraint + The added Constraint object. """ + assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__ + + cdef SCIP_CONS* scip_cons + + kwargs = dict(name=name, initial=initial, separate=separate, + enforce=enforce, check=check, propagate=propagate, + local=local, modifiable=False, dynamic=dynamic, + removable=removable, stickingatnode=stickingatnode) + pycons_initial = self.createConsFromExpr(cons, **kwargs) + scip_cons = (pycons_initial).scip_cons + if isinstance(validnode, Node): - PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, validnode.scip_node)) + PY_SCIP_CALL(SCIPaddConsLocal(self._scip, scip_cons, validnode.scip_node)) else: - PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, NULL)) - Py_INCREF(cons) + PY_SCIP_CALL(SCIPaddConsLocal(self._scip, scip_cons, NULL)) + + pycons = Constraint.create(scip_cons) + pycons.data = (pycons_initial).data + PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons)) + + return pycons def addConsKnapsack(self, vars, weights, capacity, name="", initial=True, separate=True, enforce=True, check=True, diff --git a/tests/test_addconsnode.py b/tests/test_addconsnode.py new file mode 100644 index 000000000..5943c0568 --- /dev/null +++ b/tests/test_addconsnode.py @@ -0,0 +1,121 @@ +from pyscipopt import Branchrule, SCIP_RESULT +from helpers.utils import random_mip_1 + + +class MyBranchrule(Branchrule): + """ + A branching rule that tests addConsNode by adding constraints to child nodes. + """ + + def __init__(self, model): + self.model = model + self.addConsNode_called = False + + def branchexeclp(self, allowaddcons): + if not allowaddcons: + return {"result": SCIP_RESULT.DIDNOTRUN} + + # Branch on the first variable (just to test) + var = self.model.getVars()[0] + self.branch_var = var + + # Create two child nodes + child1 = self.model.createChild(1, self.model.getLPObjVal()) + child2 = self.model.createChild(1, self.model.getLPObjVal()) + + # Test addConsNode with ExprCons + cons1 = self.model.addConsNode(child1, var == var.getLbGlobal(), name="branch_down") + self.addConsNode_called = True + assert cons1 is not None, "addConsNode should return a Constraint" + + # Making it infeasible to ensure down branch is taken + cons2 = self.model.addConsNode(child2, var <= var.getLbGlobal()-1, name="branch_up") + assert cons2 is not None, "addConsNode should return a Constraint" + + return {"result": SCIP_RESULT.BRANCHED} + + def branchexecps(self, allowaddcons): + return {"result": SCIP_RESULT.DIDNOTRUN} + + +class MyBranchruleLocal(Branchrule): + """ + A branching rule that tests addConsLocal by adding constraints to the current node. + """ + + def __init__(self, model): + self.model = model + self.addConsLocal_called = False + self.call_count = 0 + + def branchexeclp(self, allowaddcons): + if not allowaddcons: + return {"result": SCIP_RESULT.DIDNOTRUN} + + self.call_count += 1 + + # Only test on the first call + if self.call_count > 1: + return {"result": SCIP_RESULT.DIDNOTRUN} + + # Get branching candidates + branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.model.getLPBranchCands() + + if npriocands == 0: + return {"result": SCIP_RESULT.DIDNOTRUN} + + v = self.model.getVars()[0] + cons = self.model.addConsLocal(v <= v.getLbGlobal() - 1) + self.addConsLocal_called = True + assert cons is not None, "addConsLocal should return a Constraint" + + return {"result": SCIP_RESULT.BRANCHED} + + def branchexecps(self, allowaddcons): + return {"result": SCIP_RESULT.DIDNOTRUN} + + +def test_addConsNode(): + """Test that addConsNode works with ExprCons.""" + m = random_mip_1(node_lim=3, small=True) + + branchrule = MyBranchrule(m) + m.includeBranchrule( + branchrule, + "test_addConsNode", + "test addConsNode with ExprCons", + priority=10000000, + maxdepth=-1, + maxbounddist=1 + ) + + var_to_be_branched = m.getVars()[0] + var_to_be_branched_lb = var_to_be_branched.getLbGlobal() + + m.optimize() + + assert branchrule.addConsNode_called, "addConsNode should have been called" + + var_to_be_branched_val = m.getSolVal(expr=var_to_be_branched, sol=None) + assert var_to_be_branched_val == var_to_be_branched_lb, \ + f"Variable should be equal to its lower bound {var_to_be_branched_lb}, but got {var_to_be_branched_val}" + + + +def test_addConsLocal(): + """Test that addConsLocal works with ExprCons.""" + m = random_mip_1(node_lim=500, small=True) + + branchrule = MyBranchruleLocal(m) + m.includeBranchrule( + branchrule, + "test_addConsLocal", + "test addConsLocal with ExprCons", + priority=10000000, + maxdepth=-1, + maxbounddist=1 + ) + + m.optimize() + assert branchrule.addConsLocal_called, "addConsLocal should have been called" + assert m.getStatus() == "infeasible", "The problem should be infeasible after adding the local constraint"