Skip to content

Commit 549dd51

Browse files
Change addConsLocal(), addConsNode() to accept ExprCons (#1151)
* change addConsLocal(), addConsNode() to accept ExprCons * copilot suggestions * Update stubs for addConsLocal and addConsNode with all parameters
1 parent c06e0b3 commit 549dd51

File tree

4 files changed

+250
-15
lines changed

4 files changed

+250
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- Speed up np.ndarray(..., dtype=np.float64) @ MatrixExpr
2525
- MatrixExpr and MatrixExprCons use `__array_ufunc__` protocol to control all numpy.ufunc inputs and outputs
2626
- Set `__array_priority__` for MatrixExpr and MatrixExprCons
27+
- changed addConsNode() and addConsLocal() to mirror addCons() and accept ExprCons instead of Constraint
2728
### Removed
2829

2930
## 6.0.0 - 2025.xx.yy

src/pyscipopt/scip.pxi

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6044,7 +6044,7 @@ cdef class Model:
60446044
Parameters
60456045
----------
60466046
cons : ExprCons
6047-
The expression constraint that is not yet an actual constraint
6047+
the constraint expression to add to the model (e.g., x + y <= 5)
60486048
name : str, optional
60496049
the name of the constraint, generic name if empty (Default value = "")
60506050
initial : bool, optional
@@ -6692,43 +6692,131 @@ cdef class Model:
66926692
else:
66936693
raise NotImplementedError("Adding coefficients to %s constraints is not implemented." % constype)
66946694

6695-
def addConsNode(self, Node node, Constraint cons, Node validnode=None):
6695+
def addConsNode(self, Node node, ExprCons cons, Node validnode=None, name='',
6696+
initial=True, separate=True, enforce=True, check=True,
6697+
propagate=True, local=True, dynamic=False, removable=True,
6698+
stickingatnode=True):
66966699
"""
66976700
Add a constraint to the given node.
66986701
66996702
Parameters
67006703
----------
67016704
node : Node
67026705
node at which the constraint will be added
6703-
cons : Constraint
6704-
the constraint to add to the node
6706+
cons : ExprCons
6707+
the constraint expression to add to the node (e.g., x + y <= 5)
67056708
validnode : Node or None, optional
67066709
more global node where cons is also valid. (Default=None)
6710+
name : str, optional
6711+
name of the constraint (Default value = '')
6712+
initial : bool, optional
6713+
should the LP relaxation of constraint be in the initial LP? (Default value = True)
6714+
separate : bool, optional
6715+
should the constraint be separated during LP processing? (Default value = True)
6716+
enforce : bool, optional
6717+
should the constraint be enforced during node processing? (Default value = True)
6718+
check : bool, optional
6719+
should the constraint be checked for feasibility? (Default value = True)
6720+
propagate : bool, optional
6721+
should the constraint be propagated during node processing? (Default value = True)
6722+
local : bool, optional
6723+
is the constraint only valid locally? (Default value = True)
6724+
dynamic : bool, optional
6725+
is the constraint subject to aging? (Default value = False)
6726+
removable : bool, optional
6727+
should the relaxation be removed from the LP due to aging or cleanup? (Default value = True)
6728+
stickingatnode : bool, optional
6729+
should the constraint always be kept at the node where it was added? (Default value = True)
6730+
6731+
Returns
6732+
-------
6733+
Constraint
6734+
The added Constraint object.
67076735
67086736
"""
6737+
assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__
6738+
6739+
cdef SCIP_CONS* scip_cons
6740+
6741+
kwargs = dict(name=name, initial=initial, separate=separate,
6742+
enforce=enforce, check=check, propagate=propagate,
6743+
local=local, modifiable=False, dynamic=dynamic,
6744+
removable=removable, stickingatnode=stickingatnode)
6745+
pycons_initial = self.createConsFromExpr(cons, **kwargs)
6746+
scip_cons = (<Constraint>pycons_initial).scip_cons
6747+
67096748
if isinstance(validnode, Node):
6710-
PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, validnode.scip_node))
6749+
PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, scip_cons, validnode.scip_node))
67116750
else:
6712-
PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, cons.scip_cons, NULL))
6713-
Py_INCREF(cons)
6751+
PY_SCIP_CALL(SCIPaddConsNode(self._scip, node.scip_node, scip_cons, NULL))
6752+
6753+
pycons = Constraint.create(scip_cons)
6754+
pycons.data = (<Constraint>pycons_initial).data
6755+
PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons))
6756+
6757+
return pycons
67146758

