From 9a95bd552a4401b5f9272976db51bfa4a2825027 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Sat, 11 Apr 2020 19:01:07 -0500 Subject: [PATCH] Parse warm start value from log files --- src/python/miplearn/solvers.py | 49 +++++++++++++++++++----- src/python/miplearn/tests/test_solver.py | 22 ++++++++++- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/python/miplearn/solvers.py b/src/python/miplearn/solvers.py index db02560..dcba7fe 100644 --- a/src/python/miplearn/solvers.py +++ b/src/python/miplearn/solvers.py @@ -3,8 +3,9 @@ # Released under the modified BSD license. See COPYING.md for more details. import logging +import re import sys -from abc import ABC +from abc import ABC, abstractmethod from copy import deepcopy from io import StringIO @@ -246,8 +247,8 @@ class InternalSolver(ABC): ------- dict A dictionary of solver statistics containing the following keys: - "Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense" - and "Log". + "Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense", + "Log" and "Warm start value". """ total_wallclock_time = 0 streams = [StringIO()] @@ -257,7 +258,8 @@ class InternalSolver(ABC): while True: logger.debug("Solving MIP...") with RedirectOutput(streams): - results = self._pyomo_solver.solve(tee=True) + results = self._pyomo_solver.solve(tee=True, + warmstart=self._is_warm_start_available) total_wallclock_time += results["Solver"][0]["Wallclock time"] if not hasattr(self.instance, "find_violations"): break @@ -271,15 +273,35 @@ class InternalSolver(ABC): cut = self.instance.build_lazy_constraint(self.model, v) self.add_constraint(cut) + log = streams[0].getvalue() return { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], "Wallclock time": total_wallclock_time, "Nodes": 1, "Sense": self._obj_sense, - "Log": streams[0].getvalue() + "Log": log, + "Warm start value": self.extract_warm_start_value(log), } + def extract_warm_start_value(self, log): + """ + Extracts and returns the objective value of the user-provided MIP start + from the provided solver log. If more than one value is found, returns + the last one. If no value is present in the logs, returns None. + """ + ws_value = None + for line in log.splitlines(): + matches = re.findall(self._get_warm_start_regexp(), line) + if len(matches) == 0: + continue + ws_value = float(matches[0]) + return ws_value + + @abstractmethod + def _get_warm_start_regexp(self): + pass + class GurobiSolver(InternalSolver): def __init__(self): @@ -324,15 +346,21 @@ class GurobiSolver(InternalSolver): warmstart=self._is_warm_start_available) self._pyomo_solver.set_callback(None) node_count = int(self._pyomo_solver._solver_model.getAttr("NodeCount")) + + log = streams[0].getvalue() return { "Lower bound": results["Problem"][0]["Lower bound"], "Upper bound": results["Problem"][0]["Upper bound"], "Wallclock time": results["Solver"][0]["Wallclock time"], "Nodes": max(1, node_count), "Sense": self._obj_sense, - "Log": streams[0].getvalue(), - } - + "Log": log, + "Warm start value": self.extract_warm_start_value(log), + } + + def _get_warm_start_regexp(self): + return "MIP start with objective ([0-9.e+-]*)" + class CPLEXSolver(InternalSolver): def __init__(self, @@ -373,7 +401,10 @@ class CPLEXSolver(InternalSolver): return { "Optimal value": results["Problem"][0]["Lower bound"], } - + + def _get_warm_start_regexp(self): + return "MIP start .* with objective ([0-9.e+-]*)\\." + class LearningSolver: """ diff --git a/src/python/miplearn/tests/test_solver.py b/src/python/miplearn/tests/test_solver.py index 1b61971..7027691 100644 --- a/src/python/miplearn/tests/test_solver.py +++ b/src/python/miplearn/tests/test_solver.py @@ -19,7 +19,7 @@ def _get_instance(): ) -def test_internal_solver(): +def test_internal_solver_warm_starts(): for solver in [GurobiSolver(), CPLEXSolver(presolve=False)]: instance = _get_instance() model = instance.to_model() @@ -29,11 +29,31 @@ def test_internal_solver(): "x": { 0: 1.0, 1: 0.0, + 2: 0.0, + 3: 1.0, + } + }) + stats = solver.solve(tee=True) + assert stats["Warm start value"] == 725.0 + + solver.set_warm_start({ + "x": { + 0: 1.0, + 1: 1.0, 2: 1.0, 3: 1.0, } }) + stats = solver.solve(tee=True) + assert stats["Warm start value"] is None + + +def test_internal_solver(): + for solver in [GurobiSolver(), CPLEXSolver(presolve=False)]: + instance = _get_instance() + model = instance.to_model() + solver.set_instance(instance, model) stats = solver.solve(tee=True) assert len(stats["Log"]) > 100 assert stats["Lower bound"] == 1183.0