Skip to content

Commit ba12195

Browse files
Added utility interface to SCIP for copyLargeNeighborhoodSearch and its prerequisites (#942)
* Update scip.pxd * Update scip.pxi * Update CHANGELOG.md * Add files via upload added test for copyLargeNeighborhoodSearch and translateSubSol * Rename sub_sol_test.py to test_sub_sol.py * Update src/pyscipopt/scip.pxi Co-authored-by: João Dionísio <[email protected]> * Update tests/test_sub_sol.py Co-authored-by: João Dionísio <[email protected]> * Update scip.pxi * Update scip.pxi * Update scip.pxi hopefully ended white space errors * Update scip.pxi * Update scip.pxi * Update scip.pxi * Update docstring --------- Co-authored-by: João Dionísio <[email protected]>
1 parent 4335bd9 commit ba12195

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Added printProblem to print problem to stdout
99
- Added stage checks to presolve, freereoptsolve, freetransform
1010
- Added primal_dual_evolution recipe and a plot recipe
11+
- Added python wrappers for usage of SCIPcopyLargeNeighborhoodSearch, SCIPtranslateSubSol and SCIPhashmapCreate
1112
### Fixed
1213
- Added default names to indicator constraints
1314
### Changed

src/pyscipopt/scip.pxd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,16 @@ cdef extern from "scip/cons_indicator.h":
18251825

18261826
SCIP_VAR* SCIPgetSlackVarIndicator(SCIP_CONS* cons)
18271827

1828+
cdef extern from "scip/misc.h":
1829+
SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize)
1830+
void SCIPhashmapFree(SCIP_HASHMAP** hashmap)
1831+
1832+
cdef extern from "scip/scip_copy.h":
1833+
SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol)
1834+
1835+
cdef extern from "scip/heuristics.h":
1836+
SCIP_RETCODE SCIPcopyLargeNeighborhoodSearch(SCIP* sourcescip, SCIP* subscip, SCIP_HASHMAP* varmap, const char* suffix, SCIP_VAR** fixedvars, SCIP_Real* fixedvals, int nfixedvars, SCIP_Bool uselprows, SCIP_Bool copycuts, SCIP_Bool* success, SCIP_Bool* valid)
1837+
18281838
cdef extern from "scip/cons_countsols.h":
18291839
SCIP_RETCODE SCIPcount(SCIP* scip)
18301840
SCIP_RETCODE SCIPsetParamsCountsols(SCIP* scip)

src/pyscipopt/scip.pxi

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6833,6 +6833,85 @@ cdef class Model:
68336833
conshdlr.model = <Model>weakref.proxy(self)
68346834
conshdlr.name = name
68356835
Py_INCREF(conshdlr)
6836+
6837+
def copyLargeNeighborhoodSearch(self, to_fix, fix_vals) -> Model:
6838+
"""
6839+
Creates a configured copy of the transformed problem and applies provided fixings intended for LNS heuristics.
6840+
6841+
Parameters
6842+
----------
6843+
to_fix : List[Variable]
6844+
A list of variables to fix in the copy
6845+
fix_vals : List[Real]
6846+
A list of the values to which to fix the variables in the copy (care their order)
6847+
6848+
Returns
6849+
-------
6850+
model : Model
6851+
A model containing the created copy
6852+
"""
6853+
6854+
orig_vars = SCIPgetVars(self._scip)
6855+
vars = <SCIP_VAR**> malloc(len(to_fix) * sizeof(SCIP_VAR*))
6856+
vals = <SCIP_Real*> malloc(len(fix_vals) * sizeof(SCIP_Real))
6857+
j = 0
6858+
name_to_val = {var.name: val for var, val in zip(to_fix, fix_vals)}
6859+
for i, var in enumerate(self.getVars()):
6860+
if var.name in name_to_val:
6861+
vars[j] = orig_vars[i]
6862+
vals[j] = <SCIP_Real>name_to_val[var.name]
6863+
j+= 1
6864+
6865+
cdef SCIP_Bool success
6866+
cdef SCIP_Bool valid
6867+
cdef SCIP* subscip
6868+
cdef SCIP_HASHMAP* varmap
6869+
6870+
PY_SCIP_CALL(SCIPcreate(&subscip))
6871+
PY_SCIP_CALL( SCIPhashmapCreate(&varmap, SCIPblkmem(subscip), self.getNVars()) )
6872+
PY_SCIP_CALL( SCIPcopyLargeNeighborhoodSearch(self._scip, subscip, varmap, "LNhS_subscip", vars, vals,
6873+
<int>len(to_fix), False, False, &success, &valid) )
6874+
sub_model = Model.create(subscip)
6875+
sub_model._freescip = True
6876+
free(vars)
6877+
free(vals)
6878+
SCIPhashmapFree(&varmap)
6879+
return sub_model
6880+
6881+
def translateSubSol(self, Model sub_model, Solution sol, heur) -> Solution:
6882+
"""
6883+
Translates a solution of a model copy into a solution of the main model
6884+
6885+
Parameters
6886+
----------
6887+
sub_model : Model
6888+
The python-wrapper of the subscip
6889+
sol : Solution
6890+
The python-wrapper of the solution of the subscip
6891+
heur : Heur
6892+
The python-wrapper of the heuristic that found the solution
6893+
6894+
Returns
6895+
-------
6896+
solution : Solution
6897+
The corresponding solution in the main model
6898+
"""
6899+
6900+
cdef SCIP_SOL* real_sol
6901+
cdef SCIP_SOL* subscip_sol
6902+
cdef SCIP_Bool success
6903+
subscip_sol = sol.sol
6904+
vars = <SCIP_VAR**> malloc(self.getNVars() * sizeof(SCIP_VAR*))
6905+
for i, var in enumerate(sub_model.getVars()):
6906+
vars[i] = (<Variable>var).scip_var
6907+
6908+
cdef SCIP_HEUR* _heur
6909+
name = str_conversion(heur.name)
6910+
_heur = SCIPfindHeur(self._scip, name)
6911+
PY_SCIP_CALL( SCIPtranslateSubSol(self._scip, sub_model._scip, subscip_sol, _heur, vars, &real_sol) )
6912+
solution = Solution.create(self._scip, real_sol)
6913+
free(vars)
6914+
return solution
68366915

