diff --git a/pulp/apis/coin_api.py b/pulp/apis/coin_api.py index 62191f9d..e0459fd5 100644 --- a/pulp/apis/coin_api.py +++ b/pulp/apis/coin_api.py @@ -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. """ @@ -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 + " " @@ -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 {}", ) diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index d50de9e1..ffc05eee 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -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 @@ -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