Fix node count for CPLEX; add tests

pull/3/head
Alinson S. Xavier 6 years ago
parent 672151d945
commit 0e3d080f92

@ -2,12 +2,12 @@
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import numpy as np
from abc import ABC, abstractmethod
from pyomo.core import Var
from tqdm.auto import tqdm, trange
from p_tqdm import p_map
import logging import logging
from abc import ABC, abstractmethod
import numpy as np
from tqdm import tqdm
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

@ -1,6 +1,7 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization # MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. # Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details. # Released under the modified BSD license. See COPYING.md for more details.
import re
import pyomo.environ as pe import pyomo.environ as pe
from scipy.stats import randint from scipy.stats import randint
@ -27,15 +28,6 @@ class CPLEXSolver(InternalSolver):
for (key, value) in options.items(): for (key, value) in options.items():
self._pyomo_solver.options[key] = value self._pyomo_solver.options[key] = value
def set_threads(self, threads):
self._pyomo_solver.options["threads"] = threads
def set_time_limit(self, time_limit):
self._pyomo_solver.options["timelimit"] = time_limit
def set_gap_tolerance(self, gap_tolerance):
self._pyomo_solver.options["mip_tolerances_mipgap"] = gap_tolerance
def solve_lp(self, tee=False): def solve_lp(self, tee=False):
import cplex import cplex
lp = self._pyomo_solver._solver_model lp = self._pyomo_solver._solver_model
@ -48,14 +40,31 @@ class CPLEXSolver(InternalSolver):
"Optimal value": results["Problem"][0]["Lower bound"], "Optimal value": results["Problem"][0]["Lower bound"],
} }
def set_threads(self, threads):
self._pyomo_solver.options["threads"] = threads
def set_time_limit(self, time_limit):
self._pyomo_solver.options["timelimit"] = time_limit
def set_gap_tolerance(self, gap_tolerance):
self._pyomo_solver.options["mip_tolerances_mipgap"] = gap_tolerance
def _get_warm_start_regexp(self): def _get_warm_start_regexp(self):
return "MIP start .* with objective ([0-9.e+-]*)\\." return "MIP start .* with objective ([0-9.e+-]*)\\."
def _get_node_count_regexp(self):
return "^[ *] *([0-9]+)"
def _get_threads_option_name(self): def _get_threads_option_name(self):
return "threads" return "threads"
def _get_time_limit_option_name(self): def _get_time_limit_option_name(self):
return "timelimit" return "timelimit"
def _get_node_limit_option_name(self):
return "mip_limits_nodes"
def _get_gap_tolerance_option_name(self): def _get_gap_tolerance_option_name(self):
return "mip_gap_tolerances_mipgap" return "mip_gap_tolerances_mipgap"

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class GurobiSolver(InternalSolver): class GurobiSolver(InternalSolver):
def __init__(self, def __init__(self,
use_lazy_callbacks=False, use_lazy_callbacks=True,
options=None): options=None):
""" """
Creates a new GurobiSolver. Creates a new GurobiSolver.
@ -70,26 +70,35 @@ class GurobiSolver(InternalSolver):
results = self._pyomo_solver.solve(tee=True, results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available) warmstart=self._is_warm_start_available)
self._pyomo_solver.set_callback(None) self._pyomo_solver.set_callback(None)
node_count = int(self._pyomo_solver._solver_model.getAttr("NodeCount"))
log = streams[0].getvalue() log = streams[0].getvalue()
return { return {
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"], "Upper bound": results["Problem"][0]["Upper bound"],
"Wallclock time": results["Solver"][0]["Wallclock time"], "Wallclock time": results["Solver"][0]["Wallclock time"],
"Nodes": max(1, node_count), "Nodes": self._extract_node_count(log),
"Sense": self._obj_sense, "Sense": self._obj_sense,
"Log": log, "Log": log,
"Warm start value": self.extract_warm_start_value(log), "Warm start value": self._extract_warm_start_value(log),
} }
def _extract_node_count(self, log):
return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount")))
def _get_warm_start_regexp(self): def _get_warm_start_regexp(self):
return "MIP start with objective ([0-9.e+-]*)" return "MIP start with objective ([0-9.e+-]*)"
def _get_node_count_regexp(self):
return None
def _get_threads_option_name(self): def _get_threads_option_name(self):
return "Threads" return "Threads"
def _get_time_limit_option_name(self): def _get_time_limit_option_name(self):
return "TimeLimit" return "TimeLimit"
def _get_node_limit_option_name(self):
return "NodeLimit"
def _get_gap_tolerance_option_name(self): def _get_gap_tolerance_option_name(self):
return "MIPGap" return "MIPGap"

