Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
- 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 optimalNogil, getSolOrigin, retransform, copyModel, solveFirstInterruptOthers
- Added SCIPsolve nogil, SCIPretransformSol, SCIPtranslateSubSol, SCIPsolGetOrigin, SCIPcopyOrigProb, SCIPcopyOrigVars, SCIPcopyOrigConss, SCIPhashmapCreate, SCIPhashmapFree
- Added additional tests to test_multi_threads, test_solution, and test_copy
### Fixed
- Fixed too strict getObjVal, getVal check
### Changed
Expand Down
1 change: 1 addition & 0 deletions src/pyscipopt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 23 additions & 1 deletion src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -532,6 +543,9 @@ cdef extern from "scip/scip.h":
SCIP_Bool threadsafe,
SCIP_Bool passmessagehdlr,
SCIP_Bool* valid)
SCIP_RETCODE SCIPcopyOrigProb(SCIP* sourcescip, SCIP* targetscip, SCIP_HASHMAP* varmap, SCIP_HASHMAP* consmap, const char* name )
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,
Expand Down Expand Up @@ -669,6 +683,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)
Expand Down Expand Up @@ -871,7 +886,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)
Expand Down Expand Up @@ -1367,6 +1384,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)

Expand Down
114 changes: 113 additions & 1 deletion src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from libc.stdio cimport fdopen, fclose
from posix.stdio cimport fileno

from collections.abc import Iterable
from collections.abc import Iterable, Sequence
from itertools import repeat
from dataclasses import dataclass
from concurrent.futures import ThreadPoolExecutor, as_completed

include "expr.pxi"
include "lp.pxi"
Expand Down Expand Up @@ -258,11 +259,21 @@
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
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

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

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Integration-test (3.8)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Windows-test (3.8)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Integration-test (3.9)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Windows-test (3.9)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Integration-test (3.10)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Windows-test (3.10)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Integration-test (3.11)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Windows-test (3.11)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Integration-test (3.12)

SCIP: unspecified error!

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

View workflow job for this annotation

GitHub Actions / Windows-test (3.12)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand Down Expand Up @@ -1009,6 +1020,19 @@
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 getSolOrigin(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))

cdef class BoundChange:
"""Bound change."""
Expand Down Expand Up @@ -2050,6 +2074,39 @@
n = str_conversion(problemName)
PY_SCIP_CALL(SCIPcreateProbBasic(self._scip, n))

def copyModel(self, problemName='copy_model'):
"""
Create a copy of the model/problem.

Parameters
----------
problemName : str, optional
name of model or problem (Default value = 'model')

"""
cdef Model cpy
cdef SCIP_Bool valid
cdef SCIP_HASHMAP* localvarmap
cdef SCIP_HASHMAP* localconsmap

cpy = Model(createscip=False)
PY_SCIP_CALL(SCIPcreate(&cpy._scip))
cpy._bestSol = None
cpy.includeDefaultPlugins()
# cpy._bestSol = <Solution> model._bestSol
cname = str_conversion(problemName)

PY_SCIP_CALL( SCIPhashmapCreate(&localvarmap, SCIPblkmem(cpy._scip), SCIPgetNVars(self._scip)) )
PY_SCIP_CALL( SCIPhashmapCreate(&localconsmap, SCIPblkmem(cpy._scip), SCIPgetNConss(self._scip)) )

PY_SCIP_CALL(SCIPcopyOrigProb(self._scip, cpy._scip, localvarmap, localconsmap, cname))
PY_SCIP_CALL(SCIPcopyOrigVars(self._scip, cpy._scip, localvarmap, localconsmap, NULL, NULL, 0))
PY_SCIP_CALL(SCIPcopyOrigConss(self._scip, cpy._scip, localvarmap, localconsmap, False, &valid))

SCIPhashmapFree(&localvarmap)
SCIPhashmapFree(&localconsmap)
return cpy

def freeProb(self):
"""Frees problem and solution process data."""
PY_SCIP_CALL(SCIPfreeProb(self._scip))
Expand Down Expand Up @@ -6161,6 +6218,44 @@
"""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))

@staticmethod
def solveFirstInterruptOthers(executor: ThreadPoolExecutor, models: Sequence[Model]) -> Tuple[int, Model]:
"""
Solve models return the first solved model and interrupt other models.

Parameters
----------
executor : ThreadPoolExecutor
models: Sequence[Model]

