From ba121958d26105deef956329ea368c5533d2e1a3 Mon Sep 17 00:00:00 2001 From: stefanmaak Date: Wed, 15 Jan 2025 15:33:09 +0100 Subject: [PATCH] Added utility interface to SCIP for copyLargeNeighborhoodSearch and its prerequisites (#942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 <57299939+Joao-Dionisio@users.noreply.github.com> * Update tests/test_sub_sol.py Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> * 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 <57299939+Joao-Dionisio@users.noreply.github.com> --- CHANGELOG.md | 1 + src/pyscipopt/scip.pxd | 10 ++++++ src/pyscipopt/scip.pxi | 79 ++++++++++++++++++++++++++++++++++++++++++ tests/test_sub_sol.py | 58 +++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 tests/test_sub_sol.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b323dd31..72dd6bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/pyscipopt/scip.pxd b/src/pyscipopt/scip.pxd index 013faf9d..8662f5d5 100644 --- a/src/pyscipopt/scip.pxd +++ b/src/pyscipopt/scip.pxd @@ -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) diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index e7bb9895..f1eea037 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6833,6 +6833,85 @@ cdef class Model: conshdlr.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 = malloc(len(to_fix) * sizeof(SCIP_VAR*)) + vals = 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] = 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, + 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 = malloc(self.getNVars() * sizeof(SCIP_VAR*)) + for i, var in enumerate(sub_model.getVars()): + vars[i] = (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): diff --git a/tests/test_sub_sol.py b/tests/test_sub_sol.py new file mode 100644 index 00000000..36f8f435 --- /dev/null +++ b/tests/test_sub_sol.py @@ -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