mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Fix node count for CPLEX; add tests
This commit is contained in:
@@ -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
|
||||||
Reference in New Issue
Block a user