68376916
def createCons(self, Conshdlr conshdlr, name, initial=True, separate=True, enforce=True, check=True, propagate=True,
68386917
local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False):

tests/test_sub_sol.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Tests the usage of sub solutions found in heuristics with copyLargeNeighborhoodSearch()
3+
"""
4+
import pytest
5+
from pyscipopt import Model, Heur, SCIP_HEURTIMING, SCIP_RESULT
6+
7+
8+
class MyHeur(Heur):
9+
def __init__(self, model: Model, fix_vars, fix_vals):
10+
super().__init__()
11+
self.original_model = model
12+
self.used = False
13+
self.fix_vars = fix_vars
14+
self.fix_vals = fix_vals
15+
16+
def heurexec(self, heurtiming, nodeinfeasible):
17+
self.used = True
18+
# fix z to 2 and optimize the remaining problem
19+
m2 = self.original_model.copyLargeNeighborhoodSearch(self.fix_vars, self.fix_vals)
20+
m2.optimize()
21+
22+
# translate the solution to the original problem
23+
sub_sol = m2.getBestSol()
24+
sol_translation = self.original_model.translateSubSol(m2, sub_sol, self)
25+
26+
accepted = self.original_model.trySol(sol_translation)
27+
assert accepted
28+
m2.freeProb()
29+
return {"result": SCIP_RESULT.FOUNDSOL}
30+
31+
32+
def test_sub_sol():
33+
m = Model("sub_sol_test")
34+
x = m.addVar(name="x", lb=0, ub=3, obj=1)
35+
y = m.addVar(name="y", lb=0, ub=3, obj=2)
36+
z = m.addVar(name="z", lb=0, ub=3, obj=3)
37+
38+
m.addCons(4 <= x + y + z)
39+
40+
# include the heuristic
41+
my_heur = MyHeur(m, fix_vars= [z], fix_vals = [2])
42+
m.includeHeur(my_heur, "name", "description", "Y", timingmask=SCIP_HEURTIMING.BEFOREPRESOL, usessubscip=True)
43+
44+
#optimize
45+
m.optimize()
46+
# assert the heuristic did run
47+
assert my_heur.used
48+
49+
heur_sol = [2, 0, 2]
50+
opt_sol = [3, 1, 0]
51+
52+
found_solutions = []
53+
for sol in m.getSols():
54+
found_solutions.append([sol[x], sol[y], sol[z]])
55+
56+
# both the sub_solution and the real optimum should be in the solution pool
57+
assert heur_sol in found_solutions
58+
assert opt_sol in found_solutions

0 commit comments

Comments
 (0)