From 87d74221d84bc96da7d89bb3be14ac08a93f9b1a Mon Sep 17 00:00:00 2001 From: nariaki3551 Date: Mon, 22 Aug 2022 10:04:08 +0900 Subject: [PATCH 1/5] fix for #483 --- pulp/pulp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pulp/pulp.py b/pulp/pulp.py index 179ba3e0..cafa5956 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -2289,5 +2289,9 @@ def lpDot(v1, v2): return lpDot([v1] * len(v2), v2) elif not const.isiterable(v2): return lpDot(v1, [v2] * len(v1)) + elif isinstance(v1, LpAffineExpression): + return lpSum([v1[e1] * lpDot(e1, e2) for e1, e2 in zip(v1, v2)]) + elif isinstance(v2, LpAffineExpression): + return lpSum([v2[e2] * lpDot(e1, e2) for e1, e2 in zip(v1, v2)]) else: return lpSum([lpDot(e1, e2) for e1, e2 in zip(v1, v2)]) From a119bfb229ef4c320c43d66f09f2e6cd003b1fa7 Mon Sep 17 00:00:00 2001 From: nariaki3551 Date: Thu, 25 Aug 2022 09:56:20 +0900 Subject: [PATCH 2/5] add a unit test --- pulp/tests/test_pulp.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index d68aabda..be6c6d8d 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -6,7 +6,7 @@ from pulp.constants import PulpError from pulp.apis import * -from pulp import LpVariable, LpProblem, lpSum, LpConstraintVar, LpFractionConstraint +from pulp import LpVariable, LpProblem, lpSum, lpDot, LpConstraintVar, LpFractionConstraint from pulp import constants as const from pulp.tests.bin_packing_problem import create_bin_packing_problem from pulp.utilities import makeDict @@ -75,6 +75,18 @@ def test_pulp_001(self): print("\t Testing zero subtraction") assert str(c2) # will raise an exception + def test_pulp_002(self): + """ + Test the lpDot operation + """ + x = LpVariable("x") + y = LpVariable("y") + z = LpVariable("z") + a = [1, 2, 3] + assert dict(lpDot([x, y, z], a)) == {x: 1, y: 2, z: 3} + assert dict(lpDot([2*x, 2*y, 2*z], a)) == {x: 2, y: 4, z: 6} + assert dict(lpDot([x+y, y+z, z], a)) == {x: 1, y: 3, z: 5} + def test_pulp_009(self): # infeasible prob = LpProblem("test09", const.LpMinimize) From 0a4ad4e95ceb7741bca1d4d794b6d46dd004e838 Mon Sep 17 00:00:00 2001 From: nariaki3551 Date: Thu, 25 Aug 2022 10:07:37 +0900 Subject: [PATCH 3/5] update unit test --- pulp/tests/test_pulp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index be6c6d8d..67c55a84 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -86,6 +86,7 @@ def test_pulp_002(self): assert dict(lpDot([x, y, z], a)) == {x: 1, y: 2, z: 3} assert dict(lpDot([2*x, 2*y, 2*z], a)) == {x: 2, y: 4, z: 6} assert dict(lpDot([x+y, y+z, z], a)) == {x: 1, y: 3, z: 5} + assert dict(lpDot(a, [x+y, y+z, z])) == {x: 1, y: 3, z: 5} def test_pulp_009(self): # infeasible From 63cb5ecca8499342e7afdf6f55362a29c4d4ed58 Mon Sep 17 00:00:00 2001 From: nariaki3551 Date: Sat, 1 Apr 2023 13:26:24 +0900 Subject: [PATCH 4/5] add PSCIP_CMD --- pulp/apis/core.py | 6 ++ pulp/apis/scip_api.py | 164 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/pulp/apis/core.py b/pulp/apis/core.py index ad86ddb7..e8679c4b 100644 --- a/pulp/apis/core.py +++ b/pulp/apis/core.py @@ -129,6 +129,10 @@ def initialize(filename, operating_system="linux", arch="64"): fscip_path = config.get("locations", "FscipPath") except configparser.Error: fscip_path = "fscip" + try: + pscip_path = config.get("locations", "PscipPath") + except configparser.Error: + pscip_path = "parascip" for i, path in enumerate(coinMP_path): if not os.path.dirname(path): # if no pathname is supplied assume the file is in the same directory @@ -144,6 +148,7 @@ def initialize(filename, operating_system="linux", arch="64"): pulp_cbc_path, scip_path, fscip_path, + pscip_path, ) @@ -179,6 +184,7 @@ def initialize(filename, operating_system="linux", arch="64"): pulp_cbc_path, scip_path, fscip_path, + pscip_path, ) = initialize(config_filename, operating_system, arch) diff --git a/pulp/apis/scip_api.py b/pulp/apis/scip_api.py index bdbb1425..71335aa7 100644 --- a/pulp/apis/scip_api.py +++ b/pulp/apis/scip_api.py @@ -30,7 +30,7 @@ import warnings from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError -from .core import scip_path, fscip_path +from .core import scip_path, fscip_path, pscip_path from .. import constants from typing import Dict, List, Optional, Tuple @@ -454,6 +454,168 @@ def readsol(filename): FSCIP = FSCIP_CMD +class PSCIP_CMD(FSCIP_CMD): + """The multi-process ParaSCIP version of the SCIP optimization solver""" + + name = "PSCIP_CMD" + + def __init__( + self, + path=None, + mip=True, + keepFiles=False, + msg=True, + options=None, + timeLimit=None, + gapRel=None, + gapAbs=None, + maxNodes=None, + processes=None, + hostfile=None, + logPath=None, + ): + """ + :param bool msg: if False, no log is shown + :param bool mip: if False, assume LP even if integer variables + :param list options: list of additional options to pass to solver + :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 float timeLimit: maximum time for solver (in seconds) + :param float gapRel: relative gap tolerance for the solver to stop (in fraction) + :param float gapAbs: absolute gap tolerance for the solver to stop + :param int maxNodes: max number of nodes during branching. Stops the solving when reached. + :param int processes: sets the maximum number of processes + :param str logPath: path to the log file + """ + FSCIP_CMD.__init__( + self, + mip=mip, + msg=msg, + options=options, + path=path, + keepFiles=keepFiles, + timeLimit=timeLimit, + gapRel=gapRel, + gapAbs=gapAbs, + maxNodes=maxNodes, + logPath=logPath, + ) + if processes is not None: + self.optionsDict["processes"] = processes + if hostfile is not None: + self.optionsDict["hostfile"] = hostfile + + PSCIP_STATUSES = { + "No Solution": constants.LpStatusNotSolved, + "Final Solution": constants.LpStatusOptimal, + } + NO_SOLUTION_STATUSES = { + constants.LpStatusInfeasible, + constants.LpStatusUnbounded, + constants.LpStatusNotSolved, + } + + def defaultPath(self): + return self.executableExtension(pscip_path) + + def actualSolve(self, lp): + """Solve a well formulated lp problem""" + if not self.executable(self.path): + raise PulpSolverError("PuLP: cannot execute " + self.path) + + tmpLp, tmpSol, tmpOptions, tmpParams = self.create_tmp_files( + lp.name, "lp", "sol", "set", "prm" + ) + lp.writeLP(tmpLp) + + file_options: List[str] = [] + if self.timeLimit is not None: + file_options.append(f"limits/time={self.timeLimit}") + if "gapRel" in self.optionsDict: + file_options.append(f"limits/gap={self.optionsDict['gapRel']}") + if "gapAbs" in self.optionsDict: + file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}") + if "maxNodes" in self.optionsDict: + file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}") + if not self.mip: + warnings.warn(f"{self.name} does not allow a problem to be relaxed") + + file_parameters: List[str] = [] + # disable presolving in the LoadCoordinator to make sure a solution file is always written + file_parameters.append("NoPreprocessingInLC = TRUE") + + command: List[str] = [] + command.append("mpirun") + if "processes" in self.optionsDict: + command.extend(["-np", f"{self.optionsDict['processes']}"]) + if "hostfile" in self.optionsDict: + command.extend(["--hostfile", f"{self.optionsDict['hostfile']}"]) + command.append(self.path) + command.append(tmpParams) + command.append(tmpLp) + command.extend(["-s", tmpOptions]) + command.extend(["-fsol", tmpSol]) + if not self.msg: + command.append("-q") + if "logPath" in self.optionsDict: + command.extend(["-l", self.optionsDict["logPath"]]) + + options = iter(self.options) + for option in options: + # identify cli options by a leading dash (-) and treat other options as file options + if option.starts_with("-"): + # assumption: all cli options require an argument which is provided as a separate parameter + argument = next(options) + command.extend([option, argument]) + else: + # assumption: all file options contain a slash (/) + is_file_options = "/" in option + + # assumption: all file options and parameters require an argument which is provided after the equal sign (=) + if "=" not in option: + argument = next(options) + option += f"={argument}" + + if is_file_options: + file_options.append(option) + else: + file_parameters.append(option) + + # wipe the solution file since FSCIP does not overwrite it if no solution was found which causes parsing errors + self.silent_remove(tmpSol) + with open(tmpOptions, "w") as options_file: + options_file.write("\n".join(file_options)) + with open(tmpParams, "w") as parameters_file: + parameters_file.write("\n".join(file_parameters)) + try: + subprocess.check_call( + command, + stdout=sys.stdout if self.msg else subprocess.DEVNULL, + stderr=sys.stderr if self.msg else subprocess.DEVNULL, + ) + except subprocess.CalledProcessError as e: + print(f"CalledProcessError: {e}") + except Exception as e: + print(f"Error: {e}") + raise e + + if not os.path.exists(tmpSol): + raise PulpSolverError("PuLP: Error while executing " + self.path) + status, values = self.readsol(tmpSol) + # Make sure to add back in any 0-valued variables SCIP leaves out. + finalVals = {} + for v in lp.variables(): + finalVals[v.name] = values.get(v.name, 0.0) + + lp.assignVarsVals(finalVals) + lp.assignStatus(status) + self.delete_tmp_files(tmpLp, tmpSol, tmpOptions, tmpParams) + return status + + +PSCIP = PSCIP_CMD + + class SCIP_PY(LpSolver): """ The SCIP Optimization Suite (via its python interface) From e3c029ae1b414333990ed12975b3727dad152ac8 Mon Sep 17 00:00:00 2001 From: nariaki3551 Date: Wed, 22 Nov 2023 12:31:29 +0900 Subject: [PATCH 5/5] Revert "add PSCIP_CMD" This reverts commit 63cb5ecca8499342e7afdf6f55362a29c4d4ed58. --- pulp/apis/core.py | 6 -- pulp/apis/scip_api.py | 164 +----------------------------------------- 2 files changed, 1 insertion(+), 169 deletions(-) diff --git a/pulp/apis/core.py b/pulp/apis/core.py index e8679c4b..ad86ddb7 100644 --- a/pulp/apis/core.py +++ b/pulp/apis/core.py @@ -129,10 +129,6 @@ def initialize(filename, operating_system="linux", arch="64"): fscip_path = config.get("locations", "FscipPath") except configparser.Error: fscip_path = "fscip" - try: - pscip_path = config.get("locations", "PscipPath") - except configparser.Error: - pscip_path = "parascip" for i, path in enumerate(coinMP_path): if not os.path.dirname(path): # if no pathname is supplied assume the file is in the same directory @@ -148,7 +144,6 @@ def initialize(filename, operating_system="linux", arch="64"): pulp_cbc_path, scip_path, fscip_path, - pscip_path, ) @@ -184,7 +179,6 @@ def initialize(filename, operating_system="linux", arch="64"): pulp_cbc_path, scip_path, fscip_path, - pscip_path, ) = initialize(config_filename, operating_system, arch) diff --git a/pulp/apis/scip_api.py b/pulp/apis/scip_api.py index 71335aa7..bdbb1425 100644 --- a/pulp/apis/scip_api.py +++ b/pulp/apis/scip_api.py @@ -30,7 +30,7 @@ import warnings from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError -from .core import scip_path, fscip_path, pscip_path +from .core import scip_path, fscip_path from .. import constants from typing import Dict, List, Optional, Tuple @@ -454,168 +454,6 @@ def readsol(filename): FSCIP = FSCIP_CMD -class PSCIP_CMD(FSCIP_CMD): - """The multi-process ParaSCIP version of the SCIP optimization solver""" - - name = "PSCIP_CMD" - - def __init__( - self, - path=None, - mip=True, - keepFiles=False, - msg=True, - options=None, - timeLimit=None, - gapRel=None, - gapAbs=None, - maxNodes=None, - processes=None, - hostfile=None, - logPath=None, - ): - """ - :param bool msg: if False, no log is shown - :param bool mip: if False, assume LP even if integer variables - :param list options: list of additional options to pass to solver - :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 float timeLimit: maximum time for solver (in seconds) - :param float gapRel: relative gap tolerance for the solver to stop (in fraction) - :param float gapAbs: absolute gap tolerance for the solver to stop - :param int maxNodes: max number of nodes during branching. Stops the solving when reached. - :param int processes: sets the maximum number of processes - :param str logPath: path to the log file - """ - FSCIP_CMD.__init__( - self, - mip=mip, - msg=msg, - options=options, - path=path, - keepFiles=keepFiles, - timeLimit=timeLimit, - gapRel=gapRel, - gapAbs=gapAbs, - maxNodes=maxNodes, - logPath=logPath, - ) - if processes is not None: - self.optionsDict["processes"] = processes - if hostfile is not None: - self.optionsDict["hostfile"] = hostfile - - PSCIP_STATUSES = { - "No Solution": constants.LpStatusNotSolved, - "Final Solution": constants.LpStatusOptimal, - } - NO_SOLUTION_STATUSES = { - constants.LpStatusInfeasible, - constants.LpStatusUnbounded, - constants.LpStatusNotSolved, - } - - def defaultPath(self): - return self.executableExtension(pscip_path) - - def actualSolve(self, lp): - """Solve a well formulated lp problem""" - if not self.executable(self.path): - raise PulpSolverError("PuLP: cannot execute " + self.path) - - tmpLp, tmpSol, tmpOptions, tmpParams = self.create_tmp_files( - lp.name, "lp", "sol", "set", "prm" - ) - lp.writeLP(tmpLp) - - file_options: List[str] = [] - if self.timeLimit is not None: - file_options.append(f"limits/time={self.timeLimit}") - if "gapRel" in self.optionsDict: - file_options.append(f"limits/gap={self.optionsDict['gapRel']}") - if "gapAbs" in self.optionsDict: - file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}") - if "maxNodes" in self.optionsDict: - file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}") - if not self.mip: - warnings.warn(f"{self.name} does not allow a problem to be relaxed") - - file_parameters: List[str] = [] - # disable presolving in the LoadCoordinator to make sure a solution file is always written - file_parameters.append("NoPreprocessingInLC = TRUE") - - command: List[str] = [] - command.append("mpirun") - if "processes" in self.optionsDict: - command.extend(["-np", f"{self.optionsDict['processes']}"]) - if "hostfile" in self.optionsDict: - command.extend(["--hostfile", f"{self.optionsDict['hostfile']}"]) - command.append(self.path) - command.append(tmpParams) - command.append(tmpLp) - command.extend(["-s", tmpOptions]) - command.extend(["-fsol", tmpSol]) - if not self.msg: - command.append("-q") - if "logPath" in self.optionsDict: - command.extend(["-l", self.optionsDict["logPath"]]) - - options = iter(self.options) - for option in options: - # identify cli options by a leading dash (-) and treat other options as file options - if option.starts_with("-"): - # assumption: all cli options require an argument which is provided as a separate parameter - argument = next(options) - command.extend([option, argument]) - else: - # assumption: all file options contain a slash (/) - is_file_options = "/" in option - - # assumption: all file options and parameters require an argument which is provided after the equal sign (=) - if "=" not in option: - argument = next(options) - option += f"={argument}" - - if is_file_options: - file_options.append(option) - else: - file_parameters.append(option) - - # wipe the solution file since FSCIP does not overwrite it if no solution was found which causes parsing errors - self.silent_remove(tmpSol) - with open(tmpOptions, "w") as options_file: - options_file.write("\n".join(file_options)) - with open(tmpParams, "w") as parameters_file: - parameters_file.write("\n".join(file_parameters)) - try: - subprocess.check_call( - command, - stdout=sys.stdout if self.msg else subprocess.DEVNULL, - stderr=sys.stderr if self.msg else subprocess.DEVNULL, - ) - except subprocess.CalledProcessError as e: - print(f"CalledProcessError: {e}") - except Exception as e: - print(f"Error: {e}") - raise e - - if not os.path.exists(tmpSol): - raise PulpSolverError("PuLP: Error while executing " + self.path) - status, values = self.readsol(tmpSol) - # Make sure to add back in any 0-valued variables SCIP leaves out. - finalVals = {} - for v in lp.variables(): - finalVals[v.name] = values.get(v.name, 0.0) - - lp.assignVarsVals(finalVals) - lp.assignStatus(status) - self.delete_tmp_files(tmpLp, tmpSol, tmpOptions, tmpParams) - return status - - -PSCIP = PSCIP_CMD - - class SCIP_PY(LpSolver): """ The SCIP Optimization Suite (via its python interface)