Skip to content

Commit

Permalink
Added utility interface to SCIP for copyLargeNeighborhoodSearch and i…
Browse files Browse the repository at this point in the history
…ts 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]>
  • Loading branch information
stefanmaak and Joao-Dionisio authored Jan 15, 2025
1 parent 4335bd9 commit ba12195
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 0 deletions.
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 @@ -6833,6 +6833,85 @@ cdef class Model:
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

0 comments on commit ba12195

Please sign in to comment.