diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c444dd67..203eeb454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ - Added additional tests to test_nodesel, test_heur, and test_strong_branching - Migrated documentation to Readthedocs - `attachEventHandlerCallback` method to Model for a more ergonomic way to attach event handlers +- Added Model method: optimizeNogil +- Added Solution method: getOrigin, retransform, translate +- Added SCIP.pxd: SCIP_SOLORIGIN, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPhashmapCreate, SCIPhashmapFree +- Added additional tests to test_multi_threads, test_solution, and test_copy ### Fixed - Fixed too strict getObjVal, getVal check ### Changed diff --git a/src/pyscipopt/__init__.py b/src/pyscipopt/__init__.py index cd6528f74..eece6c939 100644 --- a/src/pyscipopt/__init__.py +++ b/src/pyscipopt/__init__.py @@ -46,3 +46,4 @@ from pyscipopt.scip import PY_SCIP_BRANCHDIR as SCIP_BRANCHDIR from pyscipopt.scip import PY_SCIP_BENDERSENFOTYPE as SCIP_BENDERSENFOTYPE from pyscipopt.scip import PY_SCIP_ROWORIGINTYPE as SCIP_ROWORIGINTYPE +from pyscipopt.scip import PY_SCIP_SOLORIGIN as SCIP_SOLORIGIN diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index b0ca1ecf2..2b629053c 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -320,6 +320,17 @@ cdef extern from "scip/scip.h": SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_SEPA SCIP_ROWORIGINTYPE SCIP_ROWORIGINTYPE_REOPT + ctypedef int SCIP_SOLORIGIN + cdef extern from "scip/type_sol.h": + SCIP_SOLORIGIN SCIP_SOLORIGIN_ORIGINAL + SCIP_SOLORIGIN SCIP_SOLORIGIN_ZERO + SCIP_SOLORIGIN SCIP_SOLORIGIN_LPSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_NLPSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_RELAXSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_PSEUDOSOL + SCIP_SOLORIGIN SCIP_SOLORIGIN_PARTIAL + SCIP_SOLORIGIN SCIP_SOLORIGIN_UNKNOWN + ctypedef bint SCIP_Bool ctypedef long long SCIP_Longint @@ -532,6 +543,8 @@ cdef extern from "scip/scip.h": SCIP_Bool threadsafe, SCIP_Bool passmessagehdlr, SCIP_Bool* valid) + SCIP_RETCODE SCIPcopyOrigVars(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_VAR** fixedvars, SCIP_Real* fixedvals, int nfixedvars ) + SCIP_RETCODE SCIPcopyOrigConss(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, SCIP_Bool enablepricing, SCIP_Bool* valid) SCIP_RETCODE SCIPmessagehdlrCreate(SCIP_MESSAGEHDLR **messagehdlr, SCIP_Bool bufferedoutput, const char *filename, @@ -669,6 +682,7 @@ cdef extern from "scip/scip.h": # Solve Methods SCIP_RETCODE SCIPsolve(SCIP* scip) + SCIP_RETCODE SCIPsolve(SCIP* scip) noexcept nogil SCIP_RETCODE SCIPsolveConcurrent(SCIP* scip) SCIP_RETCODE SCIPfreeTransform(SCIP* scip) SCIP_RETCODE SCIPpresolve(SCIP* scip) @@ -871,7 +885,9 @@ cdef extern from "scip/scip.h": SCIP_RETCODE SCIPreadSolFile(SCIP* scip, const char* filename, SCIP_SOL* sol, SCIP_Bool xml, SCIP_Bool* partial, SCIP_Bool* error) 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) SCIP_RETCODE SCIPcheckSolOrig(SCIP* scip, SCIP_SOL* sol, SCIP_Bool* feasible, SCIP_Bool printreason, SCIP_Bool completely) - + SCIP_RETCODE SCIPretransformSol(SCIP* scip, SCIP_SOL* sol) + SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol) + SCIP_SOLORIGIN SCIPsolGetOrigin(SCIP_SOL* sol) SCIP_Real SCIPgetSolTime(SCIP* scip, SCIP_SOL* sol) SCIP_RETCODE SCIPsetRelaxSolVal(SCIP* scip, SCIP_RELAX* relax, SCIP_VAR* var, SCIP_Real val) @@ -1367,6 +1383,11 @@ cdef extern from "scip/scip.h": BMS_BLKMEM* SCIPblkmem(SCIP* scip) + # pub_misc.h + SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize) + void SCIPhashmapFree(SCIP_HASHMAP** hashmap) + + cdef extern from "scip/tree.h": int SCIPnodeGetNAddedConss(SCIP_NODE* node) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e24e65efa..d2063f5fd 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -258,6 +258,16 @@ cdef class PY_SCIP_ROWORIGINTYPE: SEPA = SCIP_ROWORIGINTYPE_SEPA REOPT = SCIP_ROWORIGINTYPE_REOPT +cdef class PY_SCIP_SOLORIGIN: + ORIGINAL = SCIP_SOLORIGIN_ORIGINAL + ZERO = SCIP_SOLORIGIN_ZERO + LPSOL = SCIP_SOLORIGIN_LPSOL + NLPSOL = SCIP_SOLORIGIN_NLPSOL + RELAXSOL = SCIP_SOLORIGIN_RELAXSOL + PSEUDOSOL = SCIP_SOLORIGIN_PSEUDOSOL + PARTIAL = SCIP_SOLORIGIN_PARTIAL + UNKNOWN = SCIP_SOLORIGIN_UNKNOWN + def PY_SCIP_CALL(SCIP_RETCODE rc): if rc == SCIP_OKAY: pass @@ -1009,6 +1019,40 @@ cdef class Solution: if not stage_check or self.sol == NULL and SCIPgetStage(self.scip) != SCIP_STAGE_SOLVING: raise Warning(f"{method} can only be called with a valid solution or in stage SOLVING (current stage: {SCIPgetStage(self.scip)})") + def getOrigin(self): + """ + Returns origin of solution: where to retrieve uncached elements. + + Returns + ------- + PY_SCIP_SOLORIGIN + """ + return SCIPsolGetOrigin(self.sol) + + def retransform(self): + """ retransforms solution to original problem space """ + PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol)) + + def translate(self, Model target): + """ + translate solution to a target model solution + + Parameters + ---------- + target : Model + + Returns + ------- + targetSol: Solution + """ + if self.getOrigin() != SCIP_SOLORIGIN_ORIGINAL: + PY_SCIP_CALL(SCIPretransformSol(self.scip, self.sol)) + cdef Solution targetSol = Solution.create(target._scip, NULL) + cdef SCIP_VAR** source_vars = SCIPgetOrigVars(self.scip) + + PY_SCIP_CALL(SCIPtranslateSubSol(target._scip, self.scip, self.sol, NULL, source_vars, &(targetSol.sol))) + return targetSol + cdef class BoundChange: """Bound change.""" @@ -6170,6 +6214,14 @@ cdef class Model: """Optimize the problem.""" PY_SCIP_CALL(SCIPsolve(self._scip)) self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) + + def optimizeNogil(self): + """Optimize the problem without GIL.""" + cdef SCIP_RETCODE rc; + with nogil: + rc = SCIPsolve(self._scip) + PY_SCIP_CALL(rc) + self._bestSol = Solution.create(self._scip, SCIPgetBestSol(self._scip)) def solveConcurrent(self): """Transforms, presolves, and solves problem using additional solvers which emphasize on diff --git a/tests/test_copy.py b/tests/test_copy.py index b518d0a13..dd5aed8ae 100644 --- a/tests/test_copy.py +++ b/tests/test_copy.py @@ -1,4 +1,5 @@ from pyscipopt import Model +from helpers.utils import random_mip_1 def test_copy(): # create solver instance @@ -18,3 +19,4 @@ def test_copy(): s2.optimize() assert s.getObjVal() == s2.getObjVal() + diff --git a/tests/test_nogil.py b/tests/test_nogil.py new file mode 100644 index 000000000..90b6bdfb3 --- /dev/null +++ b/tests/test_nogil.py @@ -0,0 +1,23 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed +from pyscipopt import Model +from helpers.utils import random_mip_1 + +N_Threads = 4 + + +def test_optimalNogil(): + ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True) + models = [Model(sourceModel=ori_model) for _ in range(N_Threads)] + for i in range(N_Threads): + models[i].setParam("randomization/permutationseed", i) + + ori_model.optimize() + + with ThreadPoolExecutor(max_workers=N_Threads) as executor: + futures = [executor.submit(Model.optimizeNogil, model) for model in models] + for future in as_completed(futures): + pass + for model in models: + assert model.getStatus() == "optimal" + assert abs(ori_model.getObjVal() - model.getObjVal()) < 1e-6 + diff --git a/tests/test_solution.py b/tests/test_solution.py index 61846b167..4ab9fd5b6 100644 --- a/tests/test_solution.py +++ b/tests/test_solution.py @@ -1,6 +1,7 @@ import re import pytest -from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod +from pyscipopt import Model, scip, SCIP_PARAMSETTING, quicksum, quickprod, SCIP_SOLORIGIN +from helpers.utils import random_mip_1 def test_solution_getbest(): @@ -193,3 +194,28 @@ def test_getSols(): assert len(m.getSols()) >= 1 assert any(m.isEQ(sol[x], 0.0) for sol in m.getSols()) + + +def test_getOrigin_retrasform(): + m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + m.optimize() + + sol = m.getBestSol() + assert sol.getOrigin() == SCIP_SOLORIGIN.ZERO + + sol.retransform() + assert sol.getOrigin() == SCIP_SOLORIGIN.ORIGINAL + + +def test_translate(): + m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + m.optimize() + sol = m.getBestSol() + + m1 = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True) + sol1 = sol.translate(m1) + assert m1.addSol(sol1) == True + assert m1.getNSols() == 1 + m1.optimize() + assert m.getObjVal() == m1.getObjVal() +