@ -208,6 +208,7 @@ class InternalSolver(ABC):
with RedirectOutput(streams): with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True, results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available) warmstart=self._is_warm_start_available)
print(results)
total_wallclock_time += results["Solver"][0]["Wallclock time"] total_wallclock_time += results["Solver"][0]["Wallclock time"]
if not hasattr(self.instance, "find_violations"): if not hasattr(self.instance, "find_violations"):
break break
@ -226,25 +227,40 @@ class InternalSolver(ABC):
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"], "Upper bound": results["Problem"][0]["Upper bound"],
"Wallclock time": total_wallclock_time, "Wallclock time": total_wallclock_time,
"Nodes": 1, "Nodes": self._extract_node_count(log),
"Sense": self._obj_sense, "Sense": self._obj_sense,
"Log": log, "Log": log,
"Warm start value": self.extract_warm_start_value(log), "Warm start value": self._extract_warm_start_value(log),
} }
def extract_warm_start_value(self, log): @staticmethod
def __extract(log, regexp, default=None):
value = default
for line in log.splitlines():
matches = re.findall(regexp, line)
if len(matches) == 0:
continue
value = matches[0]
return value
def _extract_warm_start_value(self, log):
""" """
Extracts and returns the objective value of the user-provided MIP start 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 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. the last one. If no value is present in the logs, returns None.
""" """
ws_value = None value = self.__extract(log, self._get_warm_start_regexp())
for line in log.splitlines(): if value is not None:
matches = re.findall(self._get_warm_start_regexp(), line) value = float(value)
if len(matches) == 0: return value
continue
ws_value = float(matches[0]) def _extract_node_count(self, log):
return ws_value """
Extracts and returns the number of explored branch-and-bound nodes.
"""
return int(self.__extract(log,
self._get_node_count_regexp(),
default=1))
def set_threads(self, threads): def set_threads(self, threads):
key = self._get_threads_option_name() key = self._get_threads_option_name()
@ -254,6 +270,10 @@ class InternalSolver(ABC):
key = self._get_time_limit_option_name() key = self._get_time_limit_option_name()
self._pyomo_solver.options[key] = time_limit self._pyomo_solver.options[key] = time_limit
def set_node_limit(self, node_limit):
key = self._get_node_limit_option_name()
self._pyomo_solver.options[key] = node_limit
def set_gap_tolerance(self, gap_tolerance): def set_gap_tolerance(self, gap_tolerance):
key = self._get_gap_tolerance_option_name() key = self._get_gap_tolerance_option_name()
self._pyomo_solver.options[key] = gap_tolerance self._pyomo_solver.options[key] = gap_tolerance
@ -262,6 +282,10 @@ class InternalSolver(ABC):
def _get_warm_start_regexp(self): def _get_warm_start_regexp(self):
pass pass
@abstractmethod
def _get_node_count_regexp(self):
pass
@abstractmethod @abstractmethod
def _get_threads_option_name(self): def _get_threads_option_name(self):
pass pass
@ -270,8 +294,13 @@ class InternalSolver(ABC):
def _get_time_limit_option_name(self): def _get_time_limit_option_name(self):
pass pass
@abstractmethod
def _get_node_limit_option_name(self):
pass
@abstractmethod @abstractmethod
def _get_gap_tolerance_option_name(self): def _get_gap_tolerance_option_name(self):
pass pass

@ -7,6 +7,7 @@ from miplearn.solvers.cplex import CPLEXSolver
from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.gurobi import GurobiSolver
from . import _get_instance from . import _get_instance
from ...problems.knapsack import ChallengeA
def test_internal_solver_warm_starts(): def test_internal_solver_warm_starts():
@ -72,3 +73,13 @@ def test_internal_solver():
solver.solve_lp() solver.solve_lp()
assert model.x[0].value == 0.5 assert model.x[0].value == 0.5
def test_node_count():
for solver in [GurobiSolver(),
GurobiSolver(use_lazy_callbacks=False),
CPLEXSolver()]:
challenge = ChallengeA()
solver.set_time_limit(1)
solver.set_instance(challenge.test_instances[0])
stats = solver.solve(tee=True)
assert stats["Nodes"] > 1
Loading…
Cancel
Save