PuLP 3.2.1__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {pulp-3.2.1 → pulp-3.2.2}/PKG-INFO +4 -4
  2. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/PKG-INFO +4 -4
  3. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/requires.txt +3 -1
  4. {pulp-3.2.1 → pulp-3.2.2}/README.rst +2 -2
  5. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/__init__.py +16 -18
  6. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/cplex_api.py +72 -5
  7. {pulp-3.2.1 → pulp-3.2.2}/pulp/mps_lp.py +5 -5
  8. {pulp-3.2.1 → pulp-3.2.2}/pulp/pulp.py +51 -17
  9. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/test_examples.py +4 -1
  10. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/test_gurobipy_env.py +12 -12
  11. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/test_pulp.py +238 -132
  12. {pulp-3.2.1 → pulp-3.2.2}/pyproject.toml +2 -2
  13. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/SOURCES.txt +0 -0
  14. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/dependency_links.txt +0 -0
  15. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/entry_points.txt +0 -0
  16. {pulp-3.2.1 → pulp-3.2.2}/PuLP.egg-info/top_level.txt +0 -0
  17. {pulp-3.2.1 → pulp-3.2.2}/pulp/__init__.py +0 -0
  18. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/choco_api.py +0 -0
  19. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/coin_api.py +0 -0
  20. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/copt_api.py +0 -0
  21. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/core.py +0 -0
  22. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/cuopt_api.py +0 -0
  23. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/glpk_api.py +0 -0
  24. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/gurobi_api.py +0 -0
  25. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/highs_api.py +0 -0
  26. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/mipcl_api.py +0 -0
  27. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/mosek_api.py +0 -0
  28. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/sas_api.py +0 -0
  29. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/scip_api.py +0 -0
  30. {pulp-3.2.1 → pulp-3.2.2}/pulp/apis/xpress_api.py +0 -0
  31. {pulp-3.2.1 → pulp-3.2.2}/pulp/constants.py +0 -0
  32. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/__init__.py +0 -0
  33. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/arm64/__init__.py +0 -0
  34. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/arm64/cbc +0 -0
  35. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/arm64/coin-license.txt +0 -0
  36. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i32/__init__.py +0 -0
  37. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i32/cbc +0 -0
  38. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i32/coin-license.txt +0 -0
  39. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i64/__init__.py +0 -0
  40. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i64/cbc +0 -0
  41. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/linux/i64/coin-license.txt +0 -0
  42. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/osx/i64/__init__.py +0 -0
  43. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/osx/i64/cbc +0 -0
  44. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/osx/i64/coin-license.txt +0 -0
  45. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i32/__init__.py +0 -0
  46. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i32/cbc.exe +0 -0
  47. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i32/coin-license.txt +0 -0
  48. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i64/__init__.py +0 -0
  49. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i64/cbc.exe +0 -0
  50. {pulp-3.2.1 → pulp-3.2.2}/pulp/solverdir/cbc/win/i64/coin-license.txt +0 -0
  51. {pulp-3.2.1 → pulp-3.2.2}/pulp/sparse.py +0 -0
  52. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/__init__.py +0 -0
  53. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/bin_packing_problem.py +0 -0
  54. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/run_tests.py +0 -0
  55. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/test_lpdot.py +0 -0
  56. {pulp-3.2.1 → pulp-3.2.2}/pulp/tests/test_sparse.py +0 -0
  57. {pulp-3.2.1 → pulp-3.2.2}/pulp/utilities.py +0 -0
  58. {pulp-3.2.1 → pulp-3.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PuLP
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: PuLP is an LP modeler written in python. PuLP can generate MPS or LP files and call GLPK, COIN CLP/CBC, CPLEX, and GUROBI to solve linear problems.
5
5
  Author: J.S. Roy
6
6
  Author-email: "S.A. Mitchell" <pulp@stuartmitchell.com>, Franco Peschiera <pchtsp@gmail.com>
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
19
  Requires-Python: >=3.9
20
20
  Description-Content-Type: text/x-rst
21
21
  Provides-Extra: open-py
22
- Requires-Dist: cylp; extra == "open-py"
22
+ Requires-Dist: cylp; sys_platform != "win32" and extra == "open-py"
23
23
  Requires-Dist: highspy; extra == "open-py"
24
24
  Requires-Dist: pyscipopt; extra == "open-py"
25
25
  Provides-Extra: public-py
@@ -153,8 +153,8 @@ To build, run the following in a terminal window, in the PuLP root directory
153
153
 
154
154
  ::
155
155
 
156
- cd pulp
157
- python -m pip install -r requirements-dev.txt
156
+ python3 -m pip install --upgrade pip
157
+ pip install --group=dev .
158
158
  cd doc
159
159
  make html
160
160
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PuLP
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: PuLP is an LP modeler written in python. PuLP can generate MPS or LP files and call GLPK, COIN CLP/CBC, CPLEX, and GUROBI to solve linear problems.
5
5
  Author: J.S. Roy
6
6
  Author-email: "S.A. Mitchell" <pulp@stuartmitchell.com>, Franco Peschiera <pchtsp@gmail.com>
@@ -19,7 +19,7 @@ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
19
  Requires-Python: >=3.9
20
20
  Description-Content-Type: text/x-rst
21
21
  Provides-Extra: open-py
22
- Requires-Dist: cylp; extra == "open-py"
22
+ Requires-Dist: cylp; sys_platform != "win32" and extra == "open-py"
23
23
  Requires-Dist: highspy; extra == "open-py"
24
24
  Requires-Dist: pyscipopt; extra == "open-py"
25
25
  Provides-Extra: public-py
@@ -153,8 +153,8 @@ To build, run the following in a terminal window, in the PuLP root directory
153
153
 
154
154
  ::
155
155
 
156
- cd pulp
157
- python -m pip install -r requirements-dev.txt
156
+ python3 -m pip install --upgrade pip
157
+ pip install --group=dev .
158
158
  cd doc
159
159
  make html
160
160
 
@@ -1,9 +1,11 @@
1
1
 
2
2
  [open_py]
3
- cylp
4
3
  highspy
5
4
  pyscipopt
6
5
 
6
+ [open_py:sys_platform != "win32"]
7
+ cylp
8
+
7
9
  [public_py]
8
10
  gurobipy
9
11
  coptpy
@@ -124,8 +124,8 @@ To build, run the following in a terminal window, in the PuLP root directory
124
124
 
125
125
  ::
126
126
 
127
- cd pulp
128
- python -m pip install -r requirements-dev.txt
127
+ python3 -m pip install --upgrade pip
128
+ pip install --group=dev .
129
129
  cd doc
130
130
  make html
131
131
 
@@ -1,19 +1,19 @@
1
- from typing import Dict, Optional, Type, Union
2
-
3
- from .choco_api import *
4
- from .coin_api import *
5
- from .copt_api import *
6
- from .core import *
7
- from .cplex_api import *
8
- from .glpk_api import *
9
- from .gurobi_api import *
10
- from .highs_api import *
11
- from .mipcl_api import *
12
- from .mosek_api import *
13
- from .sas_api import *
14
- from .scip_api import *
15
- from .xpress_api import *
16
- from .cuopt_api import *
1
+ from typing import Dict, Optional, Type, Union, List
2
+ import json
3
+ from .choco_api import CHOCO_CMD
4
+ from .coin_api import CYLP, PULP_CBC_CMD, COIN_CMD, COINMP_DLL, YAPOSIB
5
+ from .copt_api import COPT, COPT_DLL, COPT_CMD
6
+ from .core import LpSolver, LpSolver_CMD, PulpSolverError
7
+ from .cplex_api import CPLEX_PY, CPLEX_CMD, CPLEX
8
+ from .glpk_api import GLPK_CMD, PYGLPK, GLPK
9
+ from .gurobi_api import GUROBI, GUROBI_CMD
10
+ from .highs_api import HiGHS, HiGHS_CMD
11
+ from .mipcl_api import MIPCL_CMD
12
+ from .mosek_api import MOSEK
13
+ from .sas_api import SAS94, SASCAS, SASsolver
14
+ from .scip_api import SCIP, SCIP_CMD, SCIP_PY, FSCIP_CMD, FSCIP
15
+ from .xpress_api import XPRESS_CMD, XPRESS_PY, XPRESS
16
+ from .cuopt_api import CUOPT
17
17
 
18
18
  _all_solvers: List[Type[LpSolver]] = [
19
19
  CYLP,
@@ -45,8 +45,6 @@ _all_solvers: List[Type[LpSolver]] = [
45
45
  CUOPT,
46
46
  ]
47
47
 
48
- import json
49
-
50
48
  LpSolverDefault: Optional[Union[PULP_CBC_CMD, GLPK_CMD, COIN_CMD]] = None
51
49
  # Default solver selection
52
50
  if PULP_CBC_CMD().available():
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import warnings
3
+ from typing import Iterable, Optional
3
4
 
4
5
  from .. import constants
5
6
  from .core import LpSolver, LpSolver_CMD, PulpSolverError, clock, log, subprocess
@@ -252,7 +253,7 @@ class CPLEX_PY(LpSolver):
252
253
  name = "CPLEX_PY"
253
254
  try:
254
255
  global cplex
255
- import cplex # type: ignore[import-not-found]
256
+ import cplex # type: ignore[import-not-found, import-untyped, unused-ignore]
256
257
  except Exception as e:
257
258
  err = e
258
259
  """The CPLEX LP/MIP solver from python. Something went wrong!!!!"""
@@ -276,6 +277,7 @@ class CPLEX_PY(LpSolver):
276
277
  warmStart=False,
277
278
  logPath=None,
278
279
  threads=None,
280
+ **solverParams,
279
281
  ):
280
282
  """
281
283
  :param bool mip: if False, assume LP even if integer variables
@@ -285,6 +287,15 @@ class CPLEX_PY(LpSolver):
285
287
  :param bool warmStart: if True, the solver will use the current value of variables as a start
286
288
  :param str logPath: path to the log file
287
289
  :param int threads: number of threads to be used by CPLEX to solve a problem (default None uses all available)
290
+
291
+ :param dict solverParams: Additional parameters to set in the CPLEX solver.
292
+
293
+ Parameters should use dot notation as specified in the CPLEX documentation.
294
+ The 'parameters.' prefix is optional. For example:
295
+
296
+ * parameters.advance (or advance)
297
+ * parameters.barrier.algorithm (or barrier.algorithm)
298
+ * parameters.mip.strategy.probe (or mip.strategy.probe)
288
299
  """
289
300
 
290
301
  LpSolver.__init__(
@@ -297,22 +308,25 @@ class CPLEX_PY(LpSolver):
297
308
  logPath=logPath,
298
309
  threads=threads,
299
310
  )
311
+ self.solverParams = solverParams
300
312
 
301
313
  def available(self):
302
314
  """True if the solver is available"""
303
315
  return True
304
316
 
305
- def actualSolve(self, lp, callback=None): # type: ignore[misc]
317
+ def actualSolve(self, lp, callback: Optional[Iterable[type[cplex.callbacks.Callback]]] = None): # type: ignore[misc]
306
318
  """
307
319
  Solve a well formulated lp problem
308
320
 
309
321
  creates a cplex model, variables and constraints and attaches
310
322
  them to the lp model which it then solves
323
+
324
+ :param callback: Optional list of CPLEX callback classes to register during solve
311
325
  """
312
326
  self.buildSolverModel(lp)
313
327
  # set the initial solution
314
328
  log.debug("Solve the Model using cplex")
315
- self.callSolver(lp)
329
+ self.callSolver(lp, callback=callback)
316
330
  # get the solution information
317
331
  solutionStatus = self.findSolutionValues(lp)
318
332
  for var in lp._variables:
@@ -430,6 +444,47 @@ class CPLEX_PY(LpSolver):
430
444
  self.solverModel.MIP_starts.add(
431
445
  cplex.SparsePair(ind=ind, val=val), effort, "1"
432
446
  )
447
+ for param, value in self.solverParams.items():
448
+ self.set_param(param, value)
449
+
450
+ def set_param(self, name: str, value):
451
+ """
452
+ Sets a parameter value using its name.
453
+ """
454
+ param = self.search_param(name=name)
455
+ param.set(value)
456
+
457
+ def get_param(self, name: str):
458
+ """
459
+ Returns the value of a named parameter by searching within the instance's parameters.
460
+ """
461
+ param = self.search_param(name=name)
462
+ return param.get()
463
+
464
+ def search_param(self, name: str):
465
+ """
466
+ Searches for a solver model parameter by its name and returns the corresponding attribute.
467
+
468
+ The method takes a parameter name string, processes it to remove the "parameters." prefix
469
+ and splits it by periods to traverse the attribute hierarchy of the solver model's parameters.
470
+ """
471
+ name = name.replace("parameters.", "")
472
+ param = self.solverModel.parameters
473
+ for attr in name.split("."):
474
+ param = getattr(param, attr)
475
+ return param
476
+
477
+ def get_all_params(self):
478
+ """
479
+ Returns all parameters from the solver model.
480
+ """
481
+ return self.solverModel.parameters.get_all()
482
+
483
+ def get_changed_params(self):
484
+ """
485
+ Returns the parameters that have been changed in the solver model.
486
+ """
487
+ return self.solverModel.parameters.get_changed()
433
488
 
434
489
  def setlogfile(self, fileobj):
435
490
  """
@@ -458,9 +513,21 @@ class CPLEX_PY(LpSolver):
458
513
  """
459
514
  self.solverModel.parameters.timelimit.set(timeLimit)
460
515
 
461
- def callSolver(self, isMIP):
462
- """Solves the problem with cplex"""
516
+ def callSolver(
517
+ self,
518
+ isMIP,
519
+ callback: Optional[Iterable[type[cplex.callbacks.Callback]]] = None,
520
+ ):
521
+ """
522
+ Solves the problem with cplex
523
+
524
+
525
+ :param callback: Optional list of CPLEX callback classes to register during solve
526
+ """
463
527
  # solve the problem
528
+ if callback is not None:
529
+ for call in callback:
530
+ self.solverModel.register_callback(call)
464
531
  self.solveTime = -clock()
465
532
  self.solverModel.solve()
466
533
  self.solveTime += clock()
@@ -312,9 +312,9 @@ def writeMPS(
312
312
  if mpsSense != lp.sense:
313
313
  n = cobj.name
314
314
  cobj = -cobj
315
- cobj.name = n # type: ignore[union-attr]
315
+ cobj.name = n
316
316
  if rename:
317
- constrNames, varNames, cobj.name = lp.normalisedNames() # type: ignore[union-attr]
317
+ constrNames, varNames, cobj.name = lp.normalisedNames()
318
318
  # No need to call self.variables() again, we have just filled self._variables:
319
319
  vs = lp._variables
320
320
  else:
@@ -324,7 +324,7 @@ def writeMPS(
324
324
  model_name = lp.name
325
325
  if rename:
326
326
  model_name = "MODEL"
327
- objName = cobj.name # type: ignore[union-attr]
327
+ objName = cobj.name
328
328
  if not objName:
329
329
  objName = "OBJ"
330
330
 
@@ -346,7 +346,7 @@ def writeMPS(
346
346
  for v in vs:
347
347
  name = varNames[v.name]
348
348
  columns_lines.extend(
349
- writeMPSColumnLines(coefs[name], v, mip, name, cobj, objName) # type: ignore[arg-type]
349
+ writeMPSColumnLines(coefs[name], v, mip, name, cobj, objName)
350
350
  )
351
351
 
352
352
  # right hand side
@@ -382,7 +382,7 @@ def writeMPS(
382
382
  if not rename:
383
383
  return vs
384
384
  else:
385
- return vs, varNames, constrNames, cobj.name # type: ignore[union-attr]
385
+ return vs, varNames, constrNames, cobj.name
386
386
 
387
387
 
388
388
  def writeMPSColumnLines(
@@ -1,6 +1,5 @@
1
1
  #! /usr/bin/env python
2
2
  # PuLP : Python LP Modeler
3
- from __future__ import annotations
4
3
 
5
4
  # Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
6
5
  # Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
@@ -25,6 +24,7 @@ from __future__ import annotations
25
24
  # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
25
  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
26
 
27
+
28
28
  """
29
29
  PuLP is an linear and mixed integer programming modeler written in Python.
30
30
 
@@ -123,12 +123,14 @@ References
123
123
 
124
124
  """
125
125
 
126
+ from __future__ import annotations
127
+
126
128
  from collections import Counter
127
129
  import sys
128
130
  import warnings
129
131
  import math
130
132
  from time import time
131
- from typing import Any, Literal
133
+ from typing import Any, Literal, Optional
132
134
 
133
135
  from .apis import LpSolverDefault, PULP_CBC_CMD
134
136
  from .apis.core import clock
@@ -257,8 +259,21 @@ class LpVariable(LpElement):
257
259
  existence in the objective function and constraints
258
260
  """
259
261
 
262
+ varValue: Optional[float]
263
+ dj: Optional[float]
264
+ lowBound: Optional[float]
265
+ upBound: Optional[float]
266
+ cat: str
267
+ _lowbound_original: Optional[float]
268
+ _upbound_original: Optional[float]
269
+
260
270
  def __init__(
261
- self, name, lowBound=None, upBound=None, cat=const.LpContinuous, e=None
271
+ self,
272
+ name: str,
273
+ lowBound: Optional[float] = None,
274
+ upBound: Optional[float] = None,
275
+ cat: str = const.LpContinuous,
276
+ e=None,
262
277
  ):
263
278
  LpElement.__init__(self, name)
264
279
  self._lowbound_original = self.lowBound = lowBound
@@ -270,6 +285,20 @@ class LpVariable(LpElement):
270
285
  self._lowbound_original = self.lowBound = 0
271
286
  self._upbound_original = self.upBound = 1
272
287
  self.cat = const.LpInteger
288
+ if self.lowBound is not None:
289
+ if not math.isfinite(self.lowBound):
290
+ raise const.PulpError(
291
+ "The lower bound of a variable must be finite, got {}".format(
292
+ self.lowBound
293
+ )
294
+ )
295
+ if self.upBound is not None:
296
+ if not math.isfinite(self.upBound):
297
+ raise const.PulpError(
298
+ "The upper bound of a variable must be finite, got {}".format(
299
+ self.upBound
300
+ )
301
+ )
273
302
  # Code to add a variable to constraints for column based
274
303
  # modelling.
275
304
  if e:
@@ -501,7 +530,7 @@ class LpVariable(LpElement):
501
530
  ):
502
531
  self.varValue = self.upBound
503
532
  elif (
504
- self.lowBound != None
533
+ self.lowBound is not None
505
534
  and self.varValue < self.lowBound
506
535
  and self.varValue >= self.lowBound - eps
507
536
  ):
@@ -515,7 +544,7 @@ class LpVariable(LpElement):
515
544
  def roundedValue(self, eps=1e-5):
516
545
  if (
517
546
  self.cat == const.LpInteger
518
- and self.varValue != None
547
+ and self.varValue is not None
519
548
  and abs(self.varValue - round(self.varValue)) <= eps
520
549
  ):
521
550
  return round(self.varValue)
@@ -523,10 +552,10 @@ class LpVariable(LpElement):
523
552
  return self.varValue
524
553
 
525
554
  def valueOrDefault(self):
526
- if self.varValue != None:
555
+ if self.varValue is not None:
527
556
  return self.varValue
528
- elif self.lowBound != None:
529
- if self.upBound != None:
557
+ elif self.lowBound is not None:
558
+ if self.upBound is not None:
530
559
  if 0 >= self.lowBound and 0 <= self.upBound:
531
560
  return 0
532
561
  else:
@@ -539,7 +568,7 @@ class LpVariable(LpElement):
539
568
  return 0
540
569
  else:
541
570
  return self.lowBound
542
- elif self.upBound != None:
571
+ elif self.upBound is not None:
543
572
  if 0 <= self.upBound:
544
573
  return 0
545
574
  else:
@@ -564,11 +593,11 @@ class LpVariable(LpElement):
564
593
  return True
565
594
 
566
595
  def infeasibilityGap(self, mip=1):
567
- if self.varValue == None:
596
+ if self.varValue is None:
568
597
  raise ValueError("variable value is None")
569
- if self.upBound != None and self.varValue > self.upBound:
598
+ if self.upBound is not None and self.varValue > self.upBound:
570
599
  return self.varValue - self.upBound
571
- if self.lowBound != None and self.varValue < self.lowBound:
600
+ if self.lowBound is not None and self.varValue < self.lowBound:
572
601
  return self.varValue - self.lowBound
573
602
  if (
574
603
  mip
@@ -600,7 +629,7 @@ class LpVariable(LpElement):
600
629
  return self.name + " free"
601
630
  if self.isConstant():
602
631
  return self.name + f" = {self.lowBound:.12g}"
603
- if self.lowBound == None:
632
+ if self.lowBound is None:
604
633
  s = "-inf <= "
605
634
  # Note: XPRESS and CPLEX do not interpret integer variables without
606
635
  # explicit bounds
@@ -731,6 +760,11 @@ class LpAffineExpression(dict):
731
760
  # TODO remove isinstance usage
732
761
  if e is None:
733
762
  e = {}
763
+ # maybe check for constant
764
+ if not math.isfinite(constant):
765
+ raise const.PulpError(
766
+ f"Invalid constant value: {constant}. It must be a finite number."
767
+ )
734
768
  if isinstance(e, (LpAffineExpression, LpConstraint)):
735
769
  # Will not copy the name
736
770
  self.constant = e.constant # type: ignore[has-type]
@@ -832,9 +866,7 @@ class LpAffineExpression(dict):
832
866
  return result
833
867
 
834
868
  def __repr__(self, override_constant: float | None = None):
835
- constant = constant = (
836
- self.constant if override_constant is None else override_constant
837
- )
869
+ constant = self.constant if override_constant is None else override_constant
838
870
  l = [str(self[v]) + "*" + str(v) for v in self.sorted_keys()]
839
871
  l.append(str(constant))
840
872
  s = " + ".join(l)
@@ -1090,6 +1122,8 @@ class LpConstraint:
1090
1122
  self.name = name
1091
1123
  self.constant: float = self.expr.constant # type: ignore[annotation-unchecked]
1092
1124
  if rhs is not None:
1125
+ if not math.isfinite(rhs):
1126
+ raise const.PulpError("Cannot set constraint RHS to NaN/inf values")
1093
1127
  self.constant -= rhs
1094
1128
  self.sense = sense
1095
1129
  self.pi = None
@@ -1893,7 +1927,7 @@ class LpProblem:
1893
1927
 
1894
1928
  def coefficients(self, translation=None):
1895
1929
  coefs = []
1896
- if translation == None:
1930
+ if translation is None:
1897
1931
  for c in self.constraints:
1898
1932
  cst = self.constraints[c]
1899
1933
  coefs.extend([(v.name, c, cst[v]) for v in cst])
@@ -10,7 +10,10 @@ class Examples_DocsTests(unittest.TestCase):
10
10
 
11
11
  this_file = os.path.realpath(__file__)
12
12
  parent_dir = os.path.dirname(this_file)
13
- files = os.listdir(os.path.join(parent_dir, examples_dir))
13
+ try:
14
+ files = os.listdir(os.path.join(parent_dir, examples_dir))
15
+ except FileNotFoundError:
16
+ raise unittest.SkipTest("Examples not found")
14
17
  TMP_dir = "_tmp/"
15
18
  if not os.path.exists(TMP_dir):
16
19
  os.mkdir(TMP_dir)
@@ -55,11 +55,11 @@ class GurobiEnvTests(unittest.TestCase):
55
55
  solver.close()
56
56
  check_dummy_env()
57
57
 
58
- @unittest.skipUnless(
59
- is_single_use_license(),
60
- "this test is only expected to pass with a single-use license",
61
- )
62
58
  def test_gp_env_no_close(self):
59
+ if not is_single_use_license():
60
+ raise unittest.SkipTest(
61
+ "this test is only expected to pass with a single-use license"
62
+ )
63
63
  # Not closing results in an error for a single use license.
64
64
  with gp.Env(params=self.env_options) as env:
65
65
  prob = generate_lp()
@@ -82,16 +82,16 @@ class GurobiEnvTests(unittest.TestCase):
82
82
 
83
83
  check_dummy_env()
84
84
 
85
- @unittest.skipUnless(
86
- is_single_use_license(),
87
- "this test is only expected to pass with a single-use license",
88
- )
89
85
  def test_backward_compatibility(self):
90
86
  """
91
87
  Backward compatibility check as previously the environment was not being
92
88
  freed. On a single-use license this passes (fails to initialise a dummy
93
89
  env).
94
90
  """
91
+ if not is_single_use_license():
92
+ raise unittest.SkipTest(
93
+ "this test is only expected to pass with a single-use license"
94
+ )
95
95
  solver = GUROBI(msg=False, **self.options)
96
96
  prob = generate_lp()
97
97
  prob.solve(solver)
@@ -122,16 +122,16 @@ class GurobiEnvTests(unittest.TestCase):
122
122
  solver2.close()
123
123
  check_dummy_env()
124
124
 
125
- @unittest.skipUnless(
126
- is_single_use_license(),
127
- "this test is only expected to pass with a single-use license",
128
- )
129
125
  def test_leak(self):
130
126
  """
131
127
  Check that we cannot initialise environments after a memory leak. On a
132
128
  single-use license this passes (fails to initialise a dummy env with a
133
129
  memory leak).
134
130
  """
131
+ if not is_single_use_license():
132
+ raise unittest.SkipTest(
133
+ "this test is only expected to pass with a single-use license"
134
+ )
135
135
  solver = GUROBI(msg=False, **self.options)
136
136
  prob = generate_lp()
137
137
  prob.solve(solver)