6715-
def addConsLocal(self, Constraint cons, Node validnode=None):
6759+
def addConsLocal(self, ExprCons cons, Node validnode=None, name='',
6760+
initial=True, separate=True, enforce=True, check=True,
6761+
propagate=True, local=True, dynamic=False, removable=True,
6762+
stickingatnode=True):
67166763
"""
67176764
Add a constraint to the current node.
67186765
67196766
Parameters
67206767
----------
6721-
cons : Constraint
6722-
the constraint to add to the current node
6768+
cons : ExprCons
6769+
the constraint expression to add to the current node (e.g., x + y <= 5)
67236770
validnode : Node or None, optional
67246771
more global node where cons is also valid. (Default=None)
6772+
name : str, optional
6773+
name of the constraint (Default value = '')
6774+
initial : bool, optional
6775+
should the LP relaxation of constraint be in the initial LP? (Default value = True)
6776+
separate : bool, optional
6777+
should the constraint be separated during LP processing? (Default value = True)
6778+
enforce : bool, optional
6779+
should the constraint be enforced during node processing? (Default value = True)
6780+
check : bool, optional
6781+
should the constraint be checked for feasibility? (Default value = True)
6782+
propagate : bool, optional
6783+
should the constraint be propagated during node processing? (Default value = True)
6784+
local : bool, optional
6785+
is the constraint only valid locally? (Default value = True)
6786+
dynamic : bool, optional
6787+
is the constraint subject to aging? (Default value = False)
6788+
removable : bool, optional
6789+
should the relaxation be removed from the LP due to aging or cleanup? (Default value = True)
6790+
stickingatnode : bool, optional
6791+
should the constraint always be kept at the node where it was added? (Default value = True)
6792+
6793+
Returns
6794+
-------
6795+
Constraint
6796+
The added Constraint object.
67256797
67266798
"""
6799+
assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__
6800+
6801+
cdef SCIP_CONS* scip_cons
6802+
6803+
kwargs = dict(name=name, initial=initial, separate=separate,
6804+
enforce=enforce, check=check, propagate=propagate,
6805+
local=local, modifiable=False, dynamic=dynamic,
6806+
removable=removable, stickingatnode=stickingatnode)
6807+
pycons_initial = self.createConsFromExpr(cons, **kwargs)
6808+
scip_cons = (<Constraint>pycons_initial).scip_cons
6809+
67276810
if isinstance(validnode, Node):
6728-
PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, validnode.scip_node))
6811+
PY_SCIP_CALL(SCIPaddConsLocal(self._scip, scip_cons, validnode.scip_node))
67296812
else:
6730-
PY_SCIP_CALL(SCIPaddConsLocal(self._scip, cons.scip_cons, NULL))
6731-
Py_INCREF(cons)
6813+
PY_SCIP_CALL(SCIPaddConsLocal(self._scip, scip_cons, NULL))
6814+
6815+
pycons = Constraint.create(scip_cons)
6816+
pycons.data = (<Constraint>pycons_initial).data
6817+
PY_SCIP_CALL(SCIPreleaseCons(self._scip, &scip_cons))
6818+
6819+
return pycons
67326820

