From c02692beb350613c211fa685d699d45111fdb8c8 Mon Sep 17 00:00:00 2001 From: Guspan Tanadi <36249910+guspan-tanadi@users.noreply.github.com> Date: Fri, 28 Jun 2024 18:19:55 +0700 Subject: [PATCH 1/6] docs: internal archive link intended (#758) --- INSTALL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL b/INSTALL index 4ff7da5d..2910bdd5 100644 --- a/INSTALL +++ b/INSTALL @@ -120,5 +120,5 @@ Solver pulp.solvers.YAPOSIB unavailable .. _`installing python`: http://www.diveintopython.org/installing_python/index.html .. _github: https://github.com/coin-or/pulp-or .. _pip: https://pypi.python.org/pypi/pip -.. _`PuLP zipfile`: https://github.com/coi-nor/pulp-or/archive/master.zip +.. _`PuLP zipfile`: https://github.com/coin-or/pulp-or/archive/master.zip From eef6431e65a5bf29b08b815720ec6a862cf32002 Mon Sep 17 00:00:00 2001 From: Franco Peschiera Date: Fri, 12 Jul 2024 10:50:38 +0200 Subject: [PATCH 2/6] added download of highs binary. And fixed errors in subprocess (#759) * added download of highs binary. And fixed errors in subprocess * remove the prints. Show available solvers * workaround to deactivate messages on licenses * make it compatible with windows * tests on windows. * linter * keep msg=True when detecting solvers since it's blocking the print of available solver. * more tests * undo hiding messages * clean comments --- .github/workflows/pythonpackage.yml | 8 +++ HISTORY | 5 ++ pulp/apis/__init__.py | 2 +- pulp/apis/coin_api.py | 4 +- pulp/apis/copt_api.py | 14 +++-- pulp/apis/gurobi_api.py | 3 +- pulp/apis/highs_api.py | 13 +++-- pulp/pulp.py | 7 +-- pulp/tests/run_tests.py | 4 ++ pulp/tests/test_pulp.py | 88 ++++------------------------- 10 files changed, 52 insertions(+), 96 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/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 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/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/apis/copt_api.py b/pulp/apis/copt_api.py index f2631197..6b020b49 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, log - -from .core import LpSolver, LpSolver_CMD +from .core import ( + sparse, + ctypesArrayFill, + PulpSolverError, + LpSolver, + LpSolver_CMD, + clock, + operating_system, +) from ..constants import ( LpStatusNotSolved, LpStatusOptimal, @@ -894,7 +899,6 @@ def __init__( logPath=logPath, warmStart=warmStart, ) - self.coptenv = coptpy.Envr() self.coptmdl = self.coptenv.createModel() 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): 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/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__) 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 7093e5a3..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, @@ -836,19 +804,17 @@ 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): """ 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): @@ -865,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} ) @@ -883,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} ) @@ -908,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} ) @@ -928,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} ) @@ -949,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} ) @@ -967,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} ) @@ -1007,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} ) @@ -1026,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, @@ -1036,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)) @@ -1064,7 +1021,6 @@ def test_logPath(self): "PULP_CBC_CMD", "COIN_CMD", ]: - print("\t Testing logPath argument") pulpTestCheck( prob, self.solver, @@ -1085,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) @@ -1097,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 @@ -1121,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): @@ -1141,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): @@ -1159,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): @@ -1178,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): @@ -1193,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. @@ -1267,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( @@ -1278,6 +1217,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: @@ -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=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 7286f40dbe99d1847979ee289d1bd820436520ad Mon Sep 17 00:00:00 2001 From: pchtsp Date: Fri, 12 Jul 2024 10:51:27 +0200 Subject: [PATCH 3/6] bump version --- pulp/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulp/constants.py b/pulp/constants.py index 9eea9f7e..00fd751b 100644 --- a/pulp/constants.py +++ b/pulp/constants.py @@ -27,7 +27,7 @@ This file contains the constant definitions for PuLP Note that hopefully these will be changed into something more pythonic """ -VERSION = "2.8.0" +VERSION = "2.9.0" EPS = 1e-7 # variable categories From e20a405ff5262c20da404b4644612381f1a05255 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Sat, 31 Aug 2024 22:19:39 +1000 Subject: [PATCH 4/6] update docs to reflect MILP features (#770) --- README.rst | 119 ++++++++++-------- doc/source/CaseStudies/a_blending_problem.rst | 2 + doc/source/index.rst | 36 ++++-- doc/source/main/basic_python_coding.rst | 3 + doc/source/main/optimisation_concepts.rst | 2 + doc/source/main/the_optimisation_process.rst | 3 + pulp/pulp.py | 117 ++++++++++------- 7 files changed, 176 insertions(+), 106 deletions(-) diff --git a/README.rst b/README.rst index 7ed640d7..d3685d48 100644 --- a/README.rst +++ b/README.rst @@ -10,74 +10,53 @@ pulp :target: https://pypi.org/project/PuLP/ :alt: PyPI - Downloads -PuLP is an LP modeler written in Python. PuLP can generate MPS or LP files and call GLPK_, COIN-OR CLP/`CBC`_, CPLEX_, GUROBI_, MOSEK_, XPRESS_, CHOCO_, MIPCL_, HiGHS_, SCIP_/FSCIP_ to solve linear problems. - -Installation -================ - -The easiest way to install pulp is via `PyPi `_ - -If pip is available on your system:: - - python -m pip install pulp - -Otherwise follow the download instructions on the PyPi page. - - -If you want to install the latest version from github you can run the following:: - - python -m pip install -U git+https://github.com/coin-or/pulp +PuLP is an linear and mixed integer programming modeler written in Python. With PuLP, it is simple to create MILP optimisation problems and solve them with the latest open-source (or proprietary) solvers. PuLP can generate MPS or LP files and call solvers such as GLPK_, COIN-OR CLP/`CBC`_, CPLEX_, GUROBI_, MOSEK_, XPRESS_, CHOCO_, MIPCL_, HiGHS_, SCIP_/FSCIP_. +The documentation for PuLP can be `found here `_. -On Linux and OSX systems the tests must be run to make the default -solver executable. +PuLP is part of the `COIN-OR project `_. -:: - - sudo pulptest - -Examples +Installation ================ -See the examples directory for examples. - PuLP requires Python 3.7 or newer. -The examples use the default solver (CBC). To use other solvers they must be available (installed and accessible). For more information on how to do that, see the `guide on configuring solvers `_. +The easiest way to install PuLP is with ``pip``. If ``pip`` is available on your system, type:: -Documentation -================ + python -m pip install pulp -Documentation is found on https://coin-or.github.io/pulp/. +Otherwise follow the download instructions on the `PyPi page `_. -Use LpVariable() to create new variables. To create a variable 0 <= x <= 3:: +Quickstart +=============== +Use ``LpVariable`` to create new variables. To create a variable x with 0 ≤ x ≤ 3:: + + from pulp import * x = LpVariable("x", 0, 3) -To create a variable 0 <= y <= 1:: +To create a binary variable, y, with values either 0 or 1:: - y = LpVariable("y", 0, 1) + y = LpVariable("y", cat="Binary") -Use LpProblem() to create new problems. Create "myProblem":: +Use ``LpProblem`` to create new problems. Create a problem called "myProblem" like so:: prob = LpProblem("myProblem", LpMinimize) -Combine variables to create expressions and constraints, then add them to the -problem:: +Combine variables in order to create expressions and constraints, and then add them to the problem.:: prob += x + y <= 2 -If you add an expression (not a constraint), it will -become the objective:: +An expression is a constraint without a right-hand side (RHS) sense (one of ``=``, ``<=`` or ``>=``). If you add an expression to a problem, it will become the objective:: prob += -4*x + y -To solve with the default included solver:: +To solve the problem with the default included solver:: status = prob.solve() -To use another sovler to solve the problem:: +If you want to try another solver to solve the problem:: status = prob.solve(GLPK(msg = 0)) @@ -86,26 +65,53 @@ Display the status of the solution:: LpStatus[status] > 'Optimal' -You can get the value of the variables using value(). ex:: +You can get the value of the variables using ``value``. ex:: value(x) > 2.0 -Exported Classes: -* ``LpProblem`` -- Container class for a Linear programming problem -* ``LpVariable`` -- Variables that are added to constraints in the LP -* ``LpConstraint`` -- A constraint of the general form +Essential Classes +------------------ + + +* ``LpProblem`` -- Container class for a Linear or Integer programming problem +* ``LpVariable`` -- Variables that are added into constraints in the LP problem +* ``LpConstraint`` -- Constraints of the general form - a1x1+a2x2 ...anxn (<=, =, >=) b + a1x1 + a2x2 + ... + anxn (<=, =, >=) b -* ``LpConstraintVar`` -- Used to construct a column of the model in column-wise modelling +* ``LpConstraintVar`` -- A special type of constraint for constructing column of the model in column-wise modelling -Exported Functions: +Useful Functions +------------------ * ``value()`` -- Finds the value of a variable or expression -* ``lpSum()`` -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct a linear expression to be used as a constraint or variable -* ``lpDot()`` --given two lists of the form [a1, a2, ..., an] and [ x1, x2, ..., xn] will construct a linear expression to be used as a constraint or variable +* ``lpSum()`` -- Given a list of the form [a1*x1, a2*x2, ..., an*xn] will construct a linear expression to be used as a constraint or variable +* ``lpDot()`` -- Given two lists of the form [a1, a2, ..., an] and [x1, x2, ..., xn] will construct a linear expression to be used as a constraint or variable + +More Examples +================ + +Several tutorial are given in `documentation `_ and pure code examples are available in `examples/ directory `_ . + +The examples use the default solver (CBC). To use other solvers they must be available (installed and accessible). For more information on how to do that, see the `guide on configuring solvers `_. + + +For Developers +================ + + +If you want to install the latest version from GitHub you can run:: + + python -m pip install -U git+https://github.com/coin-or/pulp + + +On Linux and MacOS systems, you must run the tests to make the default solver executable:: + + sudo pulptest + + Building the documentation @@ -126,17 +132,20 @@ To build, run the following in a terminal window, in the PuLP root directory A folder named html will be created inside the ``build/`` directory. The home page for the documentation is ``doc/build/html/index.html`` which can be opened in a browser. +Contributing to PuLP +----------------------- +Instructions for making your first contribution to PuLP are given `here `_. - - - - -**Comments, bug reports, patches and suggestions are welcome.** +**Comments, bug reports, patches and suggestions are very welcome!** * Comments and suggestions: https://github.com/coin-or/pulp/discussions * Bug reports: https://github.com/coin-or/pulp/issues * Patches: https://github.com/coin-or/pulp/pulls +Copyright and License +======================= +PuLP is distributed under an MIT license. + Copyright J.S. Roy, 2003-2005 Copyright Stuart A. Mitchell See the LICENSE file for copyright information. diff --git a/doc/source/CaseStudies/a_blending_problem.rst b/doc/source/CaseStudies/a_blending_problem.rst index 9b4371e4..5d526757 100644 --- a/doc/source/CaseStudies/a_blending_problem.rst +++ b/doc/source/CaseStudies/a_blending_problem.rst @@ -1,3 +1,5 @@ +.. _blending_problem: + A Blending Problem =================== diff --git a/doc/source/index.rst b/doc/source/index.rst index abeb2e4d..50156bd8 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -6,14 +6,21 @@ Optimization with PuLP ---------------------- -You can begin learning Python and using PuLP -by looking at the content below. We recommend that you read The Optimisation Process, -Optimisation Concepts, and the Introduction to Python -before beginning the case-studies. For instructions for the installation of PuLP -see :ref:`installation`. - -The full PuLP function documentation is available, and useful functions -will be explained in the case studies. +PuLP is an linear and mixed integer programming modeler written in Python. + +With PuLP, it is simple to create MILP optimisation problems and solve them with the +latest open-source (or proprietary) solvers. PuLP can generate MPS or LP files and +call solvers such as GLPK_, COIN-OR CLP/`CBC`_, CPLEX_, GUROBI_, MOSEK_, XPRESS_, +CHOCO_, MIPCL_, HiGHS_, SCIP_/FSCIP_. + +Here are some ways to get started using PuLP: + +* for instructions about installing PuLP see :ref:`installation`. +* If you're new to Python and optimisation we recommend that you read :ref:`optimisation_concepts`, :ref:`optimisation_process`, and the :ref:`getting_started_with_python`. +* If you want to jump right in then start reading the case studies starting with :ref:`blending_problem`. + +The full PuLP API documentation is available, and useful functions +are also explained in the case studies. The case studies are in order, so the later case studies will assume you have (at least) read the earlier case studies. However, we will provide links to any relevant information you will need. @@ -34,3 +41,16 @@ Authors The authors of this documentation (the pulp documentation team) include: .. include:: AUTHORS.txt + + +.. _GLPK: http://www.gnu.org/software/glpk/glpk.html +.. _CBC: https://github.com/coin-or/Cbc +.. _CPLEX: http://www.cplex.com/ +.. _GUROBI: http://www.gurobi.com/ +.. _MOSEK: https://www.mosek.com/ +.. _XPRESS: https://www.fico.com/es/products/fico-xpress-solver +.. _CHOCO: https://choco-solver.org/ +.. _MIPCL: http://mipcl-cpp.appspot.com/ +.. _SCIP: https://www.scipopt.org/ +.. _HiGHS: https://highs.dev +.. _FSCIP: https://ug.zib.de diff --git a/doc/source/main/basic_python_coding.rst b/doc/source/main/basic_python_coding.rst index 0b0f6cdf..35ea9597 100644 --- a/doc/source/main/basic_python_coding.rst +++ b/doc/source/main/basic_python_coding.rst @@ -1,3 +1,6 @@ +.. _getting_started_with_python: + + Basic Python Coding =================== diff --git a/doc/source/main/optimisation_concepts.rst b/doc/source/main/optimisation_concepts.rst index d7416612..5841d855 100644 --- a/doc/source/main/optimisation_concepts.rst +++ b/doc/source/main/optimisation_concepts.rst @@ -1,3 +1,5 @@ +.. _optimisation_concepts: + Optimisation Concepts ===================== diff --git a/doc/source/main/the_optimisation_process.rst b/doc/source/main/the_optimisation_process.rst index 67098ff9..a07df8d9 100644 --- a/doc/source/main/the_optimisation_process.rst +++ b/doc/source/main/the_optimisation_process.rst @@ -1,3 +1,6 @@ +.. _optimisation_process: + + The Optimisation Process ======================== diff --git a/pulp/pulp.py b/pulp/pulp.py index b186e6ed..79b473b1 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -26,70 +26,101 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -PuLP is an LP modeler written in python. PuLP can generate MPS or LP files -and call GLPK[1], COIN CLP/CBC[2], CPLEX[3], GUROBI[4] and MOSEK[5] to solve linear -problems. +PuLP is an linear and mixed integer programming modeler written in Python. -See the examples directory for examples. +With PuLP, it is simple to create MILP optimisation problems and solve them with the +latest open-source (or proprietary) solvers. PuLP can generate MPS or LP files and +call solvers such as GLPK_, COIN-OR CLP/`CBC`_, CPLEX_, GUROBI_, MOSEK_, XPRESS_, +CHOCO_, MIPCL_, HiGHS_, SCIP_/FSCIP_. +The documentation for PuLP can be `found here `_. + +Many examples are shown in the `documentation `_ +and pure code examples are available in `examples/ directory `_ . The examples require at least a solver in your PATH or a shared library file. -Documentation is found on https://www.coin-or.org/PuLP/. -A comprehensive wiki can be found at https://www.coin-or.org/PuLP/ +Quickstart +------------ +Use ``LpVariable`` to create new variables. To create a variable x with 0 ≤ x ≤ 3:: + + from pulp import * + x = LpVariable("x", 0, 3) + +To create a binary variable, y, with values either 0 or 1:: + + y = LpVariable("y", cat="Binary") + +Use ``LpProblem`` to create new problems. Create a problem called "myProblem" like so:: + + prob = LpProblem("myProblem", LpMinimize) + +Combine variables in order to create expressions and constraints, and then add them to +the problem.:: + + prob += x + y <= 2 + +An expression is a constraint without a right-hand side (RHS) sense (one of ``=``, +``<=`` or ``>=``). If you add an expression to a problem, it will become the +objective:: -Use LpVariable() to create new variables. To create a variable 0 <= x <= 3 ->>> x = LpVariable("x", 0, 3) + prob += -4*x + y -To create a variable 0 <= y <= 1 ->>> y = LpVariable("y", 0, 1) +To solve the problem with the default included solver:: -Use LpProblem() to create new problems. Create "myProblem" ->>> prob = LpProblem("myProblem", const.LpMinimize) + status = prob.solve() -Combine variables to create expressions and constraints and add them to the -problem. ->>> prob += x + y <= 2 +If you want to try another solver to solve the problem:: -If you add an expression (not a constraint), it will -become the objective. ->>> prob += -4 * x + y + status = prob.solve(GLPK(msg = 0)) -Choose a solver and solve the problem. ex: ->>> status = prob.solve(PULP_CBC_CMD(msg=0)) +Display the status of the solution:: -Display the status of the solution ->>> const.LpStatus[status] -'Optimal' + LpStatus[status] + > 'Optimal' -You can get the value of the variables using value(). ex: ->>> value(x) -2.0 +You can get the value of the variables using ``value``. ex:: -Exported Classes: - - LpProblem -- Container class for a Linear programming problem - - LpVariable -- Variables that are added to constraints in the LP - - LpConstraint -- A constraint of the general form - a1x1+a2x2 ...anxn (<=, =, >=) b - - LpConstraintVar -- Used to construct a column of the model in column-wise - modelling + value(x) + > 2.0 -Exported Functions: - - value() -- Finds the value of a variable or expression - - lpSum() -- given a list of the form [a1*x1, a2x2, ..., anxn] will construct - a linear expression to be used as a constraint or variable - - lpDot() --given two lists of the form [a1, a2, ..., an] and - [ x1, x2, ..., xn] will construct a linear epression to be used - as a constraint or variable +Useful Classes and Functions +----------------------------- -Comments, bug reports, patches and suggestions are welcome. -https://github.com/coin-or/pulp +Exported classes: -References: +* ``LpProblem`` -- Container class for a Linear or Integer programming problem +* ``LpVariable`` -- Variables that are added into constraints in the LP problem +* ``LpConstraint`` -- Constraints of the general form + + a1x1 + a2x2 + ... + anxn (<=, =, >=) b + +* ``LpConstraintVar`` -- A special type of constraint for constructing column of the model in column-wise modelling + +Exported functions: + +* ``value()`` -- Finds the value of a variable or expression +* ``lpSum()`` -- Given a list of the form [a1*x1, a2*x2, ..., an*xn] will construct a linear expression to be used as a constraint or variable +* ``lpDot()`` -- Given two lists of the form [a1, a2, ..., an] and [x1, x2, ..., xn] will construct a linear expression to be used as a constraint or variable + +Contributing to PuLP +----------------------- +Instructions for making your first contribution to PuLP are given +`here `_. + +**Comments, bug reports, patches and suggestions are very welcome!** + +* Comments and suggestions: https://github.com/coin-or/pulp/discussions +* Bug reports: https://github.com/coin-or/pulp/issues +* Patches: https://github.com/coin-or/pulp/pulls + +References +---------- [1] http://www.gnu.org/software/glpk/glpk.html [2] http://www.coin-or.org/ [3] http://www.cplex.com/ [4] http://www.gurobi.com/ [5] http://www.mosek.com/ + """ from collections import Counter From b5a445f4ef95d347621c41ff55922b38f8cb1475 Mon Sep 17 00:00:00 2001 From: Frederick Robinson Date: Sat, 31 Aug 2024 05:20:03 -0700 Subject: [PATCH 5/6] add test / fix for lpdot (#769) * add test / fix for lpdot * add tests, clean up implementation --- pulp/constants.py | 9 --------- pulp/pulp.py | 10 +++++++--- pulp/tests/test_lpdot.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 pulp/tests/test_lpdot.py diff --git a/pulp/constants.py b/pulp/constants.py index 00fd751b..976bb457 100644 --- a/pulp/constants.py +++ b/pulp/constants.py @@ -87,15 +87,6 @@ LpCplexLPLineSize = 78 -def isiterable(obj): - try: - obj = iter(obj) - except: - return False - else: - return True - - class PulpError(Exception): """ Pulp Exception Class diff --git a/pulp/pulp.py b/pulp/pulp.py index 79b473b1..a05a964f 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -2264,13 +2264,17 @@ def lpSum(vector): return LpAffineExpression().addInPlace(vector) +def _vector_like(obj): + return isinstance(obj, Iterable) and not isinstance(obj, LpAffineExpression) + + def lpDot(v1, v2): """Calculate the dot product of two lists of linear expressions""" - if not const.isiterable(v1) and not const.isiterable(v2): + if not _vector_like(v1) and not _vector_like(v2): return v1 * v2 - elif not const.isiterable(v1): + elif not _vector_like(v1): return lpDot([v1] * len(v2), v2) - elif not const.isiterable(v2): + elif not _vector_like(v2): return lpDot(v1, [v2] * len(v1)) else: return lpSum([lpDot(e1, e2) for e1, e2 in zip(v1, v2)]) diff --git a/pulp/tests/test_lpdot.py b/pulp/tests/test_lpdot.py new file mode 100644 index 00000000..7e48af3f --- /dev/null +++ b/pulp/tests/test_lpdot.py @@ -0,0 +1,22 @@ +from pulp import lpDot, LpVariable + + +def test_lpdot(): + x = LpVariable(name="x") + + product = lpDot(1, 2 * x) + assert product.toDict() == [{"name": "x", "value": 2}] + + +def test_pulp_002(): + """ + 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} + assert dict(lpDot(a, [x + y, y + z, z])) == {x: 1, y: 3, z: 5} From 2aa580c193f6dcb4b8236103f7553ceb989281c1 Mon Sep 17 00:00:00 2001 From: Peter Cock Date: Sat, 31 Aug 2024 13:21:25 +0100 Subject: [PATCH 6/6] Fix detection of osx-arm64 (#766) --- pulp/apis/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pulp/apis/core.py b/pulp/apis/core.py index 23570e45..3dcc68be 100644 --- a/pulp/apis/core.py +++ b/pulp/apis/core.py @@ -163,7 +163,6 @@ def initialize(filename, operating_system="linux", arch="64"): PULPCFGFILE += ".win" elif sys.platform in ["darwin"]: operating_system = "osx" - arch = "64" PULPCFGFILE += ".osx" else: operating_system = "linux"