Skip to content

Commit a22bd67

Browse files
Recipe for getting local constraints (#1014)
* Add enableDebugSol * Add recipe for getting local constraints * Update changelog (and remove some stuff from other branch) * Correct stage requirements and allow node input * Add negative check * SCIPwriteMIP * Light changes * Consistency * Copilot suggestions
1 parent 5eecd95 commit a22bd67

File tree

6 files changed

+113
-1
lines changed

6 files changed

+113
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- Added recipe for getting local constraints
56
- More support for AND-Constraints
67
- Added support for knapsack constraints
78
- Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from pyscipopt import Model, Constraint
2+
3+
def getLocalConss(model: Model, node = None) -> list[list[Constraint]]:
4+
"""
5+
Returns local constraints.
6+
7+
Parameters
8+
----------
9+
model : Model
10+
The model from which to retrieve the local constraints.
11+
node : Node, optional
12+
The node from which to retrieve the local constraints. If not provided, the current node is used.
13+
14+
Returns
15+
-------
16+
list[Constraint]
17+
A list of local constraints. First entry are global constraints, second entry are all the added constraints.
18+
"""
19+
20+
if node is None:
21+
assert model.getStageName() in ["INITPRESOLVE", "PRESOLVING", "EXITPRESOLVE", "SOLVING"], "Model cannot be called in stage %s." % model.getStageName()
22+
cur_node = model.getCurrentNode()
23+
else:
24+
cur_node = node
25+
26+
added_conss = []
27+
while cur_node is not None:
28+
added_conss = cur_node.getAddedConss() + added_conss
29+
cur_node = cur_node.getParent()
30+
31+
return [model.getConss(), added_conss]
32+
33+
def getNLocalConss(model: Model, node = None) -> tuple[int,int]:
34+
"""
35+
Returns the number of local constraints of a node.
36+
37+
Parameters
38+
----------
39+
model : Model
40+
The model from which to retrieve the number of local constraints.
41+
node : Node, optional
42+
The node from which to retrieve the number of local constraints. If not provided, the current node is used.
43+
44+
Returns
45+
-------
46+
list[int]
47+
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.
48+
"""
49+
local_conss = getLocalConss(model, node)
50+
return [len(local_conss[0]), len(local_conss[1])]

src/pyscipopt/recipes/nonlinear.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from pyscipopt import Model
22

3-
43
def set_nonlinear_objective(model: Model, expr, sense="minimize"):
54
"""
65
Takes a nonlinear expression and performs an epigraph reformulation.

src/pyscipopt/scip.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,7 @@ cdef extern from "scip/scip.h":
699699
SCIP_RETCODE SCIPwriteOrigProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames)
700700
SCIP_RETCODE SCIPwriteTransProblem(SCIP* scip, char* filename, char* extension, SCIP_Bool genericnames)
701701
SCIP_RETCODE SCIPwriteLP(SCIP* scip, const char*)
702+
SCIP_RETCODE SCIPwriteMIP(SCIP * scip, const char * filename, SCIP_Bool genericnames, SCIP_Bool origobj, SCIP_Bool lazyconss)
702703
SCIP_STATUS SCIPgetStatus(SCIP* scip)
703704
SCIP_Real SCIPepsilon(SCIP* scip)
704705
SCIP_Real SCIPfeastol(SCIP* scip)

src/pyscipopt/scip.pxi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9665,6 +9665,29 @@ cdef class Model:
96659665
PY_SCIP_CALL( SCIPwriteLP(self._scip, absfile) )
96669666

96679667
locale.setlocale(locale.LC_NUMERIC,user_locale)
9668+
9669+
def writeMIP(self, filename, genericnames=False, origobj=False, lazyconss=False):
9670+
"""
9671+
Writes MIP relaxation of the current branch-and-bound node to a file
9672+
9673+
Parameters
9674+
----------
9675+
filename : str
9676+
name of the output file
9677+
genericnames : bool, optional
9678+
should generic names like x_i and row_j be used in order to avoid troubles with reserved symbols? (Default value = False)
9679+
origobj : bool, optional
9680+
should the original objective function be used (Default value = False)
9681+
lazyconss : bool, optional
9682+
output removable rows as lazy constraints? (Default value = False)
9683+
"""
9684+
user_locale = locale.getlocale(category=locale.LC_NUMERIC)
9685+
locale.setlocale(locale.LC_NUMERIC, "C")
9686+
9687+
absfile = str_conversion(abspath(filename))
9688+
PY_SCIP_CALL(SCIPwriteMIP(self._scip, absfile, genericnames, origobj, lazyconss))
9689+
9690+
locale.setlocale(locale.LC_NUMERIC,user_locale)
96689691

96699692
def createSol(self, Heur heur = None, initlp=False):
96709693
"""

tests/test_recipe_getLocalConss.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pyscipopt import Model, SCIP_EVENTTYPE
2+
from pyscipopt.recipes.getLocalConss import *
3+
from helpers.utils import random_mip_1
4+
5+
def localconss(model, event):
6+
local_conss = getLocalConss(model)
7+
assert len(local_conss[1]) == getNLocalConss(model)[1]
8+
assert len(local_conss[0]) == len(model.getConss())
9+
assert local_conss[0] == model.getConss()
10+
11+
vars = model.getVars()
12+
if model.getCurrentNode().getNumber() == 1:
13+
pass
14+
15+
elif model.getCurrentNode().getNumber() == 2:
16+
model.data["local_cons1"] = model.addCons(vars[0] + vars[1] <= 1, name="c1", local=True)
17+
assert getNLocalConss(model)[1] == 1
18+
assert getLocalConss(model)[1][0] == model.data["local_cons1"]
19+
20+
elif model.getCurrentNode().getNumber() == 4:
21+
local_conss = getLocalConss(model)
22+
model.data["local_cons2"] = model.addCons(vars[1] + vars[2] <= 1, name="c2", local=True)
23+
model.data["local_cons3"] = model.addCons(vars[2] + vars[3] <= 1, name="c3", local=True)
24+
assert getNLocalConss(model)[1] == 3
25+
assert getLocalConss(model)[1][0] == model.data["local_cons1"]
26+
assert getLocalConss(model)[1][1] == model.data["local_cons2"]
27+
assert getLocalConss(model)[1][2] == model.data["local_cons3"]
28+
29+
elif model.getCurrentNode().getParent().getNumber() not in [2,4]:
30+
assert getLocalConss(model) == [model.getConss(), []]
31+
32+
def test_getLocalConss():
33+
model = random_mip_1(node_lim=4)
34+
model.data = {}
35+
36+
model.attachEventHandlerCallback(localconss, [SCIP_EVENTTYPE.NODEFOCUSED])
37+
model.optimize()
38+
assert len(model.data) == 3

0 commit comments

Comments
 (0)