67336821
def addConsKnapsack(self, vars, weights, capacity, name="",
67346822
initial=True, separate=True, enforce=True, check=True,

src/pyscipopt/scip.pyi

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,10 +682,35 @@ class Model:
682682
stickingatnode: Incomplete = ...,
683683
) -> Incomplete: ...
684684
def addConsLocal(
685-
self, cons: Incomplete, validnode: Incomplete = ...
685+
self,
686+
cons: Incomplete,
687+
validnode: Incomplete = ...,
688+
name: Incomplete = ...,
689+
initial: Incomplete = ...,
690+
separate: Incomplete = ...,
691+
enforce: Incomplete = ...,
692+
check: Incomplete = ...,
693+
propagate: Incomplete = ...,
694+
local: Incomplete = ...,
695+
dynamic: Incomplete = ...,
696+
removable: Incomplete = ...,
697+
stickingatnode: Incomplete = ...,
686698
) -> Incomplete: ...
687699
def addConsNode(
688-
self, node: Incomplete, cons: Incomplete, validnode: Incomplete = ...
700+
self,
701+
node: Incomplete,
702+
cons: Incomplete,
703+
validnode: Incomplete = ...,
704+
name: Incomplete = ...,
705+
initial: Incomplete = ...,
706+
separate: Incomplete = ...,
707+
enforce: Incomplete = ...,
708+
check: Incomplete = ...,
709+
propagate: Incomplete = ...,
710+
local: Incomplete = ...,
711+
dynamic: Incomplete = ...,
712+
removable: Incomplete = ...,
713+
stickingatnode: Incomplete = ...,
689714
) -> Incomplete: ...
690715
def addConsOr(
691716
self,

tests/test_addconsnode.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from pyscipopt import Branchrule, SCIP_RESULT
2+
from helpers.utils import random_mip_1
3+
4+
5+
class MyBranchrule(Branchrule):
6+
"""
7+
A branching rule that tests addConsNode by adding constraints to child nodes.
8+
"""
9+
10+
def __init__(self, model):
11+
self.model = model
12+
self.addConsNode_called = False
13+
14+
def branchexeclp(self, allowaddcons):
15+
if not allowaddcons:
16+
return {"result": SCIP_RESULT.DIDNOTRUN}
17+
18+
# Branch on the first variable (just to test)
19+
var = self.model.getVars()[0]
20+
self.branch_var = var
21+
22+
# Create two child nodes
23+
child1 = self.model.createChild(1, self.model.getLPObjVal())
24+
child2 = self.model.createChild(1, self.model.getLPObjVal())
25+
26+
# Test addConsNode with ExprCons
27+
cons1 = self.model.addConsNode(child1, var == var.getLbGlobal(), name="branch_down")
28+
self.addConsNode_called = True
29+
assert cons1 is not None, "addConsNode should return a Constraint"
30+
31+
# Making it infeasible to ensure down branch is taken
32+
cons2 = self.model.addConsNode(child2, var <= var.getLbGlobal()-1, name="branch_up")
33+
assert cons2 is not None, "addConsNode should return a Constraint"
34+
35+
return {"result": SCIP_RESULT.BRANCHED}
36+
37+
def branchexecps(self, allowaddcons):
38+
return {"result": SCIP_RESULT.DIDNOTRUN}
39+
40+
41+
class MyBranchruleLocal(Branchrule):
42+
"""
43+
A branching rule that tests addConsLocal by adding constraints to the current node.
44+
"""
45+
46+
def __init__(self, model):
47+
self.model = model
48+
self.addConsLocal_called = False
49+
self.call_count = 0
50+
51+
def branchexeclp(self, allowaddcons):
52+
if not allowaddcons:
53+
return {"result": SCIP_RESULT.DIDNOTRUN}
54+
55+
self.call_count += 1
56+
57+
# Only test on the first call
58+
if self.call_count > 1:
59+
return {"result": SCIP_RESULT.DIDNOTRUN}
60+
61+
# Get branching candidates
62+
branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.model.getLPBranchCands()
63+
64+
if npriocands == 0:
65+
return {"result": SCIP_RESULT.DIDNOTRUN}
66+
67+
v = self.model.getVars()[0]
68+
cons = self.model.addConsLocal(v <= v.getLbGlobal() - 1)
69+
self.addConsLocal_called = True
70+
assert cons is not None, "addConsLocal should return a Constraint"
71+
72+
return {"result": SCIP_RESULT.BRANCHED}
73+
74+
def branchexecps(self, allowaddcons):
75+
return {"result": SCIP_RESULT.DIDNOTRUN}
76+
77+
78+
def test_addConsNode():
79+
"""Test that addConsNode works with ExprCons."""
80+
m = random_mip_1(node_lim=3, small=True)
81+
82+
branchrule = MyBranchrule(m)
83+
m.includeBranchrule(
84+
branchrule,
85+
"test_addConsNode",
86+
"test addConsNode with ExprCons",
87+
priority=10000000,
88+
maxdepth=-1,
89+
maxbounddist=1
90+
)
91+
92+
var_to_be_branched = m.getVars()[0]
93+
var_to_be_branched_lb = var_to_be_branched.getLbGlobal()
94+
95+
m.optimize()
96+
97+
assert branchrule.addConsNode_called, "addConsNode should have been called"
98+
99+
var_to_be_branched_val = m.getSolVal(expr=var_to_be_branched, sol=None)
100+
assert var_to_be_branched_val == var_to_be_branched_lb, \
101+
f"Variable should be equal to its lower bound {var_to_be_branched_lb}, but got {var_to_be_branched_val}"
102+
103+
104+
105+
def test_addConsLocal():
106+
"""Test that addConsLocal works with ExprCons."""
107+
m = random_mip_1(node_lim=500, small=True)
108+
109+
branchrule = MyBranchruleLocal(m)
110+
m.includeBranchrule(
111+
branchrule,
112+
"test_addConsLocal",
113+
"test addConsLocal with ExprCons",
114+
priority=10000000,
115+
maxdepth=-1,
116+
maxbounddist=1
117+
)
118+
119+
m.optimize()
120+
assert branchrule.addConsLocal_called, "addConsLocal should have been called"
121+
assert m.getStatus() == "infeasible", "The problem should be infeasible after adding the local constraint"

0 commit comments

Comments
 (0)