Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added utility interface to SCIP for copyLargeNeighborhoodSearch and its prerequisites #942

Merged
merged 15 commits into from
Jan 15, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Added printProblem to print problem to stdout
- Added stage checks to presolve, freereoptsolve, freetransform
- Added primal_dual_evolution recipe and a plot recipe
- Added python wrappers for usage of SCIPcopyLargeNeighborhoodSearch, SCIPtranslateSubSol and SCIPhashmapCreate
### Fixed
- Added default names to indicator constraints
### Changed
Expand Down
10 changes: 10 additions & 0 deletions src/pyscipopt/scip.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -1825,6 +1825,16 @@ cdef extern from "scip/cons_indicator.h":

SCIP_VAR* SCIPgetSlackVarIndicator(SCIP_CONS* cons)

cdef extern from "scip/misc.h":
SCIP_RETCODE SCIPhashmapCreate(SCIP_HASHMAP** hashmap, BMS_BLKMEM* blkmem, int mapsize)
void SCIPhashmapFree(SCIP_HASHMAP** hashmap)

cdef extern from "scip/scip_copy.h":
SCIP_RETCODE SCIPtranslateSubSol(SCIP* scip, SCIP* subscip, SCIP_SOL* subsol, SCIP_HEUR* heur, SCIP_VAR** subvars, SCIP_SOL** newsol)

cdef extern from "scip/heuristics.h":
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)

cdef extern from "scip/cons_countsols.h":
SCIP_RETCODE SCIPcount(SCIP* scip)
SCIP_RETCODE SCIPsetParamsCountsols(SCIP* scip)
Expand Down
79 changes: 79 additions & 0 deletions src/pyscipopt/scip.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
if rc == SCIP_OKAY:
pass
elif rc == SCIP_ERROR:
raise Exception('SCIP: unspecified error!')

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

View workflow job for this annotation

GitHub Actions / test-coverage (3.11)

SCIP: unspecified error!
elif rc == SCIP_NOMEMORY:
raise MemoryError('SCIP: insufficient memory error!')
elif rc == SCIP_READERROR:
Expand Down Expand Up @@ -6833,6 +6833,85 @@
conshdlr.model = <Model>weakref.proxy(self)
conshdlr.name = name
Py_INCREF(conshdlr)

def copyLargeNeighborhoodSearch(self, to_fix, fix_vals) -> Model:
"""
Creates a configured copy of the transformed problem and applies provided fixings intended for LNS heuristics.

Parameters
----------
to_fix : List[Variable]
A list of variables to fix in the copy
fix_vals : List[Real]
A list of the values to which to fix the variables in the copy (care their order)

Returns
-------
model : Model
A model containing the created copy
"""

orig_vars = SCIPgetVars(self._scip)
vars = <SCIP_VAR**> malloc(len(to_fix) * sizeof(SCIP_VAR*))
vals = <SCIP_Real*> malloc(len(fix_vals) * sizeof(SCIP_Real))
j = 0
name_to_val = {var.name: val for var, val in zip(to_fix, fix_vals)}
for i, var in enumerate(self.getVars()):
if var.name in name_to_val:
vars[j] = orig_vars[i]
vals[j] = <SCIP_Real>name_to_val[var.name]
j+= 1

cdef SCIP_Bool success
cdef SCIP_Bool valid
cdef SCIP* subscip
cdef SCIP_HASHMAP* varmap

PY_SCIP_CALL(SCIPcreate(&subscip))
PY_SCIP_CALL( SCIPhashmapCreate(&varmap, SCIPblkmem(subscip), self.getNVars()) )
PY_SCIP_CALL( SCIPcopyLargeNeighborhoodSearch(self._scip, subscip, varmap, "LNhS_subscip", vars, vals,
<int>len(to_fix), False, False, &success, &valid) )
sub_model = Model.create(subscip)
sub_model._freescip = True
free(vars)
free(vals)
SCIPhashmapFree(&varmap)
return sub_model

def translateSubSol(self, Model sub_model, Solution sol, heur) -> Solution:
"""
Translates a solution of a model copy into a solution of the main model

Parameters
----------
sub_model : Model
The python-wrapper of the subscip
sol : Solution
The python-wrapper of the solution of the subscip
heur : Heur
The python-wrapper of the heuristic that found the solution

Returns
-------
solution : Solution
The corresponding solution in the main model
"""

cdef SCIP_SOL* real_sol
cdef SCIP_SOL* subscip_sol
cdef SCIP_Bool success
subscip_sol = sol.sol
vars = <SCIP_VAR**> malloc(self.getNVars() * sizeof(SCIP_VAR*))
for i, var in enumerate(sub_model.getVars()):
vars[i] = (<Variable>var).scip_var

cdef SCIP_HEUR* _heur
name = str_conversion(heur.name)
_heur = SCIPfindHeur(self._scip, name)
PY_SCIP_CALL( SCIPtranslateSubSol(self._scip, sub_model._scip, subscip_sol, _heur, vars, &real_sol) )
solution = Solution.create(self._scip, real_sol)
free(vars)
return solution

def createCons(self, Conshdlr conshdlr, name, initial=True, separate=True, enforce=True, check=True, propagate=True,
local=False, modifiable=False, dynamic=False, removable=False, stickingatnode=False):
Expand Down
58 changes: 58 additions & 0 deletions tests/test_sub_sol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Tests the usage of sub solutions found in heuristics with copyLargeNeighborhoodSearch()
"""
import pytest
from pyscipopt import Model, Heur, SCIP_HEURTIMING, SCIP_RESULT


class MyHeur(Heur):
def __init__(self, model: Model, fix_vars, fix_vals):
super().__init__()
self.original_model = model
self.used = False
self.fix_vars = fix_vars
self.fix_vals = fix_vals

def heurexec(self, heurtiming, nodeinfeasible):
self.used = True
# fix z to 2 and optimize the remaining problem
m2 = self.original_model.copyLargeNeighborhoodSearch(self.fix_vars, self.fix_vals)
m2.optimize()

# translate the solution to the original problem
sub_sol = m2.getBestSol()
sol_translation = self.original_model.translateSubSol(m2, sub_sol, self)

accepted = self.original_model.trySol(sol_translation)
assert accepted
m2.freeProb()
return {"result": SCIP_RESULT.FOUNDSOL}


def test_sub_sol():
m = Model("sub_sol_test")
x = m.addVar(name="x", lb=0, ub=3, obj=1)
y = m.addVar(name="y", lb=0, ub=3, obj=2)
z = m.addVar(name="z", lb=0, ub=3, obj=3)

m.addCons(4 <= x + y + z)

# include the heuristic
my_heur = MyHeur(m, fix_vars= [z], fix_vals = [2])
m.includeHeur(my_heur, "name", "description", "Y", timingmask=SCIP_HEURTIMING.BEFOREPRESOL, usessubscip=True)

#optimize
m.optimize()
# assert the heuristic did run
assert my_heur.used

heur_sol = [2, 0, 2]
opt_sol = [3, 1, 0]

found_solutions = []
for sol in m.getSols():
found_solutions.append([sol[x], sol[y], sol[z]])

# both the sub_solution and the real optimum should be in the solution pool
assert heur_sol in found_solutions
assert opt_sol in found_solutions