Skip to content

Commit

Permalink
Fix pulp cbc cmd solver params (#778)
Browse files Browse the repository at this point in the history
* Add pulp/tests/utilities

* Add unit tests to pulp/tests/test_pulp

* Fix command passed to cbc application from PULP_CBC_CMD parameters

* Remove type annotations and add docstring

* Move tests to PULP_CBC_CMDTest

* Move functions in tests/utilities to PULP_CBC_CMDTest
  • Loading branch information
marcusreaiche authored Oct 22, 2024
1 parent 76b8d2e commit de9b104
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 5 deletions.
22 changes: 17 additions & 5 deletions pulp/apis/coin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ def __init__(
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param str logPath: path to the log file
:param bool presolve: if True, adds presolve on
:param bool cuts: if True, adds gomory on knapsack on probing on
:param bool strong: if True, adds strong
:param bool presolve: if True, adds presolve on, if False, adds presolve off
:param bool cuts: if True, adds gomory on knapsack on probing on, if False adds cuts off
:param int strong: number of variables to look at in strong branching (range is 0 to 2147483647)
:param str timeMode: "elapsed": count wall-time to timeLimit; "cpu": count cpu-time
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
"""
Expand Down Expand Up @@ -142,6 +142,20 @@ def solve_CBC(self, lp, use_mps=True):
cmds += f"-mips {tmpMst} "
if self.timeLimit is not None:
cmds += f"-sec {self.timeLimit} "
if self.optionsDict.get("presolve") is not None:
if self.optionsDict["presolve"]:
# presolve is True: add 'presolve on'
cmds += f"-presolve on "
else:
# presolve is False: add 'presolve off'
cmds += f"-presolve off "
if self.optionsDict.get("cuts") is not None:
if self.optionsDict["cuts"]:
# activate gomory, knapsack, and probing cuts
cmds += f"-gomory on knapsack on probing on "
else:
# turn off all cuts
cmds += f"-cuts off "
options = self.options + self.getOptions()
for option in options:
cmds += "-" + option + " "
Expand Down Expand Up @@ -209,9 +223,7 @@ def getOptions(self):
gapRel="ratio {}",
gapAbs="allow {}",
threads="threads {}",
presolve="presolve on",
strong="strong {}",
cuts="gomory on knapsack on probing on",
timeMode="timeMode {}",
maxNodes="maxNodes {}",
)
Expand Down
194 changes: 194 additions & 0 deletions pulp/tests/test_pulp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pulp import constants as const
from pulp.tests.bin_packing_problem import create_bin_packing_problem
from pulp.utilities import makeDict
import re
import functools
import unittest

Expand Down Expand Up @@ -1440,6 +1441,199 @@ def test_multiply_nan_values(self):
class PULP_CBC_CMDTest(BaseSolverTest.PuLPTest):
solveInst = PULP_CBC_CMD

@staticmethod
def read_command_line_from_log_file(logPath):
"""
Read from log file the command line executed.
"""
with open(logPath) as fp:
for row in fp.readlines():
if row.startswith("command line "):
return row
raise ValueError(f"Unable to find the command line in {logPath}")

@staticmethod
def extract_option_from_command_line(
command_line, option, prefix="-", grp_pattern="[a-zA-Z]+"
):
"""
Extract option value from command line string.
:param command_line: str that we extract the option value from
:param option: str representing the option name (e.g., presolve, sec, etc)
:param prefix: str (default: '-')
:param grp_pattern: str (default: '[a-zA-Z]+') - regex to capture option value
:return: option value captured (str); otherwise, None
example:
>>> cmd = "cbc model.mps -presolve off -timeMode elapsed -branch"
>>> PULP_CBC_CMDTest.extract_option_from_command_line(cmd, "presolve")
'off'
>>> cmd = "cbc model.mps -strong 101 -timeMode elapsed -branch"
>>> PULP_CBC_CMDTest.extract_option_from_command_line(cmd, "strong", grp_pattern="\d+")
'101'
"""
pattern = re.compile(rf"{prefix}{option}\s+({grp_pattern})\s*")
m = pattern.search(command_line)
if not m:
print(f"{option} not found in {command_line}")
return None
option_value = m.groups()[0]
return option_value

def test_presolve_off(self):
"""
Test if setting presolve=False in PULP_CBC_CMD adds presolve off to the
command line.
"""
name = self._testMethodName
prob = LpProblem(name, const.LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0)
w = LpVariable("w", 0)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7, "c3"
prob += w >= 0, "c4"
logFilename = name + ".log"
self.solver.optionsDict["logPath"] = logFilename
self.solver.optionsDict["presolve"] = False
pulpTestCheck(
prob,
self.solver,
[const.LpStatusOptimal],
{x: 4, y: -1, z: 6, w: 0},
)
if not os.path.exists(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
if not os.path.getsize(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
# Extract option_value from command line
command_line = PULP_CBC_CMDTest.read_command_line_from_log_file(logFilename)
option_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="presolve"
)
self.assertEqual("off", option_value)

def test_cuts_on(self):
"""
Test if setting cuts=True in PULP_CBC_CMD adds "gomory on knapsack on
probing on" to the command line.
"""
name = self._testMethodName
prob = LpProblem(name, const.LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0)
w = LpVariable("w", 0)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7, "c3"
prob += w >= 0, "c4"
logFilename = name + ".log"
self.solver.optionsDict["logPath"] = logFilename
self.solver.optionsDict["cuts"] = True
pulpTestCheck(
prob,
self.solver,
[const.LpStatusOptimal],
{x: 4, y: -1, z: 6, w: 0},
)
if not os.path.exists(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
if not os.path.getsize(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
# Extract option values from command line
command_line = PULP_CBC_CMDTest.read_command_line_from_log_file(logFilename)
gomory_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="gomory"
)
knapsack_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="knapsack", prefix=""
)
probing_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="probing", prefix=""
)
self.assertListEqual(
["on", "on", "on"], [gomory_value, knapsack_value, probing_value]
)

def test_cuts_off(self):
"""
Test if setting cuts=False adds cuts off to the command line.
"""
name = self._testMethodName
prob = LpProblem(name, const.LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0)
w = LpVariable("w", 0)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7, "c3"
prob += w >= 0, "c4"
logFilename = name + ".log"
self.solver.optionsDict["logPath"] = logFilename
self.solver.optionsDict["cuts"] = False
pulpTestCheck(
prob,
self.solver,
[const.LpStatusOptimal],
{x: 4, y: -1, z: 6, w: 0},
)
if not os.path.exists(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
if not os.path.getsize(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
# Extract option value from the command line
command_line = PULP_CBC_CMDTest.read_command_line_from_log_file(logFilename)
option_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="cuts"
)
self.assertEqual("off", option_value)

def test_strong(self):
"""
Test if setting strong=10 adds strong 10 to the command line.
"""
name = self._testMethodName
prob = LpProblem(name, const.LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0)
w = LpVariable("w", 0)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7, "c3"
prob += w >= 0, "c4"
logFilename = name + ".log"
self.solver.optionsDict["logPath"] = logFilename
self.solver.optionsDict["strong"] = 10
pulpTestCheck(
prob,
self.solver,
[const.LpStatusOptimal],
{x: 4, y: -1, z: 6, w: 0},
)
if not os.path.exists(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
if not os.path.getsize(logFilename):
raise PulpError(f"Test failed for solver: {self.solver}")
# Extract option value from command line
command_line = PULP_CBC_CMDTest.read_command_line_from_log_file(logFilename)
option_value = PULP_CBC_CMDTest.extract_option_from_command_line(
command_line, option="strong", grp_pattern="\d+"
)
self.assertEqual("10", option_value)


class CPLEX_CMDTest(BaseSolverTest.PuLPTest):
solveInst = CPLEX_CMD
Expand Down

0 comments on commit de9b104

Please sign in to comment.