mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Parse warm start value from log files
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
# 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 logging
|
import logging
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from abc import ABC
|
from abc import ABC, abstractmethod
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
@@ -246,8 +247,8 @@ class InternalSolver(ABC):
|
|||||||
-------
|
-------
|
||||||
dict
|
dict
|
||||||
A dictionary of solver statistics containing the following keys:
|
A dictionary of solver statistics containing the following keys:
|
||||||
"Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense"
|
"Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense",
|
||||||
and "Log".
|
"Log" and "Warm start value".
|
||||||
"""
|
"""
|
||||||
total_wallclock_time = 0
|
total_wallclock_time = 0
|
||||||
streams = [StringIO()]
|
streams = [StringIO()]
|
||||||
@@ -257,7 +258,8 @@ class InternalSolver(ABC):
|
|||||||
while True:
|
while True:
|
||||||
logger.debug("Solving MIP...")
|
logger.debug("Solving MIP...")
|
||||||
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)
|
||||||
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
|
||||||
@@ -271,15 +273,35 @@ class InternalSolver(ABC):
|
|||||||
cut = self.instance.build_lazy_constraint(self.model, v)
|
cut = self.instance.build_lazy_constraint(self.model, v)
|
||||||
self.add_constraint(cut)
|
self.add_constraint(cut)
|
||||||
|
|
||||||
|
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": total_wallclock_time,
|
"Wallclock time": total_wallclock_time,
|
||||||
"Nodes": 1,
|
"Nodes": 1,
|
||||||
"Sense": self._obj_sense,
|
"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):
|
class GurobiSolver(InternalSolver):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -324,15 +346,21 @@ class GurobiSolver(InternalSolver):
|
|||||||
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"))
|
node_count = int(self._pyomo_solver._solver_model.getAttr("NodeCount"))
|
||||||
|
|
||||||
|
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": max(1, node_count),
|
||||||
"Sense": self._obj_sense,
|
"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):
|
class CPLEXSolver(InternalSolver):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@@ -374,6 +402,9 @@ class CPLEXSolver(InternalSolver):
|
|||||||
"Optimal value": results["Problem"][0]["Lower bound"],
|
"Optimal value": results["Problem"][0]["Lower bound"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _get_warm_start_regexp(self):
|
||||||
|
return "MIP start .* with objective ([0-9.e+-]*)\\."
|
||||||
|
|
||||||
|
|
||||||
class LearningSolver:
|
class LearningSolver:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def _get_instance():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_internal_solver():
|
def test_internal_solver_warm_starts():
|
||||||
for solver in [GurobiSolver(), CPLEXSolver(presolve=False)]:
|
for solver in [GurobiSolver(), CPLEXSolver(presolve=False)]:
|
||||||
instance = _get_instance()
|
instance = _get_instance()
|
||||||
model = instance.to_model()
|
model = instance.to_model()
|
||||||
@@ -29,11 +29,31 @@ def test_internal_solver():
|
|||||||
"x": {
|
"x": {
|
||||||
0: 1.0,
|
0: 1.0,
|
||||||
1: 0.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,
|
2: 1.0,
|
||||||
3: 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)
|
stats = solver.solve(tee=True)
|
||||||
assert len(stats["Log"]) > 100
|
assert len(stats["Log"]) > 100
|
||||||
assert stats["Lower bound"] == 1183.0
|
assert stats["Lower bound"] == 1183.0
|
||||||
|
|||||||
Reference in New Issue
Block a user