Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Option To Kill CBC if it Hangs #701

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions pulp/apis/coin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""

from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, PulpTimeoutError, clock, log
from .core import cbc_path, pulp_cbc_path, coinMP_path, devnull, operating_system
import os
from .. import constants
from tempfile import mktemp
import ctypes
import warnings
from threading import Timer


class COIN_CMD(LpSolver_CMD):
Expand Down Expand Up @@ -64,6 +65,7 @@ def __init__(
timeMode="elapsed",
mip_start=False,
maxNodes=None,
killOnTimeLimit=False,
):
"""
:param bool mip: if False, assume LP even if integer variables
Expand All @@ -85,6 +87,7 @@ def __init__(
:param str timeMode: "elapsed": count wall-time to timeLimit; "cpu": count cpu-time
:param bool mip_start: deprecated for warmStart
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
:param bool killOnTimeLimit: If the solver takes more than 10 seconds than the time limit to stop, kill it and raise an error.
"""

if fracGap is not None:
Expand Down Expand Up @@ -127,6 +130,7 @@ def __init__(
logPath=logPath,
timeMode=timeMode,
maxNodes=maxNodes,
killOnTimeLimit=killOnTimeLimit,
)

def copy(self):
Expand Down Expand Up @@ -203,13 +207,33 @@ def solve_CBC(self, lp, use_mps=True):
)
else:
cbc = subprocess.Popen(args, stdout=pipe, stderr=pipe, stdin=devnull)
if cbc.wait() != 0:
if pipe:
pipe.close()
raise PulpSolverError(
"Pulp: Error while trying to execute, use msg=True for more details"
+ self.path
)

# Optionally implement a timeout that kills the process if it takes too long
timer = None
if self.optionsDict.get('killOnTimeLimit', False) and self.timeLimit is not None:
# Give the solver a buffer above the time limit before we kill it
# since it's better for the solver to timeout gracefully
buffer_time = 10
timer = Timer(self.timeLimit + buffer_time, cbc.kill)
timer.start()

try:
exit_code = cbc.wait()
if exit_code != 0:
if pipe:
pipe.close()
if exit_code == -9 and timer is not None:
raise PulpTimeoutError(
f"Pulp: CBC Solver timed out after {self.timeLimit} seconds"
)
else:
raise PulpSolverError(
f"Pulp: Error while trying to execute, use msg=True for more details {self.path}"
)
finally:
if timer is not None:
timer.cancel()

if pipe:
pipe.close()
if not os.path.exists(tmpSol):
Expand Down Expand Up @@ -392,6 +416,7 @@ def __init__(
logPath=None,
mip_start=False,
timeMode="elapsed",
killOnTimeLimit=False,
):
if path is not None:
raise PulpSolverError("Use COIN_CMD if you want to set a path")
Expand All @@ -416,6 +441,7 @@ def __init__(
logPath=logPath,
mip_start=mip_start,
timeMode=timeMode,
killOnTimeLimit=killOnTimeLimit
)


Expand Down
8 changes: 7 additions & 1 deletion pulp/apis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ class PulpSolverError(const.PulpError):

pass

class PulpTimeoutError(PulpSolverError):
"""
Exception that's raised when we have to kill a solver due to time limit
"""
pass


# import configuration information
def initialize(filename, operating_system="linux", arch="64"):
Expand Down Expand Up @@ -210,7 +216,7 @@ def __init__(
self.timeLimit = timeLimit

# here we will store all other relevant information including:
# gapRel, gapAbs, maxMemory, maxNodes, threads, logPath, timeMode
# gapRel, gapAbs, maxMemory, maxNodes, threads, logPath, timeMode, killOnTimeLimit
self.optionsDict = {k: v for k, v in kwargs.items() if v is not None}

def available(self):
Expand Down
Loading