Skip to content

Commit 4f896dc

Browse files
Add wrappers for methods used in node selection (#1099)
* Add wrappers for methods use in node selection * Add test for getMaxDepth * Add tests for all new methods * Update CHANGELOG * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * Better docstrings for getCutoffbound and getLowerbound * Remove unneeded triggering of tests * Better way to test getMaxDepth * Remove unneeded print --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com>
1 parent c9d87de commit 4f896dc

File tree

5 files changed

+259
-2
lines changed

5 files changed

+259
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Added possibility of having variables in exponent.
66
- Added basic type stubs to help with IDE autocompletion and type checking.
77
- MatrixVariable comparisons (<=, >=, ==) now support numpy's broadcast feature.
8+
- Added methods: getMaxDepth(), getPlungeDepth(), getLowerbound(), getCutoffbound(), getNNodeLPIterations(), getNStrongbranchLPIterations().
89
### Fixed
910
- Implemented all binary operations between MatrixExpr and GenExpr
1011
- Fixed the type of @ matrix operation result from MatrixVariable to MatrixExpr.

src/pyscipopt/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@
5353
from pyscipopt.scip import PY_SCIP_LPSOLSTAT as SCIP_LPSOLSTAT
5454
from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR
5555
from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE
56-
from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE
57-
from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN
56+
from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE
57+
from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN
58+
from pyscipopt.scip import PY_SCIP_NODETYPE as SCIP_NODETYPE

src/pyscipopt/scip.pxd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,6 +1361,12 @@ cdef extern from "scip/scip.h":
13611361
SCIP_Longint SCIPgetNLPs(SCIP* scip)
13621362
SCIP_Longint SCIPgetNLPIterations(SCIP* scip)
13631363
int SCIPgetNSepaRounds(SCIP* scip)
1364+
SCIP_Real SCIPgetLowerbound(SCIP* scip)
1365+
SCIP_Real SCIPgetCutoffbound(SCIP* scip)
1366+
int SCIPgetMaxDepth(SCIP* scip)
1367+
int SCIPgetPlungeDepth(SCIP* scip)
1368+
SCIP_Longint SCIPgetNNodeLPIterations(SCIP* scip)
1369+
SCIP_Longint SCIPgetNStrongbranchLPIterations(SCIP* scip)
13641370

13651371
# Parameter Functions
13661372
SCIP_RETCODE SCIPsetBoolParam(SCIP* scip, char* name, SCIP_Bool value)

src/pyscipopt/scip.pxi

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3025,6 +3025,72 @@ cdef class Model:
30253025
"""
30263026
return SCIPgetDepth(self._scip)
30273027

3028+
def getMaxDepth(self):
3029+
"""
3030+
Gets maximal depth of the branch-and-bound tree processed during solving (excluding probing nodes).
3031+
3032+
Returns
3033+
-------
3034+
int
3035+
3036+
"""
3037+
return SCIPgetMaxDepth(self._scip)
3038+
3039+
def getPlungeDepth(self):
3040+
"""
3041+
Gets current plunging depth (successive selections of child/sibling nodes).
3042+
3043+
Returns
3044+
-------
3045+
int
3046+
3047+
"""
3048+
return SCIPgetPlungeDepth(self._scip)
3049+
3050+
def getLowerbound(self):
3051+
"""
3052+
Gets global lower (dual) bound of the transformed problem.
3053+
3054+
Returns
3055+
-------
3056+
float
3057+
3058+
"""
3059+
return SCIPgetLowerbound(self._scip)
3060+
3061+
def getCutoffbound(self):
3062+
"""
3063+
Gets the cutoff bound of the transformed problem.
3064+
3065+
Returns
3066+
-------
3067+
float
3068+
3069+
"""
3070+
return SCIPgetCutoffbound(self._scip)
3071+
3072+
def getNNodeLPIterations(self):
3073+
"""
3074+
Gets number of LP iterations used for solving node relaxations so far.
3075+
3076+
Returns
3077+
-------
3078+
int
3079+
3080+
"""
3081+
return SCIPgetNNodeLPIterations(self._scip)
3082+
3083+
def getNStrongbranchLPIterations(self):
3084+
"""
3085+
Gets number of LP iterations used for strong branching so far.
3086+
3087+
Returns
3088+
-------
3089+
int
3090+
3091+
"""
3092+
return SCIPgetNStrongbranchLPIterations(self._scip)
3093+
30283094
def cutoffNode(self, Node node):
30293095
"""
30303096
marks node and whole subtree to be cut off from the branch and bound tree.

tests/test_node_methods.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from helpers.utils import random_mip_1
2+
from pyscipopt import Eventhdlr, SCIP_EVENTTYPE, SCIP_RESULT
3+
4+
class MaxDepthTracker(Eventhdlr):
5+
def __init__(self):
6+
super().__init__()
7+
self.max_depth = -1
8+
9+
def eventinit(self):
10+
self.model.catchEvent(SCIP_EVENTTYPE.NODEFOCUSED, self)
11+
12+
def eventexec(self, event):
13+
current_node = self.model.getCurrentNode()
14+
if current_node is not None:
15+
depth = current_node.getDepth()
16+
self.max_depth = max(self.max_depth, depth)
17+
return {'result': SCIP_RESULT.SUCCESS}
18+
19+
def test_getMaxDepth():
20+
m = random_mip_1(
21+
disable_sepa=True,
22+
disable_heur=True,
23+
disable_presolve=True,
24+
small=True
25+
)
26+
27+
print(f"Initial max depth: {m.getMaxDepth()}")
28+
assert m.getMaxDepth() == -1
29+
30+
tracker = MaxDepthTracker()
31+
m.includeEventhdlr(tracker, "maxdepth_tracker", "Tracks maximum depth of nodes")
32+
33+
m.optimize()
34+
35+
max_depth = m.getMaxDepth()
36+
tracked_max_depth = tracker.max_depth
37+
nodes = m.getNNodes()
38+
39+
print(f"Max depth after solving: {max_depth}")
40+
print(f"Tracked max depth: {tracked_max_depth}")
41+
print(f"Number of nodes explored: {nodes}")
42+
print(f"Optimization status: {m.getStatus()}")
43+
44+
assert max_depth >= 0, f"Expected max_depth >= 0, got {max_depth}"
45+
46+
if nodes > 1:
47+
assert max_depth >= 1, f"Expected max_depth >= 1 with {nodes} nodes, got {max_depth}"
48+
49+
assert max_depth <= nodes, f"Max depth {max_depth} shouldn't exceed nodes {nodes}"
50+
51+
# Verify that getMaxDepth() matches the actual maximum depth of all nodes
52+
assert max_depth == tracked_max_depth, f"getMaxDepth() returned {max_depth} but tracked max depth is {tracked_max_depth}"
53+
54+
55+
def test_getPlungeDepth():
56+
m = random_mip_1(
57+
disable_sepa=True,
58+
disable_heur=True,
59+
disable_presolve=True,
60+
small=True
61+
)
62+
63+
initial_plunge = m.getPlungeDepth()
64+
print(f"Initial plunge depth: {initial_plunge}")
65+
assert initial_plunge == 0, f"Expected initial plunge depth to be 0, got {initial_plunge}"
66+
67+
m.optimize()
68+
69+
plunge_depth = m.getPlungeDepth()
70+
nodes = m.getNNodes()
71+
max_depth = m.getMaxDepth()
72+
73+
print(f"Plunge depth after solving: {plunge_depth}")
74+
print(f"Number of nodes: {nodes}")
75+
print(f"Max depth: {max_depth}")
76+
77+
assert plunge_depth >= 0, f"Expected plunge_depth >= 0, got {plunge_depth}"
78+
79+
# If we explored multiple nodes and reached some depth, we likely did some plunging
80+
if nodes > 1 and max_depth > 0:
81+
assert plunge_depth >= 1, f"Expected plunge_depth >= 1 with {nodes} nodes and max_depth {max_depth}, got {plunge_depth}"
82+
83+
84+
def test_getLowerbound():
85+
m = random_mip_1(
86+
disable_sepa=True,
87+
disable_heur=True,
88+
disable_presolve=True,
89+
small=True
90+
)
91+
92+
initial_lb = m.getLowerbound()
93+
print(f"Initial lower bound: {initial_lb}")
94+
95+
m.optimize()
96+
97+
lower_bound = m.getLowerbound()
98+
obj_val = m.getObjVal()
99+
100+
print(f"Lower bound after solving: {lower_bound}")
101+
print(f"Status: {m.getStatus()}")
102+
103+
assert initial_lb < lower_bound, f"Expected initial lower bound {initial_lb} to be less than final lower bound {lower_bound}"
104+
105+
106+
def test_getCutoffbound():
107+
m = random_mip_1(
108+
disable_sepa=True,
109+
disable_heur=True,
110+
disable_presolve=True,
111+
node_lim=10000,
112+
small=True
113+
)
114+
115+
m.setIntParam("limits/solutions", 1)
116+
117+
m.optimize()
118+
119+
cutoff = m.getCutoffbound()
120+
obj_val = m.getObjVal() if m.getNSols() > 0 else None
121+
122+
print(f"Cutoff bound after solving: {cutoff}")
123+
print(f"Objective value: {obj_val}")
124+
print(f"Status: {m.getStatus()}")
125+
126+
assert abs(cutoff - obj_val) < 1e-6, f"Cutoff {cutoff} should equal optimal value {obj_val}"
127+
128+
129+
def test_getNNodeLPIterations():
130+
m = random_mip_1(
131+
disable_sepa=False,
132+
disable_heur=True,
133+
disable_presolve=True,
134+
node_lim=30,
135+
small=True
136+
)
137+
138+
initial_lp_iters = m.getNNodeLPIterations()
139+
print(f"Initial node LP iterations: {initial_lp_iters}")
140+
assert initial_lp_iters == 0, f"Expected 0 initial LP iterations, got {initial_lp_iters}"
141+
142+
m.optimize()
143+
144+
lp_iters = m.getNNodeLPIterations()
145+
total_lp_iters = m.getNLPIterations()
146+
nodes = m.getNNodes()
147+
148+
print(f"Node LP iterations after solving: {lp_iters}")
149+
print(f"Total LP iterations: {total_lp_iters}")
150+
print(f"Number of nodes: {nodes}")
151+
152+
assert lp_iters >= 0, f"Expected non-negative LP iterations, got {lp_iters}"
153+
assert lp_iters <= total_lp_iters, f"Node LP iterations {lp_iters} should not exceed total LP iterations {total_lp_iters}"
154+
155+
if nodes > 0:
156+
assert lp_iters > 0, f"Expected positive LP iterations with {nodes} nodes explored"
157+
158+
159+
def test_getNStrongbranchLPIterations():
160+
m = random_mip_1(
161+
disable_sepa=True,
162+
disable_heur=True,
163+
disable_presolve=True,
164+
node_lim=20,
165+
small=True
166+
)
167+
168+
initial_sb_iters = m.getNStrongbranchLPIterations()
169+
print(f"Initial strong branching LP iterations: {initial_sb_iters}")
170+
assert initial_sb_iters == 0, f"Expected 0 initial strong branching iterations, got {initial_sb_iters}"
171+
172+
m.optimize()
173+
174+
sb_iters = m.getNStrongbranchLPIterations()
175+
total_lp_iters = m.getNLPIterations()
176+
nodes = m.getNNodes()
177+
178+
print(f"Strong branching LP iterations: {sb_iters}")
179+
print(f"Total LP iterations: {total_lp_iters}")
180+
print(f"Number of nodes: {nodes}")
181+
182+
assert sb_iters >= 0, f"Expected non-negative strong branching iterations, got {sb_iters}"
183+
assert sb_iters <= total_lp_iters, f"Strong branching iterations {sb_iters} should not exceed total LP iterations {total_lp_iters}"

0 commit comments

Comments
 (0)