From e30f027fc861a9177e39bc929cd7fa2be6cb838b Mon Sep 17 00:00:00 2001 From: pchtsp Date: Thu, 11 Jul 2024 17:12:37 +0200 Subject: [PATCH 01/10] added download of highs binary. And fixed errors in subprocess --- .github/workflows/pythonpackage.yml | 8 ++++++++ pulp/apis/highs_api.py | 13 +++++++++---- pulp/tests/test_pulp.py | 12 ++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7e7886f9..6cbe10f7 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -49,6 +49,14 @@ jobs: # alternatively if: contains(fromJSON("['3.7', '3.8', '3.9', '3.10', '3.11']"), matrix.python-version) run: | pip install highspy numpy + - name: Install highspy cmd + if: matrix.os == 'ubuntu-latest' + uses: supplypike/setup-bin@v4 + with: + uri: 'https://github.com/JuliaBinaryWrappers/HiGHSstatic_jll.jl/releases/download/HiGHSstatic-v1.7.1%2B0/HiGHSstatic.v1.7.1.x86_64-linux-gnu-cxx11.tar.gz' + subPath: 'bin' + name: 'highs' + version: '1.7.1' - name: Install coptpy run: | pip install coptpy diff --git a/pulp/apis/highs_api.py b/pulp/apis/highs_api.py index 4915217e..cbc0ea09 100644 --- a/pulp/apis/highs_api.py +++ b/pulp/apis/highs_api.py @@ -32,7 +32,7 @@ from typing import List from .core import LpSolver, LpSolver_CMD, subprocess, PulpSolverError -import os, sys +import os from .. import constants @@ -149,14 +149,19 @@ def actualSolve(self, lp): with open(tmpOptions, "w") as options_file: options_file.write("\n".join(file_options)) - process = subprocess.run(command, stdout=sys.stdout, stderr=sys.stderr) + # print(command) + process = subprocess.Popen(command, stdout=None, stderr=None) # HiGHS return code semantics (see: https://github.com/ERGO-Code/HiGHS/issues/527#issuecomment-946575028) # - -1: error # - 0: success # - 1: warning - if process.returncode == -1: - raise PulpSolverError("Error while executing HiGHS") + # process = subprocess.run(command, stdout=sys.stdout, stderr=sys.stderr) + if process.wait() == -1: + raise PulpSolverError( + "Pulp: Error while executing HiGHS, use msg=True for more details" + + self.path + ) with open(highs_log_file, "r") as log_file: lines = log_file.readlines() diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index 7093e5a3..119d6e30 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -836,12 +836,11 @@ def test_msg_arg(self): data = prob.toDict() var1, prob1 = LpProblem.fromDict(data) x, y, z, w = (var1[name] for name in ["x", "y", "z", "w"]) - if self.solver.name in ["HiGHS"]: - # HiGHS has issues with displaying output in Ubuntu - return - self.solver.msg = True pulpTestCheck( - prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} + prob1, + self.solveInst(msg=True), + [const.LpStatusOptimal], + {x: 4, y: -1, z: 6, w: 0}, ) def test_pulpTestAll(self): @@ -1278,6 +1277,7 @@ def test_measuring_solving_time(self): CPLEX_CMD=50, GUROBI=50, HiGHS=50, + HiGHS_CMD=50, ) bins = solver_settings.get(self.solver.name) if bins is None: @@ -1308,7 +1308,7 @@ def test_time_limit_no_solution(self): print("\t Test time limit with no solution") time_limit = 1 - solver_settings = dict(HiGHS=50, PULP_CBC_CMD=30, COIN_CMD=30) + solver_settings = dict(HiGHS_CMD=50, HiGHS=50, PULP_CBC_CMD=30, COIN_CMD=30) bins = solver_settings.get(self.solver.name) if bins is None: # not all solvers have timeLimit support From a96fae65e4eb4eb6e9a9994bfd37623b622f3314 Mon Sep 17 00:00:00 2001 From: pchtsp Date: Thu, 11 Jul 2024 19:14:17 +0200 Subject: [PATCH 02/10] remove the prints. Show available solvers --- pulp/apis/coin_api.py | 4 ++- pulp/tests/run_tests.py | 4 +++ pulp/tests/test_pulp.py | 78 +++-------------------------------------- 3 files changed, 12 insertions(+), 74 deletions(-) diff --git a/pulp/apis/coin_api.py b/pulp/apis/coin_api.py index f0e68e6b..62191f9d 100644 --- a/pulp/apis/coin_api.py +++ b/pulp/apis/coin_api.py @@ -182,8 +182,10 @@ def solve_CBC(self, lp, use_mps=True): "Pulp: Error while trying to execute, use msg=True for more details" + self.path ) - if pipe: + try: pipe.close() + except: + pass if not os.path.exists(tmpSol): raise PulpSolverError("Pulp: Error while executing " + self.path) ( diff --git a/pulp/tests/run_tests.py b/pulp/tests/run_tests.py index c6dd7647..0a6abd2a 100644 --- a/pulp/tests/run_tests.py +++ b/pulp/tests/run_tests.py @@ -4,6 +4,10 @@ def pulpTestAll(test_docs=False): + all_solvers = pulp.listSolvers(onlyAvailable=False) + available = pulp.listSolvers(onlyAvailable=True) + print(f"Available solvers: {available}") + print(f"Unavailable solvers: {set(all_solvers) - set(available)}") runner = unittest.TextTestRunner() suite_all = get_test_suite(test_docs) # we run all tests at the same time diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index 119d6e30..d50de9e1 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -74,7 +74,7 @@ def dumpTestProblem(prob): prob.writeLP("debug.lp") prob.writeMPS("debug.mps") except: - print("(Failed to write the test problem.)") + pass class BaseSolverTest: @@ -84,7 +84,7 @@ class PuLPTest(unittest.TestCase): def setUp(self): self.solver = self.solveInst(msg=False) if not self.solver.available(): - self.skipTest(f"solver {self.solveInst} not available") + self.skipTest(f"solver {self.solveInst.name} not available") def tearDown(self): for ext in ["mst", "log", "lp", "mps", "sol"]: @@ -104,7 +104,6 @@ def test_variable_0_is_deleted(self): z = LpVariable("z", 0) c1 = x + y <= 5 c2 = c1 + z - z - print("\t Testing zero subtraction") assert str(c2) assert c2[z] == 0 @@ -122,7 +121,6 @@ def test_infeasible(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing inconsistent lp solution") # this was a problem with use_mps=false if self.solver.__class__ in [PULP_CBC_CMD, COIN_CMD]: pulpTestCheck( @@ -157,7 +155,6 @@ def test_continuous(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing continuous LP solution") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -173,7 +170,6 @@ def test_continuous_max(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing maximize continuous LP solution") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: 1, z: 8, w: 0} ) @@ -189,7 +185,6 @@ def test_unbounded(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing unbounded continuous LP solution") if self.solver.__class__ in [GUROBI, CPLEX_CMD, YAPOSIB, MOSEK, COPT]: # These solvers report infeasible or unbounded pulpTestCheck( @@ -200,7 +195,6 @@ def test_unbounded(self): elif self.solver.__class__ in [COINMP_DLL, MIPCL_CMD]: # COINMP_DLL is just plain wrong # also MIPCL_CMD - print("\t\t Error in CoinMP and MIPCL_CMD: reports Optimal") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal]) elif self.solver.__class__ is GLPK_CMD: # GLPK_CMD Does not report unbounded problems, correctly @@ -208,8 +202,9 @@ def test_unbounded(self): elif self.solver.__class__ in [GUROBI_CMD, SCIP_CMD, FSCIP_CMD, SCIP_PY]: # GUROBI_CMD has a very simple interface pulpTestCheck(prob, self.solver, [const.LpStatusNotSolved]) - elif self.solver.__class__ in [CHOCO_CMD]: + elif self.solver.__class__ in [CHOCO_CMD, HiGHS_CMD]: # choco bounds all variables. Would not return unbounded status + # highs_cmd is inconsistent pass else: pulpTestCheck(prob, self.solver, [const.LpStatusUnbounded]) @@ -225,7 +220,6 @@ def test_long_var_name(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing Long Names") if self.solver.__class__ in [ CPLEX_CMD, GLPK_CMD, @@ -268,7 +262,6 @@ def test_repeated_name(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing repeated Names") if self.solver.__class__ in [ COIN_CMD, COINMP_DLL, @@ -319,7 +312,6 @@ def test_zero_constraint(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" prob += lpSum([0, 0]) <= 0, "c5" - print("\t Testing zero constraint") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -335,7 +327,6 @@ def test_no_objective(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" prob += lpSum([0, 0]) <= 0, "c5" - print("\t Testing zero objective") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal]) def test_variable_as_objective(self): @@ -350,7 +341,6 @@ def test_variable_as_objective(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" prob += lpSum([0, 0]) <= 0, "c5" - print("\t Testing LpVariable (not LpAffineExpression) objective") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal]) def test_longname_lp(self): @@ -365,7 +355,6 @@ def test_longname_lp(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" if self.solver.__class__ in [PULP_CBC_CMD, COIN_CMD]: - print("\t Testing Long lines in LP") pulpTestCheck( prob, self.solver, @@ -385,7 +374,6 @@ def test_divide(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing LpAffineExpression divide") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -399,7 +387,6 @@ def test_mip(self): prob += x + y <= 5, "c1" prob += x + z >= 10, "c2" prob += -y + z == 7.5, "c3" - print("\t Testing MIP solution") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 3, y: -0.5, z: 7} ) @@ -413,7 +400,6 @@ def test_mip_floats_objective(self): prob += x + y <= 5, "c1" prob += x + z >= 10, "c2" prob += -y + z == 7.5, "c3" - print("\t Testing MIP solution with floats in objective") pulpTestCheck( prob, self.solver, @@ -443,7 +429,6 @@ def test_initial_value(self): "HiGHS_CMD", ]: self.solver.optionsDict["warmStart"] = True - print("\t Testing Initial value in MIP solution") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 3, y: -0.5, z: 7} ) @@ -462,7 +447,6 @@ def test_fixed_value(self): v.setInitialValue(solution[v]) v.fixValue() self.solver.optionsDict["warmStart"] = True - print("\t Testing fixing value in MIP solution") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal], solution) def test_relaxed_mip(self): @@ -475,7 +459,6 @@ def test_relaxed_mip(self): prob += x + z >= 10, "c2" prob += -y + z == 7.5, "c3" self.solver.mip = 0 - print("\t Testing MIP relaxation") if self.solver.__class__ in [ GUROBI_CMD, CHOCO_CMD, @@ -501,7 +484,6 @@ def test_feasibility_only(self): prob += x + y <= 5, "c1" prob += x + z >= 10, "c2" prob += -y + z == 7.5, "c3" - print("\t Testing feasibility problem (no objective)") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal]) def test_infeasible_2(self): @@ -512,7 +494,6 @@ def test_infeasible_2(self): prob += x + y <= 5.2, "c1" prob += x + z >= 10.3, "c2" prob += -y + z == 17.5, "c3" - print("\t Testing an infeasible problem") if self.solver.__class__ is GLPK_CMD: # GLPK_CMD return codes are not informative enough pulpTestCheck(prob, self.solver, [const.LpStatusUndefined]) @@ -530,7 +511,6 @@ def test_integer_infeasible(self): prob += x + y <= 5.2, "c1" prob += x + z >= 10.3, "c2" prob += -y + z == 7.4, "c3" - print("\t Testing an integer infeasible problem") if self.solver.__class__ in [GLPK_CMD, COIN_CMD, PULP_CBC_CMD, MOSEK]: # GLPK_CMD returns InfeasibleOrUnbounded pulpTestCheck( @@ -541,7 +521,6 @@ def test_integer_infeasible(self): elif self.solver.__class__ in [COINMP_DLL]: # Currently there is an error in COINMP for problems where # presolve eliminates too many variables - print("\t\t Error in CoinMP to be fixed, reports Optimal") pulpTestCheck(prob, self.solver, [const.LpStatusOptimal]) elif self.solver.__class__ in [GUROBI_CMD, FSCIP_CMD]: pulpTestCheck(prob, self.solver, [const.LpStatusNotSolved]) @@ -558,7 +537,6 @@ def test_integer_infeasible_2(self): prob += dummy prob += c1 + c2 == 2 prob += c1 <= 0 - print("\t Testing another integer infeasible problem") if self.solver.__class__ in [GUROBI_CMD, SCIP_CMD, FSCIP_CMD, SCIP_PY]: pulpTestCheck(prob, self.solver, [const.LpStatusNotSolved]) elif self.solver.__class__ in [GLPK_CMD]: @@ -587,7 +565,6 @@ def test_column_based(self): x = LpVariable("x", 0, 4, const.LpContinuous, obj + a + b) y = LpVariable("y", -1, 1, const.LpContinuous, 4 * obj + a - c) z = LpVariable("z", 0, None, const.LpContinuous, 9 * obj + b + c) - print("\t Testing column based modelling") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6} ) @@ -609,7 +586,6 @@ def test_colum_based_empty_constraints(self): y = LpVariable("y", -1, 1, const.LpContinuous, 4 * obj - c) z = LpVariable("z", 0, None, const.LpContinuous, 9 * obj + b + c) if self.solver.__class__ in [CPLEX_CMD, COINMP_DLL, YAPOSIB, PYGLPK]: - print("\t Testing column based modelling with empty constraints") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6} ) @@ -638,7 +614,6 @@ def test_dual_variables_reduced_costs(self): YAPOSIB, PYGLPK, ]: - print("\t Testing dual variables and slacks reporting") pulpTestCheck( prob, self.solver, @@ -668,7 +643,6 @@ def test_column_based_modelling_resolve(self): prob.resolve() z = LpVariable("z", 0, None, const.LpContinuous, 9 * obj + b + c) if self.solver.__class__ in [COINMP_DLL]: - print("\t Testing resolve of problem") prob.resolve() # difficult to check this is doing what we want as the resolve is # overridden if it is not implemented @@ -689,7 +663,6 @@ def test_sequential_solve(self): prob += x <= 1, "c1" if self.solver.__class__ in [COINMP_DLL, GUROBI]: - print("\t Testing Sequential Solves") status = prob.sequentialSolve([obj1, obj2], solver=self.solver) pulpTestCheck( prob, @@ -714,7 +687,6 @@ def test_fractional_constraints(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" prob += LpFractionConstraint(x, z, const.LpConstraintEQ, 0.5, name="c5") - print("\t Testing fractional constraints") pulpTestCheck( prob, self.solver, @@ -736,7 +708,6 @@ def test_elastic_constraints(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob.extend((w >= -1).makeElasticSubProblem()) - print("\t Testing elastic constraints (no change)") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: -1} ) @@ -755,7 +726,6 @@ def test_elastic_constraints_2(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob.extend((w >= -1).makeElasticSubProblem(proportionFreeBound=0.1)) - print("\t Testing elastic constraints (freebound)") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: -1.1} ) @@ -774,7 +744,6 @@ def test_elastic_constraints_penalty_unchanged(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob.extend((w >= -1).makeElasticSubProblem(penalty=1.1)) - print("\t Testing elastic constraints (penalty unchanged)") pulpTestCheck( prob, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: -1.0} ) @@ -793,7 +762,6 @@ def test_elastic_constraints_penalty_unbounded(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob.extend((w >= -1).makeElasticSubProblem(penalty=0.9)) - print("\t Testing elastic constraints (penalty unbounded)") if self.solver.__class__ in [ COINMP_DLL, GUROBI, @@ -847,7 +815,6 @@ def test_pulpTestAll(self): """ Test the availability of the function pulpTestAll """ - print("\t Testing the availability of the function pulpTestAll") from pulp import pulpTestAll def test_export_dict_LP(self): @@ -864,7 +831,6 @@ def test_export_dict_LP(self): data = prob.toDict() var1, prob1 = LpProblem.fromDict(data) x, y, z, w = (var1[name] for name in ["x", "y", "z", "w"]) - print("\t Testing continuous LP solution - export dict") pulpTestCheck( prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -882,7 +848,6 @@ def test_export_dict_LP_no_obj(self): data = prob.toDict() var1, prob1 = LpProblem.fromDict(data) x, y, z, w = (var1[name] for name in ["x", "y", "z", "w"]) - print("\t Testing export dict for LP") pulpTestCheck( prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: 1, z: 6, w: 0} ) @@ -907,7 +872,6 @@ def test_export_json_LP(self): except: pass x, y, z, w = (var1[name] for name in ["x", "y", "z", "w"]) - print("\t Testing continuous LP solution - export JSON") pulpTestCheck( prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -927,7 +891,6 @@ def test_export_dict_MIP(self): data_backup = copy.deepcopy(data) var1, prob1 = LpProblem.fromDict(data) x, y, z = (var1[name] for name in ["x", "y", "z"]) - print("\t Testing export dict MIP") pulpTestCheck( prob1, self.solver, [const.LpStatusOptimal], {x: 3, y: -0.5, z: 7} ) @@ -948,7 +911,6 @@ def test_export_dict_max(self): data = prob.toDict() var1, prob1 = LpProblem.fromDict(data) x, y, z, w = (var1[name] for name in ["x", "y", "z", "w"]) - print("\t Testing maximize continuous LP solution") pulpTestCheck( prob1, self.solver, [const.LpStatusOptimal], {x: 4, y: 1, z: 8, w: 0} ) @@ -966,7 +928,6 @@ def test_export_solver_dict_LP(self): prob += w >= 0, "c4" data = self.solver.toDict() solver1 = getSolverFromDict(data) - print("\t Testing continuous LP solution - export solver dict") pulpTestCheck( prob, solver1, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -1006,7 +967,6 @@ def test_export_solver_json(self): os.remove(filename) except: pass - print("\t Testing continuous LP solution - export solver JSON") pulpTestCheck( prob, solver1, [const.LpStatusOptimal], {x: 4, y: -1, z: 6, w: 0} ) @@ -1025,7 +985,6 @@ def test_timeLimit(self): prob += w >= 0, "c4" self.solver.timeLimit = 20 # CHOCO has issues when given a time limit - print("\t Testing timeLimit argument") if self.solver.name != "CHOCO_CMD": pulpTestCheck( prob, @@ -1035,7 +994,6 @@ def test_timeLimit(self): ) def test_assignInvalidStatus(self): - print("\t Testing invalid status") t = LpProblem("test") Invalid = -100 self.assertRaises(const.PulpError, lambda: t.assignStatus(Invalid)) @@ -1063,7 +1021,6 @@ def test_logPath(self): "PULP_CBC_CMD", "COIN_CMD", ]: - print("\t Testing logPath argument") pulpTestCheck( prob, self.solver, @@ -1084,7 +1041,6 @@ def test_makeDict_behavior(self): target = {"A": {"C": 1, "D": 2}, "B": {"C": 3, "D": 4}} dict_with_default = makeDict(headers, values, default=0) dict_without_default = makeDict(headers, values) - print("\t Testing makeDict general behavior") self.assertEqual(dict_with_default, target) self.assertEqual(dict_without_default, target) @@ -1096,7 +1052,6 @@ def test_makeDict_default_value(self): values = [[1, 2], [3, 4]] dict_with_default = makeDict(headers, values, default=0) dict_without_default = makeDict(headers, values) - print("\t Testing makeDict default value behavior") # Check if a default value is passed self.assertEqual(dict_with_default["X"]["Y"], 0) # Check if a KeyError is raised @@ -1120,7 +1075,6 @@ def test_importMPS_maximize(self): _vars, prob2 = LpProblem.fromMPS(filename, sense=prob.sense) _dict1 = getSortedDict(prob) _dict2 = getSortedDict(prob2) - print("\t Testing reading MPS files - maximize") self.assertDictEqual(_dict1, _dict2) def test_importMPS_noname(self): @@ -1140,7 +1094,6 @@ def test_importMPS_noname(self): _vars, prob2 = LpProblem.fromMPS(filename, sense=prob.sense) _dict1 = getSortedDict(prob) _dict2 = getSortedDict(prob2) - print("\t Testing reading MPS files - noname") self.assertDictEqual(_dict1, _dict2) def test_importMPS_integer(self): @@ -1158,7 +1111,6 @@ def test_importMPS_integer(self): _vars, prob2 = LpProblem.fromMPS(filename, sense=prob.sense) _dict1 = getSortedDict(prob) _dict2 = getSortedDict(prob2) - print("\t Testing reading MPS files - integer variable") self.assertDictEqual(_dict1, _dict2) def test_importMPS_binary(self): @@ -1177,7 +1129,6 @@ def test_importMPS_binary(self): ) _dict1 = getSortedDict(prob, keyCons="constant") _dict2 = getSortedDict(prob2, keyCons="constant") - print("\t Testing reading MPS files - binary variable, no constraint names") self.assertDictEqual(_dict1, _dict2) def test_importMPS_RHS_fields56(self): @@ -1192,20 +1143,10 @@ def test_importMPS_PL_bound(self): """Import MPS file with PL bound type.""" with tempfile.NamedTemporaryFile(delete=False) as h: h.write(str.encode(EXAMPLE_MPS_PL_BOUNDS)) - print("\t Testing reading MPS files - PL bound") _, problem = LpProblem.fromMPS(h.name) os.unlink(h.name) self.assertIsInstance(problem, LpProblem) - # def test_importMPS_2(self): - # name = self._testMethodName - # # filename = name + ".mps" - # filename = "/home/pchtsp/Downloads/test.mps" - # _vars, _prob = LpProblem.fromMPS(filename) - # _prob.solve() - # for k, v in _vars.items(): - # print(k, v.value()) - def test_unset_objective_value__is_valid(self): """Given a valid problem that does not converge, assert that it is still categorised as valid. @@ -1266,7 +1207,6 @@ def add_const(prob): @gurobi_test def test_measuring_solving_time(self): - print("\t Testing measuring optimization time") time_limit = 10 solver_settings = dict( @@ -1305,10 +1245,9 @@ def test_measuring_solving_time(self): @gurobi_test def test_time_limit_no_solution(self): - print("\t Test time limit with no solution") time_limit = 1 - solver_settings = dict(HiGHS_CMD=50, HiGHS=50, PULP_CBC_CMD=30, COIN_CMD=30) + solver_settings = dict(HiGHS_CMD=60, HiGHS=60, PULP_CBC_CMD=60, COIN_CMD=60) bins = solver_settings.get(self.solver.name) if bins is None: # not all solvers have timeLimit support @@ -1331,7 +1270,6 @@ def test_invalid_var_names(self): prob += x + z >= 10, "c2" prob += -y + z == 7, "c3" prob += w >= 0, "c4" - print("\t Testing invalid var names") if self.solver.name not in [ "GUROBI_CMD", # end is a key-word for LP files ]: @@ -1349,7 +1287,6 @@ def test_LpVariable_indexs_param(self): customers = [1, 2, 3] agents = ["A", "B", "C"] - print("\t Testing 'indexs' param continues to work for LpVariable.dicts") # explicit param creates a dict of type LpVariable assign_vars = LpVariable.dicts(name="test", indices=(customers, agents)) for k, v in assign_vars.items(): @@ -1362,7 +1299,6 @@ def test_LpVariable_indexs_param(self): for a, b in v.items(): self.assertIsInstance(b, LpVariable) - print("\t Testing 'indexs' param continues to work for LpVariable.matrix") # explicit param creates list of LpVariable assign_vars_matrix = LpVariable.matrix( name="test", indices=(customers, agents) @@ -1385,14 +1321,12 @@ def test_LpVariable_indices_param(self): customers = [1, 2, 3] agents = ["A", "B", "C"] - print("\t Testing 'indices' argument works in LpVariable.dicts") # explicit param creates a dict of type LpVariable assign_vars = LpVariable.dicts(name="test", indices=(customers, agents)) for k, v in assign_vars.items(): for a, b in v.items(): self.assertIsInstance(b, LpVariable) - print("\t Testing 'indices' param continues to work for LpVariable.matrix") # explicit param creates list of list of LpVariable assign_vars_matrix = LpVariable.matrix( name="test", indices=(customers, agents) @@ -1407,7 +1341,6 @@ def test_parse_cplex_mipopt_solution(self): """ from io import StringIO - print("\t Testing that `readsol` can parse CPLEX mipopt solution") # Example solution generated by CPLEX mipopt solver file_content = """ @@ -1472,7 +1405,6 @@ def test_options_parsing_SCIP_HIGHS(self): prob += -y + z == 7, "c3" prob += w >= 0, "c4" # CHOCO has issues when given a time limit - print("\t Testing options parsing") if self.solver.__class__ in [SCIP_CMD, FSCIP_CMD]: self.solver.options = ["limits/time", 20] pulpTestCheck( From 1cabad0efabaee63d83ea044b5c75900e132119b Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 09:32:36 +0200 Subject: [PATCH 03/10] workaround to deactivate messages on licenses --- pulp/apis/__init__.py | 2 +- pulp/apis/copt_api.py | 16 ++++++++++++---- pulp/apis/gurobi_api.py | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pulp/apis/__init__.py b/pulp/apis/__init__.py index 41c3240f..5133a92a 100644 --- a/pulp/apis/__init__.py +++ b/pulp/apis/__init__.py @@ -151,7 +151,7 @@ def listSolvers(onlyAvailable=False): """ result = [] for s in _all_solvers: - solver = s() + solver = s(msg=False) if (not onlyAvailable) or solver.available(): result.append(solver.name) del solver diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index f2631197..e2a9ed48 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -6,7 +6,7 @@ from uuid import uuid4 from .core import sparse, ctypesArrayFill, PulpSolverError -from .core import clock, log +from .core import clock from .core import LpSolver, LpSolver_CMD from ..constants import ( @@ -894,9 +894,17 @@ def __init__( logPath=logPath, warmStart=warmStart, ) - - self.coptenv = coptpy.Envr() - self.coptmdl = self.coptenv.createModel() + # workaround to deactivate logging when msg=False + if not self.msg: + devnull = open("/dev/null", "w") + oldstdout_fno = os.dup(sys.stdout.fileno()) + os.dup2(devnull.fileno(), 1) + self.coptenv = coptpy.Envr() + self.coptmdl = self.coptenv.createModel() + os.dup2(oldstdout_fno, 1) + else: + self.coptenv = coptpy.Envr() + self.coptmdl = self.coptenv.createModel() if not self.msg: self.coptmdl.setParam("Logging", 0) diff --git a/pulp/apis/gurobi_api.py b/pulp/apis/gurobi_api.py index ba8bf66d..514e10ac 100644 --- a/pulp/apis/gurobi_api.py +++ b/pulp/apis/gurobi_api.py @@ -440,7 +440,8 @@ def available(self): # normal execution return True # error: we display the gurobi message - warnings.warn(f"GUROBI error: {out}.") + if self.msg: + warnings.warn(f"GUROBI error: {out}.") return False def actualSolve(self, lp): From 906fc6a530c79db512f6883401e984b65e3bf602 Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 09:47:20 +0200 Subject: [PATCH 04/10] make it compatible with windows --- pulp/apis/copt_api.py | 22 ++++++++++++++++------ pulp/pulp.py | 7 +------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index e2a9ed48..de897c8d 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -5,10 +5,15 @@ import warnings from uuid import uuid4 -from .core import sparse, ctypesArrayFill, PulpSolverError -from .core import clock - -from .core import LpSolver, LpSolver_CMD +from .core import ( + sparse, + ctypesArrayFill, + PulpSolverError, + LpSolver, + LpSolver_CMD, + clock, + operating_system, +) from ..constants import ( LpStatusNotSolved, LpStatusOptimal, @@ -896,9 +901,14 @@ def __init__( ) # workaround to deactivate logging when msg=False if not self.msg: - devnull = open("/dev/null", "w") oldstdout_fno = os.dup(sys.stdout.fileno()) - os.dup2(devnull.fileno(), 1) + if operating_system == "win": + # windows doesn't have /dev/null + os.dup2(0, 1) + else: + # linux and mac should have /dev/null + devnull = open("/dev/null", "w") + os.dup2(devnull.fileno(), 1) self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() os.dup2(oldstdout_fno, 1) diff --git a/pulp/pulp.py b/pulp/pulp.py index 2c910934..b186e6ed 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -104,12 +104,7 @@ from . import constants as const from . import mps_lp as mpslp -try: - from collections.abc import Iterable -except ImportError: - # python 2.7 compatible - from collections.abc import Iterable - +from collections.abc import Iterable import logging log = logging.getLogger(__name__) From 7d7c1b48414bcf0c71c2eda63ef83ec571030dde Mon Sep 17 00:00:00 2001 From: Franco Peschiera Date: Fri, 12 Jul 2024 10:14:08 +0200 Subject: [PATCH 05/10] tests on windows. --- pulp/apis/copt_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index de897c8d..cbaf3adb 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -902,13 +902,14 @@ def __init__( # workaround to deactivate logging when msg=False if not self.msg: oldstdout_fno = os.dup(sys.stdout.fileno()) + # for some reason, using os.devnull does not work. if operating_system == "win": # windows doesn't have /dev/null - os.dup2(0, 1) + devnull = open('nul', "w") else: # linux and mac should have /dev/null devnull = open("/dev/null", "w") - os.dup2(devnull.fileno(), 1) + os.dup2(devnull.fileno(), 1) self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() os.dup2(oldstdout_fno, 1) From ebf8efbd0afddd7863d6d1a56908b427e85bd9cd Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:15:00 +0200 Subject: [PATCH 06/10] linter --- pulp/apis/copt_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index cbaf3adb..71183480 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -905,7 +905,7 @@ def __init__( # for some reason, using os.devnull does not work. if operating_system == "win": # windows doesn't have /dev/null - devnull = open('nul', "w") + devnull = open("nul", "w") else: # linux and mac should have /dev/null devnull = open("/dev/null", "w") From 3ae0ff9798a908d186c9b15a29c3a3a800204944 Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:22:18 +0200 Subject: [PATCH 07/10] keep msg=True when detecting solvers since it's blocking the print of available solver. --- HISTORY | 5 +++++ pulp/apis/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY b/HISTORY index e236b327..0db4af1d 100644 --- a/HISTORY +++ b/HISTORY @@ -2,6 +2,11 @@ # Copyright S.A.Mitchell (s.mitchell@auckland.ac.nz), 2007- # Copyright F.Peschiera (pchtsp@gmail.com), 2019- # See the LICENSE file for copyright information. +2.9.0 2024-07-12 + HiGHS available as solver + added HiGHS_CMD to github actions + deactivated warnings on msg=False + minor fixes 2.8.0 2024-01-12 mip start in HiGHS_CMD and SCIP_PY GUROBI solver with environment handling diff --git a/pulp/apis/__init__.py b/pulp/apis/__init__.py index 5133a92a..41c3240f 100644 --- a/pulp/apis/__init__.py +++ b/pulp/apis/__init__.py @@ -151,7 +151,7 @@ def listSolvers(onlyAvailable=False): """ result = [] for s in _all_solvers: - solver = s(msg=False) + solver = s() if (not onlyAvailable) or solver.available(): result.append(solver.name) del solver From 96bfffa5223cc64182e9d8086b1782bd25493a68 Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:39:07 +0200 Subject: [PATCH 08/10] more tests --- pulp/apis/__init__.py | 2 +- pulp/apis/copt_api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pulp/apis/__init__.py b/pulp/apis/__init__.py index 41c3240f..5133a92a 100644 --- a/pulp/apis/__init__.py +++ b/pulp/apis/__init__.py @@ -151,7 +151,7 @@ def listSolvers(onlyAvailable=False): """ result = [] for s in _all_solvers: - solver = s() + solver = s(msg=False) if (not onlyAvailable) or solver.available(): result.append(solver.name) del solver diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index 71183480..9d55e30a 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -909,10 +909,10 @@ def __init__( else: # linux and mac should have /dev/null devnull = open("/dev/null", "w") - os.dup2(devnull.fileno(), 1) + os.dup2(devnull.fileno(), sys.stdout.fileno()) self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() - os.dup2(oldstdout_fno, 1) + os.dup2(oldstdout_fno, sys.stdout.fileno()) else: self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() From c7fd4a32f77949972eae6209cc02c2cbf31bb78e Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:44:58 +0200 Subject: [PATCH 09/10] undo hiding messages --- pulp/apis/copt_api.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index 9d55e30a..db67199a 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -901,18 +901,18 @@ def __init__( ) # workaround to deactivate logging when msg=False if not self.msg: - oldstdout_fno = os.dup(sys.stdout.fileno()) - # for some reason, using os.devnull does not work. - if operating_system == "win": - # windows doesn't have /dev/null - devnull = open("nul", "w") - else: - # linux and mac should have /dev/null - devnull = open("/dev/null", "w") - os.dup2(devnull.fileno(), sys.stdout.fileno()) + # oldstdout_fno = os.dup(sys.stdout.fileno()) + # # for some reason, using os.devnull does not work. + # if operating_system == "win": + # # windows doesn't have /dev/null + # devnull = open("nul", "w") + # else: + # # linux and mac should have /dev/null + # devnull = open("/dev/null", "w") + # os.dup2(devnull.fileno(), sys.stdout.fileno()) self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() - os.dup2(oldstdout_fno, sys.stdout.fileno()) + # os.dup2(oldstdout_fno, sys.stdout.fileno()) else: self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() From 70378535828425d149d56daf185cba6ef12ffd29 Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:50:24 +0200 Subject: [PATCH 10/10] clean comments --- pulp/apis/copt_api.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pulp/apis/copt_api.py b/pulp/apis/copt_api.py index db67199a..6b020b49 100644 --- a/pulp/apis/copt_api.py +++ b/pulp/apis/copt_api.py @@ -899,23 +899,8 @@ def __init__( logPath=logPath, warmStart=warmStart, ) - # workaround to deactivate logging when msg=False - if not self.msg: - # oldstdout_fno = os.dup(sys.stdout.fileno()) - # # for some reason, using os.devnull does not work. - # if operating_system == "win": - # # windows doesn't have /dev/null - # devnull = open("nul", "w") - # else: - # # linux and mac should have /dev/null - # devnull = open("/dev/null", "w") - # os.dup2(devnull.fileno(), sys.stdout.fileno()) - self.coptenv = coptpy.Envr() - self.coptmdl = self.coptenv.createModel() - # os.dup2(oldstdout_fno, sys.stdout.fileno()) - else: - self.coptenv = coptpy.Envr() - self.coptmdl = self.coptenv.createModel() + self.coptenv = coptpy.Envr() + self.coptmdl = self.coptenv.createModel() if not self.msg: self.coptmdl.setParam("Logging", 0)