Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 101 additions & 13 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

Check failure on line 319 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand All @@ -335,7 +335,7 @@
raise Exception('SCIP: method cannot be called at this time'
+ ' in solution process!')
elif rc == SCIP_INVALIDDATA:
raise Exception('SCIP: error in input data!')

Check failure on line 338 in src/pyscipopt/scip.pxi

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: error in input data!
elif rc == SCIP_INVALIDRESULT:
raise Exception('SCIP: method returned an invalid result code!')
elif rc == SCIP_PLUGINNOTFOUND:
Expand Down Expand Up @@ -6013,7 +6013,7 @@
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
Expand Down Expand Up @@ -6691,43 +6691,131 @@
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.

Parameters
----------
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 = (<Constraint>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 = (<Constraint>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 = (<Constraint>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 = (<Constraint>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,
Expand Down
121 changes: 121 additions & 0 deletions tests/test_addconsnode.py
Original file line number Diff line number Diff line change
@@ -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"
Loading