From 74ff77540440e09f2efb3fe1e8752d9ba78e9cfe Mon Sep 17 00:00:00 2001 From: Robert Howlett Date: Mon, 19 May 2025 21:18:19 +0100 Subject: [PATCH 1/5] Fix 811 --- doc/source/develop/add_solver.rst | 12 ++++++------ pulp/apis/choco_api.py | 10 +++++----- pulp/apis/gurobi_api.py | 12 ++++++------ pulp/apis/mipcl_api.py | 12 ++++++------ 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/source/develop/add_solver.rst b/doc/source/develop/add_solver.rst index 6beb4b9d..333527d1 100644 --- a/doc/source/develop/add_solver.rst +++ b/doc/source/develop/add_solver.rst @@ -92,13 +92,13 @@ Takes an :py:class:`pulp.pulp.LpProblem` as argument, solves it, stores the solu os.remove(tmpSol) except: pass - cmd = self.path - cmd += " %s" % tmpMps - cmd += " -solfile %s" % tmpSol + cmd = [self.path] + cmd.append(tmpMps) + cmd.extend(["-solfile", tmpSol]) if self.timeLimit is not None: - cmd += " -time %s" % self.timeLimit + cmd.extend(["-time", f"{self.timeLimit}"]) for option in self.options: - cmd += " " + option + cmd.append(option) if lp.isMIP(): if not self.mip: warnings.warn("MIPCL_CMD cannot solve the relaxation of a problem") @@ -107,7 +107,7 @@ Takes an :py:class:`pulp.pulp.LpProblem` as argument, solves it, stores the solu else: pipe = open(os.devnull, "w") - return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe) + return_code = subprocess.call(cmd, stdout=pipe, stderr=pipe) # We need to undo the objective swap before finishing if lp.sense == constants.LpMaximize: lp += -lp.objective diff --git a/pulp/apis/choco_api.py b/pulp/apis/choco_api.py index 5d37a026..743fed3d 100644 --- a/pulp/apis/choco_api.py +++ b/pulp/apis/choco_api.py @@ -89,13 +89,13 @@ def actualSolve(self, lp): os.remove(tmpSol) except: pass - cmd = java_path + ' -cp "' + self.path + '" org.chocosolver.parser.mps.ChocoMPS' + cmd = [java_path, '-cp', self.path, 'org.chocosolver.parser.mps.ChocoMPS'] if self.timeLimit is not None: - cmd += f" -limit [-{self.timeLimit}s]" - cmd += " " + " ".join([f"{key} {value}" for key, value in self.options]) - cmd += f" {tmpMps}" + cmd.extend(["-limit", f"[-{self.timeLimit}s]"]) + cmd.extend([key_or_value for key_value in self.options for key_or_value in key_value]) + cmd.append(tmpMps) if lp.sense == constants.LpMaximize: - cmd += " -max" + cmd.append("-max") if lp.isMIP(): if not self.mip: warnings.warn("CHOCO_CMD cannot solve the relaxation of a problem") diff --git a/pulp/apis/gurobi_api.py b/pulp/apis/gurobi_api.py index 517404f6..89e20c41 100644 --- a/pulp/apis/gurobi_api.py +++ b/pulp/apis/gurobi_api.py @@ -460,23 +460,23 @@ def actualSolve(self, lp): os.remove(tmpSol) except: pass - cmd = self.path + cmd = [self.path] options = self.options + self.getOptions() if self.timeLimit is not None: options.append(("TimeLimit", self.timeLimit)) - cmd += " " + " ".join([f"{key}={value}" for key, value in options]) - cmd += f" ResultFile={tmpSol}" + cmd.extend([f"{key}={value}" for key, value in options]) + cmd.append(f"ResultFile={tmpSol}") if self.optionsDict.get("warmStart", False): self.writesol(filename=tmpMst, vs=vs) - cmd += f" InputFile={tmpMst}" + cmd.append(f"InputFile={tmpMst}") if lp.isMIP(): if not self.mip: warnings.warn("GUROBI_CMD does not allow a problem to be relaxed") - cmd += f" {tmpLp}" + cmd.append(tmpLp) pipe = self.get_pipe() - return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe) + return_code = subprocess.call(cmd, stdout=pipe, stderr=pipe) # Close the pipe now if we used it. if pipe is not None: diff --git a/pulp/apis/mipcl_api.py b/pulp/apis/mipcl_api.py index d1108062..7fe9d5ff 100644 --- a/pulp/apis/mipcl_api.py +++ b/pulp/apis/mipcl_api.py @@ -92,13 +92,13 @@ def actualSolve(self, lp): os.remove(tmpSol) except: pass - cmd = self.path - cmd += f" {tmpMps}" - cmd += f" -solfile {tmpSol}" + cmd = [self.path] + cmd.append(tmpMps) + cmd.extend(["-solfile", tmpSol]) if self.timeLimit is not None: - cmd += f" -time {self.timeLimit}" + cmd.extend(["-time", f"{self.timeLimit}"]) for option in self.options: - cmd += " " + option + cmd.append(option) if lp.isMIP(): if not self.mip: warnings.warn("MIPCL_CMD cannot solve the relaxation of a problem") @@ -107,7 +107,7 @@ def actualSolve(self, lp): else: pipe = open(os.devnull, "w") - return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe) + return_code = subprocess.call(cmd, stdout=pipe, stderr=pipe) # We need to undo the objective swap before finishing if lp.sense == constants.LpMaximize: lp += -lp.objective From d5ef83db9a9117f0efc4d7dbab29a62e50d2dee3 Mon Sep 17 00:00:00 2001 From: Robert Howlett Date: Mon, 19 May 2025 22:13:59 +0100 Subject: [PATCH 2/5] micro-optimisation --- pulp/apis/gurobi_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulp/apis/gurobi_api.py b/pulp/apis/gurobi_api.py index 89e20c41..f8b0ad57 100644 --- a/pulp/apis/gurobi_api.py +++ b/pulp/apis/gurobi_api.py @@ -464,7 +464,7 @@ def actualSolve(self, lp): options = self.options + self.getOptions() if self.timeLimit is not None: options.append(("TimeLimit", self.timeLimit)) - cmd.extend([f"{key}={value}" for key, value in options]) + cmd.extend(f"{key}={value}" for key, value in options) cmd.append(f"ResultFile={tmpSol}") if self.optionsDict.get("warmStart", False): self.writesol(filename=tmpMst, vs=vs) From ecdb56d9c3f58babecc4ee61868190d6d69b2f66 Mon Sep 17 00:00:00 2001 From: Robert Howlett Date: Tue, 20 May 2025 09:05:33 +0100 Subject: [PATCH 3/5] Format --- pulp/apis/choco_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pulp/apis/choco_api.py b/pulp/apis/choco_api.py index 743fed3d..ddb502e2 100644 --- a/pulp/apis/choco_api.py +++ b/pulp/apis/choco_api.py @@ -89,10 +89,12 @@ def actualSolve(self, lp): os.remove(tmpSol) except: pass - cmd = [java_path, '-cp', self.path, 'org.chocosolver.parser.mps.ChocoMPS'] + cmd = [java_path, "-cp", self.path, "org.chocosolver.parser.mps.ChocoMPS"] if self.timeLimit is not None: - cmd.extend(["-limit", f"[-{self.timeLimit}s]"]) - cmd.extend([key_or_value for key_value in self.options for key_or_value in key_value]) + cmd.extend(["-limit", f"[-{self.timeLimit}s]"]) + cmd.extend( + [key_or_value for key_value in self.options for key_or_value in key_value] + ) cmd.append(tmpMps) if lp.sense == constants.LpMaximize: cmd.append("-max") From df16031a28780fd1533b15153836ccfe45538d8c Mon Sep 17 00:00:00 2001 From: Robert Howlett Date: Wed, 28 May 2025 18:06:09 +0100 Subject: [PATCH 4/5] Add test --- pulp/tests/test_pulp.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index 8c7ac3d1..fb3d7040 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -175,6 +175,24 @@ def test_continuous(self): prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) + def test_continuous_with_spaces_in_path(self): + with tempfile.TemporaryDirectory() as tmpdir: + prob = LpProblem(self._testMethodName, const.LpMinimize) + prob.tmpDir = tmpdir + "/folder with spaces" + 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" + + pulpTestCheck( + prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} + ) + def test_non_intermediate_var(self): prob = LpProblem(self._testMethodName, const.LpMinimize) x_vars = { From 2e2a29beb434d4293ec5529eabe044b1187db6d2 Mon Sep 17 00:00:00 2001 From: Robert Howlett Date: Wed, 28 May 2025 22:40:47 +0100 Subject: [PATCH 5/5] Format --- pulp/tests/test_pulp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index fb3d7040..8ee0cf1d 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -190,7 +190,10 @@ def test_continuous_with_spaces_in_path(self): prob += w >= 0, "c4" pulpTestCheck( - prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} + prob, + self.solver, + [const.LpStatusOptimal], + {x: 4, y: -1, z: 6, w: 0}, ) def test_non_intermediate_var(self):