Returns
-------
first_seq : int
returns the index of the first resolved model from models
models[first_idx] : Model
returns the first resolved model
"""
futures = [executor.submit(Model.optimizeNogil, model) for model in models]
interrupt_callback = lambda model: model.interruptSolve()
for future in as_completed(futures):
first_future = future
break
for idx, furture_to_cancel in enumerate(futures):
if furture_to_cancel != first_future:
interrupt_callback(models[idx])
furture_to_cancel.cancel()
else:
first_idx = idx
return first_idx, models[first_idx]

def solveConcurrent(self):
"""Transforms, presolves, and solves problem using additional solvers which emphasize on
Expand Down Expand Up @@ -8026,6 +8121,23 @@
PY_SCIP_CALL(SCIPaddSol(self._scip, solution.sol, &stored))
return stored

def addCopyModelSol(self, Solution solution):
if solution.getSolOrigin() != SCIP_SOLORIGIN_ORIGINAL:
PY_SCIP_CALL(SCIPretransformSol(solution.scip, solution.sol))
cdef Solution newsol = Solution.create(self._scip, NULL)
cdef SCIP_VAR** subvars = SCIPgetOrigVars(solution.scip)
SCIPtranslateSubSol(self._scip, solution.scip, solution.sol, NULL, subvars, &(newsol.sol))
self.addSol(newsol, free=True)

def addCopyModelBestSol(self, Model cpy_model):
solution = cpy_model.getBestSol()
self.addCopyModelSol(solution)

def addCopyModelSols(self, Model cpy_model):
solutions = cpy_model.getSols()
for solution in solutions:
self.addCopyModelSol(solution)

def freeSol(self, Solution solution):
"""
Free given solution
Expand Down
48 changes: 48 additions & 0 deletions tests/test_copy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from pyscipopt import Model
from helpers.utils import random_mip_1


def test_copy():
# create solver instance
Expand All @@ -18,3 +20,49 @@ def test_copy():
s2.optimize()

assert s.getObjVal() == s2.getObjVal()


def test_copyModel():
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=False)
cpy_model = ori_model.copyModel()
sub_model = Model(sourceModel=ori_model)

assert len(ori_model.getParams()) == len(cpy_model.getParams()) > len(sub_model.getParams())
assert ori_model.getNVars() == cpy_model.getNVars()
assert ori_model.getNConss() == cpy_model.getNConss()

ori_model.optimize()
cpy_model.optimize()
assert ori_model.getStatus() == cpy_model.getStatus() == "optimal"
assert ori_model.getObjVal() == cpy_model.getObjVal()


def test_addCopyModelSol_BestSol_Sols():
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=False)
cpy_model0 = ori_model.copyModel()
cpy_model1 = ori_model.copyModel()
cpy_model2 = ori_model.copyModel()

ori_model.optimize()
solution = ori_model.getBestSol()

cpy_model0.addCopyModelSol(solution)
cpy_model1.addCopyModelBestSol(ori_model)
cpy_model2.addCopyModelSols(ori_model)

assert cpy_model0.getNSols() == 1
assert cpy_model1.getNSols() == 1
assert cpy_model2.getNSols() == ori_model.getNSols() >= 1

cpy_model0.optimize()
cpy_model1.optimize()
cpy_model2.optimize()

assert ori_model.getStatus() == "optimal"
assert cpy_model0.getStatus() == "optimal"
assert cpy_model1.getStatus() == "optimal"
assert cpy_model2.getStatus() == "optimal"
assert abs(ori_model.getObjVal() - cpy_model0.getObjVal()) < 1e-6
assert abs(ori_model.getObjVal() - cpy_model1.getObjVal()) < 1e-6
assert abs(ori_model.getObjVal() - cpy_model2.getObjVal()) < 1e-6

36 changes: 36 additions & 0 deletions tests/test_multi_threads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 = [ori_model.copyModel() 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


def test_solveFirstInterruptOthers():
ori_model = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, node_lim=2000, small=True)
models = [ori_model.copyModel() 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:
seq, fast_model = Model.solveFirstInterruptOthers(executor=executor, models=models) #
assert fast_model.getStatus() == "optimal"
assert abs(ori_model.getObjVal() - fast_model.getObjVal()) < 1e-6
15 changes: 14 additions & 1 deletion tests/test_solution.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -193,3 +194,15 @@ def test_getSols():

assert len(m.getSols()) >= 1
assert any(m.isEQ(sol[x], 0.0) for sol in m.getSols())


def test_getSolOrigin_retrasform():
m = random_mip_1(disable_sepa=False, disable_huer=False, disable_presolve=False, small=True)
m.optimize()

sol = m.getBestSol()
assert sol.getSolOrigin() == SCIP_SOLORIGIN.ZERO

sol.retransform()
assert sol.getSolOrigin() == SCIP_SOLORIGIN.ORIGINAL

Loading