Skip to content

Commit 6318bf8

Browse files
committed
1. model optimize without gil
2. solveFirstInterruptOthers 3. add copy model method 4. add copy model solution 5. add some Solution methods
1 parent 38fb332 commit 6318bf8

File tree

7 files changed

+238
-3
lines changed

7 files changed

+238
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
- Added additional tests to test_nodesel, test_heur, and test_strong_branching
1818
- Migrated documentation to Readthedocs
1919
- `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers
20+
- Added optimalNogil, getSolOrigin, retransform, copyModel, solveFirstInterruptOthers
21+
- Added SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPcopyOrigProb, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPhashmapCreate, SCIPhashmapFree
22+
- Added additional tests to test_multi_threads, test_solution, and test_copy
2023
### Fixed
2124
- Fixed too strict getObjVal, getVal check
2225
### Changed

src/pyscipopt/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@
4646
from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR
4747
from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE
4848
from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE
49+
from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN

src/pyscipopt/scip.pxd

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,17 @@ cdef extern from "scip/scip.h":
320320
SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_SEPA
321321
SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_REOPT
322322

323+
ctypedef int SCIP_SOLORIGIN
324+
cdef extern from "scip/type_sol.h":
325+
SCIP_SOLORIGIN SCIP_SOLORIGIN_ORIGINAL
326+
SCIP_SOLORIGIN SCIP_SOLORIGIN_ZERO
327+
SCIP_SOLORIGIN SCIP_SOLORIGIN_LPSOL
328+
SCIP_SOLORIGIN SCIP_SOLORIGIN_NLPSOL
329+
SCIP_SOLORIGIN SCIP_SOLORIGIN_RELAXSOL
330+
SCIP_SOLORIGIN SCIP_SOLORIGIN_PSEUDOSOL
331+
SCIP_SOLORIGIN SCIP_SOLORIGIN_PARTIAL
332+
SCIP_SOLORIGIN SCIP_SOLORIGIN_UNKNOWN
333+
323334
ctypedef bint SCIP_Bool
324335

325336
ctypedef long long SCIP_Longint
@@ -532,6 +543,9 @@ cdef extern from "scip/scip.h":
532543
SCIP_Bool threadsafe,
533544
SCIP_Bool passmessagehdlr,
534545
SCIP_Bool* valid)
546+
SCIP_RETCODE SCIPcopyOrigProb(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, const char* name )
547+
SCIP_RETCODE SCIPcopyOrigVars(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_VAR** fixedvars, SCIP_Real* fixedvals, int nfixedvars )
548+
SCIP_RETCODE SCIPcopyOrigConss(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_Bool enablepricing, SCIP_Bool* valid)
535549
SCIP_RETCODE SCIPmessagehdlrCreate(SCIP_MESSAGEHDLR **messagehdlr,
536550
SCIP_Bool bufferedoutput,
537551
const char *filename,
@@ -669,6 +683,7 @@ cdef extern from "scip/scip.h":
669683

670684
# Solve Methods
671685
SCIP_RETCODE SCIPsolve(SCIP* scip)
686+
SCIP_RETCODE SCIPsolve(SCIP* scip) noexcept nogil
672687
SCIP_RETCODE SCIPsolveConcurrent(SCIP* scip)
673688
SCIP_RETCODE SCIPfreeTransform(SCIP* scip)
674689
SCIP_RETCODE SCIPpresolve(SCIP* scip)
@@ -871,7 +886,9 @@ cdef extern from "scip/scip.h":
871886
SCIP_RETCODE SCIPreadSolFile(SCIP* scip, const char* filename, SCIP_SOL* sol, SCIP_Bool xml, SCIP_Bool* partial, SCIP_Bool* error)
872887
SCIP_RETCODE SCIPcheckSol(SCIP* scip, SCIP_SOL* sol, SCIP_Bool printreason, SCIP_Bool completely, SCIP_Bool checkbounds, SCIP_Bool checkintegrality, SCIP_Bool checklprows, SCIP_Bool* feasible)
873888
SCIP_RETCODE SCIPcheckSolOrig(SCIP* scip, SCIP_SOL* sol, SCIP_Bool* feasible, SCIP_Bool printreason, SCIP_Bool completely)
874-
889+
SCIP_RETCODE SCIPretransformSol(SCIP* scip, SCIP_SOL* sol)
890+
SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol)
891+
SCIP_SOLORIGIN SCIPsolGetOrigin(SCIP_SOL* sol)
875892
SCIP_Real SCIPgetSolTime(SCIP* scip, SCIP_SOL* sol)
876893

877894
SCIP_RETCODE SCIPsetRelaxSolVal(SCIP* scip, SCIP_RELAX* relax, SCIP_VAR* var, SCIP_Real val)
@@ -1367,6 +1384,11 @@ cdef extern from "scip/scip.h":
13671384

13681385
BMS_BLKMEM* SCIPblkmem(SCIP* scip)
13691386

1387+
# pub_misc.h
1388+
SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize)
1389+
void SCIPhashmapFree(SCIP_HASHMAP** hashmap)
1390+
1391+
13701392
cdef extern from "scip/tree.h":
13711393
int SCIPnodeGetNAddedConss(SCIP_NODE* node)
13721394

src/pyscipopt/scip.pxi

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ from libc.stdlib cimport malloc, free
1515
from libc.stdio cimport fdopen, fclose
1616
from posix.stdio cimport fileno
1717

18-
from collections.abc import Iterable
18+
from collections.abc import Iterable, Sequence
1919
from itertools import repeat
2020
from dataclasses import dataclass
21+
from concurrent.futures import ThreadPoolExecutor, as_completed
2122

2223
include "expr.pxi"
2324
include "lp.pxi"
@@ -258,6 +259,16 @@ cdef class PY_SCIP_ROWORIGINTYPE:
258259
SEPA = SCIP_ROWORIGINTYPE_SEPA
259260
REOPT = SCIP_ROWORIGINTYPE_REOPT
260261

262+
cdef class PY_SCIP_SOLORIGIN:
263+
ORIGINAL = SCIP_SOLORIGIN_ORIGINAL
264+
ZERO = SCIP_SOLORIGIN_ZERO
265+
LPSOL = SCIP_SOLORIGIN_LPSOL
266+
NLPSOL = SCIP_SOLORIGIN_NLPSOL
267+
RELAXSOL = SCIP_SOLORIGIN_RELAXSOL
268+
PSEUDOSOL = SCIP_SOLORIGIN_PSEUDOSOL
269+
PARTIAL = SCIP_SOLORIGIN_PARTIAL
270+
UNKNOWN = SCIP_SOLORIGIN_UNKNOWN
271+
261272
def PY_SCIP_CALL(SCIP_RETCODE rc):
262273
if rc == SCIP_OKAY:
263274
pass
@@ -1009,6 +1020,19 @@ cdef class Solution:
10091020
if not stage_check or self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING:
10101021
raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})")
10111022

1023+
def getSolOrigin(self):
1024+
"""
1025+
Returns origin of solution: where to retrieve uncached elements.
1026+
1027+
Returns
1028+
-------
1029+
PY_SCIP_SOLORIGIN
1030+
"""
1031+
return SCIPsolGetOrigin(self.sol)
1032+
1033+
def retransform(self):
1034+
""" retransforms solution to original problem space """
1035+
PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol))
10121036

10131037
cdef class BoundChange:
10141038
"""Bound change."""
@@ -2050,6 +2074,39 @@ cdef class Model:
20502074
n = str_conversion(problemName)
20512075
PY_SCIP_CALL(SCIPcreateProbBasic(self._scip, n))
20522076

2077+
def copyModel(self, problemName='copy_model'):
2078+
"""
2079+
Create a copy of the model/problem.
2080+
2081+
Parameters
2082+
----------
2083+
problemName : str, optional
2084+
name of model or problem (Default value = 'model')
2085+
2086+
"""
2087+
cdef Model cpy
2088+
cdef SCIP_Bool valid
2089+
cdef SCIP_HASHMAP* localvarmap
2090+
cdef SCIP_HASHMAP* localconsmap
2091+
2092+
cpy = Model(createscip=False)
2093+
PY_SCIP_CALL(SCIPcreate(&cpy._scip))
2094+
cpy._bestSol = None
2095+
cpy.includeDefaultPlugins()
2096+
# cpy._bestSol = <Solution> model._bestSol
2097+
cname = str_conversion(problemName)
2098+
2099+
PY_SCIP_CALL( SCIPhashmapCreate(&localvarmap, SCIPblkmem(cpy._scip), SCIPgetNVars(self._scip)) )
2100+
PY_SCIP_CALL( SCIPhashmapCreate(&localconsmap, SCIPblkmem(cpy._scip), SCIPgetNConss(self._scip)) )
2101+
2102+
PY_SCIP_CALL(SCIPcopyOrigProb(self._scip, cpy._scip, localvarmap, localconsmap, cname))
2103+
PY_SCIP_CALL(SCIPcopyOrigVars(self._scip, cpy._scip, localvarmap, localconsmap, NULL, NULL, 0))
2104+
PY_SCIP_CALL(SCIPcopyOrigConss(self._scip, cpy._scip, localvarmap, localconsmap, False, &valid))
2105+
2106+
SCIPhashmapFree(&localvarmap)
2107+
SCIPhashmapFree(&localconsmap)
2108+
return cpy
2109+
20532110
def freeProb(self):
20542111
"""Frees problem and solution process data."""
20552112
PY_SCIP_CALL(SCIPfreeProb(self._scip))
@@ -6161,6 +6218,44 @@ cdef class Model:
61616218
"""Optimize the problem."""
61626219
PY_SCIP_CALL(SCIPsolve(self._scip))
61636220
self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))
6221+
6222+
def optimizeNogil(self):
6223+
"""Optimize the problem without GIL."""
6224+
cdef SCIP_RETCODE rc;
6225+
with nogil:
6226+
rc = SCIPsolve(self._scip)
6227+
PY_SCIP_CALL(rc)
6228+
self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip))
6229+
6230+
@staticmethod
6231+
def solveFirstInterruptOthers(executor: ThreadPoolExecutor, models: Sequence[Model]) -> Tuple[int, Model]:
6232+
"""
6233+
Solve models return the first solved model and interrupt other models.
6234+
6235+
Parameters
6236+
----------
6237+
executor : ThreadPoolExecutor
6238+
models: Sequence[Model]
6239+
6240+
Returns
6241+
-------
6242+
first_seq : int
6243+
returns the index of the first resolved model from models
6244+
models[first_idx] : Model
6245+
returns the first resolved model
6246+
"""
6247+
futures = [executor.submit(Model.optimizeNogil, model) for model in models]
6248+
interrupt_callback = lambda model: model.interruptSolve()
6249+
for future in as_completed(futures):
6250+
first_future = future
6251+
break
6252+
for idx, furture_to_cancel in enumerate(futures):
6253+
if furture_to_cancel != first_future:
6254+
interrupt_callback(models[idx])
6255+
furture_to_cancel.cancel()
6256+
else:
6257+
first_idx = idx
6258+
return first_idx, models[first_idx]
61646259

61656260
def solveConcurrent(self):
61666261
"""Transforms, presolves, and solves problem using additional solvers which emphasize on
@@ -8026,6 +8121,23 @@ cdef class Model:
80268121
PY_SCIP_CALL(SCIPaddSol(self._scip, solution.sol, &stored))
80278122
return stored
80288123

8124+
def addCopyModelSol(self, Solution solution):
8125+
if solution.getSolOrigin() != SCIP_SOLORIGIN_ORIGINAL:
8126+
PY_SCIP_CALL(SCIPretransformSol(solution.scip, solution.sol))
8127+
cdef Solution newsol = Solution.create(self._scip, NULL)
8128+
cdef SCIP_VAR** subvars = SCIPgetOrigVars(solution.scip)
8129+
SCIPtranslateSubSol(self._scip, solution.scip, solution.sol, NULL, subvars, &(newsol.sol))
8130+
self.addSol(newsol, free=True)
8131+
8132+
def addCopyModelBestSol(self, Model cpy_model):
8133+
solution = cpy_model.getBestSol()
8134+
self.addCopyModelSol(solution)
8135+
8136+
def addCopyModelSols(self, Model cpy_model):
8137+
solutions = cpy_model.getSols()
8138+
for solution in solutions:
8139+
self.addCopyModelSol(solution)
8140+
80298141
def freeSol(self, Solution solution):
80308142
"""
80318143
Free given solution

tests/test_copy.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from pyscipopt import Model
2+
from helpers.utils import random_mip_1
3+
24

35
def test_copy():
46
# create solver instance
@@ -18,3 +20,49 @@ def test_copy():
1820
s2.optimize()
1921

2022
assert s.getObjVal() == s2.getObjVal()
23+
24+
25+
def test_copyModel():
26+
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=False)
27+
cpy_model = ori_model.copyModel()
28+
sub_model = Model(sourceModel=ori_model)
29+
30+
assert len(ori_model.getParams()) == len(cpy_model.getParams()) > len(sub_model.getParams())
31+
assert ori_model.getNVars() == cpy_model.getNVars()
32+
assert ori_model.getNConss() == cpy_model.getNConss()
33+
34+
ori_model.optimize()
35+
cpy_model.optimize()
36+
assert ori_model.getStatus() == cpy_model.getStatus() == "optimal"
37+
assert ori_model.getObjVal() == cpy_model.getObjVal()
38+
39+
40+
def test_addCopyModelSol_BestSol_Sols():
41+
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=False)
42+
cpy_model0 = ori_model.copyModel()
43+
cpy_model1 = ori_model.copyModel()
44+
cpy_model2 = ori_model.copyModel()
45+
46+
ori_model.optimize()
47+
solution = ori_model.getBestSol()
48+
49+
cpy_model0.addCopyModelSol(solution)
50+
cpy_model1.addCopyModelBestSol(ori_model)
51+
cpy_model2.addCopyModelSols(ori_model)
52+
53+
assert cpy_model0.getNSols() == 1
54+
assert cpy_model1.getNSols() == 1
55+
assert cpy_model2.getNSols() == ori_model.getNSols() >= 1
56+
57+
cpy_model0.optimize()
58+
cpy_model1.optimize()
59+
cpy_model2.optimize()
60+
61+
assert ori_model.getStatus() == "optimal"
62+
assert cpy_model0.getStatus() == "optimal"
63+
assert cpy_model1.getStatus() == "optimal"
64+
assert cpy_model2.getStatus() == "optimal"
65+
assert abs(ori_model.getObjVal() - cpy_model0.getObjVal()) < 1e-6
66+
assert abs(ori_model.getObjVal() - cpy_model1.getObjVal()) < 1e-6
67+
assert abs(ori_model.getObjVal() - cpy_model2.getObjVal()) < 1e-6
68+

tests/test_multi_threads.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from concurrent.futures import ThreadPoolExecutor, as_completed
2+
from pyscipopt import Model
3+
from helpers.utils import random_mip_1
4+
5+
N_Threads = 4
6+
7+
8+
def test_optimalNogil():
9+
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True)
10+
models = [ori_model.copyModel() for _ in range(N_Threads)]
11+
for i in range(N_Threads):
12+
models[i].setParam("randomization/permutationseed", i)
13+
14+
ori_model.optimize()
15+
16+
with ThreadPoolExecutor(max_workers=N_Threads) as executor:
17+
futures = [executor.submit(Model.optimizeNogil, model) for model in models]
18+
for future in as_completed(futures):
19+
pass
20+
for model in models:
21+
assert model.getStatus() == "optimal"
22+
assert abs(ori_model.getObjVal() - model.getObjVal()) < 1e-6
23+
24+
25+
def test_solveFirstInterruptOthers():
26+
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True)
27+
models = [ori_model.copyModel() for _ in range(N_Threads)]
28+
for i in range(N_Threads):
29+
models[i].setParam("randomization/permutationseed", i)
30+
31+
ori_model.optimize()
32+
33+
with ThreadPoolExecutor(max_workers=N_Threads) as executor:
34+
seq, fast_model = Model.solveFirstInterruptOthers(executor=executor, models=models) #
35+
assert fast_model.getStatus() == "optimal"
36+
assert abs(ori_model.getObjVal() - fast_model.getObjVal()) < 1e-6

tests/test_solution.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
import pytest
3-
from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod
3+
from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod, SCIP_SOLORIGIN
4+
from helpers.utils import random_mip_1
45

56

67
def test_solution_getbest():
@@ -193,3 +194,15 @@ def test_getSols():
193194

194195
assert len(m.getSols()) >= 1
195196
assert any(m.isEQ(sol[x], 0.0) for sol in m.getSols())
197+
198+
199+
def test_getSolOrigin_retrasform():
200+
m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True)
201+
m.optimize()
202+
203+
sol = m.getBestSol()
204+
assert sol.getSolOrigin() == SCIP_SOLORIGIN.ZERO
205+
206+
sol.retransform()
207+
assert sol.getSolOrigin() == SCIP_SOLORIGIN.ORIGINAL
208+

0 commit comments

Comments
 (0)