diff --git a/pulp/apis/cplex_api.py b/pulp/apis/cplex_api.py index d4651a00..4e695577 100644 --- a/pulp/apis/cplex_api.py +++ b/pulp/apis/cplex_api.py @@ -339,6 +339,8 @@ def buildSolverModel(self, lp): lp.solverModel.objective.set_sense( lp.solverModel.objective.sense.maximize ) + if lp.objective is None: + raise PulpSolverError("No objective set") obj = [float(lp.objective.get(var, 0.0)) for var in model_variables] def cplex_var_lb(var): @@ -372,15 +374,16 @@ def cplex_var_types(var): rows = [] senses = [] rhs = [] - rownames = [] - for name, constraint in lp.constraints.items(): + rownames = list(lp.constraints.keys()) + for constraint in lp.constraints.values(): # build the expression - expr = [(var.name, float(coeff)) for var, coeff in constraint.items()] - if not expr: + if len(constraint) == 0: # if the constraint is empty rows.append(([], [])) else: - rows.append(list(zip(*expr))) + expr1 = [var.name for var in constraint.keys()] + expr2 = [float(coeff) for coeff in constraint.values()] + rows.append((expr1, expr2)) if constraint.sense == constants.LpConstraintLE: senses.append("L") elif constraint.sense == constants.LpConstraintGE: @@ -389,7 +392,6 @@ def cplex_var_types(var): senses.append("E") else: raise PulpSolverError("Detected an invalid constraint type") - rownames.append(name) rhs.append(float(-constraint.constant)) lp.solverModel.linear_constraints.add( lin_expr=rows, senses=senses, rhs=rhs, names=rownames diff --git a/pulp/pulp.py b/pulp/pulp.py index a05a964f..8cc7124a 100644 --- a/pulp/pulp.py +++ b/pulp/pulp.py @@ -1,6 +1,6 @@ #! /usr/bin/env python # PuLP : Python LP Modeler - +from __future__ import annotations # Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) # Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) @@ -128,6 +128,7 @@ import warnings import math from time import time +from typing import Any from .apis import LpSolverDefault, PULP_CBC_CMD from .apis.core import clock @@ -218,7 +219,7 @@ def __neg__(self): def __pos__(self): return self - def __bool__(self): + def __bool__(self) -> bool: return True def __add__(self, other): @@ -257,13 +258,13 @@ def __eq__(self, other): def __ne__(self, other): if isinstance(other, LpVariable): return self.name is not other.name - elif isinstance(other, LpAffineExpression): + elif isinstance(other, (LpAffineExpression, LpConstraint)): if other.isAtomic(): return self is not other.atom() else: - return 1 + return True else: - return 1 + return True class LpVariable(LpElement): @@ -593,13 +594,13 @@ def asCplexLpAffineExpression(self, name, constant=1): def __ne__(self, other): if isinstance(other, LpElement): return self.name is not other.name - elif isinstance(other, LpAffineExpression): + elif isinstance(other, (LpAffineExpression, LpConstraint)): if other.isAtomic(): return self is not other.atom() else: - return 1 + return True else: - return 1 + return True def __bool__(self): return bool(self.roundedValue()) @@ -685,33 +686,32 @@ class LpAffineExpression(_DICT_TYPE): """ constant: float - name: str # to remove illegal characters from the names trans = maketrans("-+[] ", "_____") - def setName(self, name): + @property + def name(self) -> str | None: + return self.__name + + @name.setter + def name(self, name: str | None): if name: self.__name = str(name).translate(self.trans) else: self.__name = None - def getName(self): - return self.__name - - name = property(fget=getName, fset=setName) - - def __init__(self, e=None, constant=0, name=None): + def __init__(self, e=None, constant: float = 0.0, name: str | None = None): self.name = name # TODO remove isinstance usage if e is None: e = {} - if isinstance(e, LpAffineExpression): + if isinstance(e, (LpAffineExpression, LpConstraint)): # Will not copy the name self.constant = e.constant - super().__init__(list(e.items())) + super().__init__(e.items()) elif isinstance(e, dict): self.constant = constant - super().__init__(list(e.items())) + super().__init__(e.items()) elif isinstance(e, Iterable): self.constant = constant super().__init__(e) @@ -725,20 +725,20 @@ def __init__(self, e=None, constant=0, name=None): # Proxy functions for variables def isAtomic(self): - return len(self) == 1 and self.constant == 0 and list(self.values())[0] == 1 + return len(self) == 1 and self.constant == 0 and next(iter(self.values())) == 1 def isNumericalConstant(self): return len(self) == 0 def atom(self): - return list(self.keys())[0] + return next(iter(self.keys())) # Functions on expressions def __bool__(self): return (float(self.constant) != 0.0) or (len(self) > 0) - def value(self): + def value(self) -> float | None: s = self.constant for v, x in self.items(): if v.varValue is None: @@ -746,17 +746,15 @@ def value(self): s += v.varValue * x return s - def valueOrDefault(self): + def valueOrDefault(self) -> float: s = self.constant for v, x in self.items(): s += v.valueOrDefault() * x return s - def addterm(self, key, value): - y = self.get(key, 0) - if y: - y += value - self[key] = y + def addterm(self, key: LpElement, value: float | int): + if key in self: + self[key] += value else: self[key] = value @@ -796,13 +794,12 @@ def __str__(self, constant=1): s = "0" return s - def sorted_keys(self): + def sorted_keys(self) -> list[LpElement]: """ returns the list of keys sorted by name """ - result = [(v.name, v) for v in self.keys()] - result.sort() - result = [v for _, v in result] + result = list(self.keys()) + result.sort(key=lambda v: v.name) return result def __repr__(self): @@ -816,7 +813,7 @@ def _count_characters(line): # counts the characters in a list of strings return sum(len(t) for t in line) - def asCplexVariablesOnly(self, name): + def asCplexVariablesOnly(self, name: str): """ helper for asCplexLpAffineExpression """ @@ -847,7 +844,7 @@ def asCplexVariablesOnly(self, name): line += [term] return result, line - def asCplexLpAffineExpression(self, name, constant=1): + def asCplexLpAffineExpression(self, name: str, constant=1): """ returns a string that represents the Affine Expression in lp format """ @@ -884,7 +881,7 @@ def addInPlace(self, other, sign=1): if isinstance(other, LpElement): # if a variable, we add it to the dictionary self.addterm(other, sign) - elif isinstance(other, LpAffineExpression): + elif isinstance(other, (LpAffineExpression, LpConstraint)): # if an expression, we add each variable and the constant self.constant += other.constant * sign for v, x in other.items(): @@ -939,7 +936,7 @@ def __isub__(self, other): def __mul__(self, other): e = self.emptyCopy() - if isinstance(other, LpAffineExpression): + if isinstance(other, (LpAffineExpression, LpConstraint)): e.constant = self.constant * other.constant if len(other): if len(self): @@ -969,7 +966,9 @@ def __rmul__(self, other): return self * other def __div__(self, other): - if isinstance(other, LpAffineExpression) or isinstance(other, LpVariable): + if isinstance(other, (LpAffineExpression, LpConstraint)) or isinstance( + other, LpVariable + ): if len(other): raise TypeError( "Expressions cannot be divided by a non-constant expression" @@ -993,7 +992,7 @@ def __rdiv__(self, other): "Expressions cannot be divided by a non-constant expression" ) c = self.constant - if isinstance(other, LpAffineExpression): + if isinstance(other, (LpAffineExpression, LpConstraint)): e.constant = other.constant / c for v, x in other.items(): e[v] = x / c @@ -1003,14 +1002,23 @@ def __rdiv__(self, other): e.constant = other / c return e - def __le__(self, other): - return LpConstraint(self - other, const.LpConstraintLE) + def __le__(self, other) -> LpConstraint: + if isinstance(other, (int, float)): + return LpConstraint(self, const.LpConstraintLE, rhs=other) + else: + return LpConstraint(self - other, const.LpConstraintLE) - def __ge__(self, other): - return LpConstraint(self - other, const.LpConstraintGE) + def __ge__(self, other) -> LpConstraint: + if isinstance(other, (int, float)): + return LpConstraint(self, const.LpConstraintGE, rhs=other) + else: + return LpConstraint(self - other, const.LpConstraintGE) - def __eq__(self, other): - return LpConstraint(self - other, const.LpConstraintEQ) + def __eq__(self, other) -> LpConstraint: + if isinstance(other, (int, float)): + return LpConstraint(self, const.LpConstraintEQ, rhs=other) + else: + return LpConstraint(self - other, const.LpConstraintEQ) def toDict(self): """ @@ -1025,7 +1033,7 @@ def toDict(self): to_dict = toDict -class LpConstraint(LpAffineExpression): +class LpConstraint: """An LP constraint""" def __init__(self, e=None, sense=const.LpConstraintEQ, name=None, rhs=None): @@ -1035,7 +1043,9 @@ def __init__(self, e=None, sense=const.LpConstraintEQ, name=None, rhs=None): :param name: identifying string :param rhs: numerical value of constraint target """ - LpAffineExpression.__init__(self, e, name=name) + self.expr = ( + e if isinstance(e, LpAffineExpression) else LpAffineExpression(e, name=name) + ) if rhs is not None: self.constant -= rhs self.sense = sense @@ -1056,7 +1066,7 @@ def getUb(self): return None def __str__(self): - s = LpAffineExpression.__str__(self, 0) + s = self.expr.__str__(0) if self.sense is not None: s += " " + const.LpConstraintSenses[self.sense] + " " + str(-self.constant) return s @@ -1065,14 +1075,14 @@ def asCplexLpConstraint(self, name): """ Returns a constraint as a string """ - result, line = self.asCplexVariablesOnly(name) - if not list(self.keys()): + result, line = self.expr.asCplexVariablesOnly(name) + if len(self.keys()) == 0: line += ["0"] c = -self.constant if c == 0: c = 0 # Supress sign term = f" {const.LpConstraintSenses[self.sense]} {c:.12g}" - if self._count_characters(line) + len(term) > const.LpCplexLPLineSize: + if self.expr._count_characters(line) + len(term) > const.LpCplexLPLineSize: result += ["".join(line)] line = [term] else: @@ -1089,7 +1099,7 @@ def changeRHS(self, RHS): self.modified = True def __repr__(self): - s = LpAffineExpression.__repr__(self) + s = repr(self.expr) if self.sense is not None: s += " " + const.LpConstraintSenses[self.sense] + " 0" return s @@ -1109,16 +1119,16 @@ def addInPlace(self, other, sign=1): """ if isinstance(other, LpConstraint): if self.sense * other.sense >= 0: - LpAffineExpression.addInPlace(self, other, 1) + self.expr.addInPlace(other.expr, 1) self.sense |= other.sense else: - LpAffineExpression.addInPlace(self, other, -1) + self.expr.addInPlace(other.expr, -1) self.sense |= -other.sense elif isinstance(other, list): for e in other: self.addInPlace(e, sign) else: - LpAffineExpression.addInPlace(self, other, sign) + self.expr.addInPlace(other, sign) # raise TypeError, "Constraints and Expressions cannot be added" return self @@ -1126,7 +1136,8 @@ def subInPlace(self, other): return self.addInPlace(other, -1) def __neg__(self): - c = LpAffineExpression.__neg__(self) + c = self.copy() + c.expr = -c.expr c.sense = -c.sense return c @@ -1144,42 +1155,51 @@ def __rsub__(self, other): def __mul__(self, other): if isinstance(other, LpConstraint): - c = LpAffineExpression.__mul__(self, other) + c = self.copy() + c.expr = c.expr * other if c.sense == 0: c.sense = other.sense elif other.sense != 0: c.sense *= other.sense return c else: - return LpAffineExpression.__mul__(self, other) + c = self.copy() + c.expr = c.expr * other + return c def __rmul__(self, other): return self * other def __div__(self, other): if isinstance(other, LpConstraint): - c = LpAffineExpression.__div__(self, other) + c = self.copy() + c.expr = c.expr / other if c.sense == 0: c.sense = other.sense elif other.sense != 0: c.sense *= other.sense return c else: - return LpAffineExpression.__mul__(self, other) + c = self.copy() + c.expr = c.expr / other + return c def __rdiv__(self, other): if isinstance(other, LpConstraint): - c = LpAffineExpression.__rdiv__(self, other) + c = self.copy() + c.expr = c.expr / other if c.sense == 0: c.sense = other.sense elif other.sense != 0: c.sense *= other.sense return c else: - return LpAffineExpression.__mul__(self, other) + c = self.copy() + c.expr = c.expr / other + return def valid(self, eps=0): - val = self.value() + val = self.expr.value() if self.sense == const.LpConstraintEQ: return abs(val) <= eps else: @@ -1204,7 +1224,7 @@ def toDict(self): pi=self.pi, constant=self.constant, name=self.name, - coefficients=LpAffineExpression.toDict(self), + coefficients=self.expr.toDict(), ) @classmethod @@ -1226,6 +1246,46 @@ def fromDict(cls, _dict): from_dict = fromDict + @property + def name(self): + return self.expr.name + + @name.setter + def name(self, v): + self.expr.name = v + + @property + def constant(self): + return self.expr.constant + + @constant.setter + def constant(self, v): + self.expr.constant = v + + def isNumericalConstant(self): + return self.expr.isNumericalConstant() + + def __len__(self): + return len(self.expr) + + def __iter__(self): + return iter(self.expr) + + def __getitem__(self, key: LpElement): + return self.expr[key] + + def get(self, key: LpVariable, default: float | None) -> float | None: + return self.expr.get(key, default) + + def keys(self): + return self.expr.keys() + + def values(self): + return self.expr.values() + + def items(self): + return self.expr.items() + class LpFractionConstraint(LpConstraint): """ @@ -1280,7 +1340,7 @@ def findLHSValue(self): else: raise ZeroDivisionError - def makeElasticSubProblem(self, *args, **kwargs): + def makeElasticSubProblem(self, *args: Any, **kwargs: Any): """ Builds an elastic subproblem by adding variables and splitting the hard constraint @@ -1304,10 +1364,10 @@ def addVariable(self, var, coeff): Adds a variable to the constraint with the activity coeff """ - self.constraint.addterm(var, coeff) + self.constraint.expr.addterm(var, coeff) def value(self): - return self.constraint.value() + return self.constraint.expr.value() class LpProblem: @@ -1328,8 +1388,8 @@ def __init__(self, name="NoName", sense=const.LpMinimize): if " " in name: warnings.warn("Spaces are not permitted in the name. Converted to '_'") name = name.replace(" ", "_") - self.objective = None - self.constraints = _DICT_TYPE() + self.objective: None | LpAffineExpression = None + self.constraints = _DICT_TYPE[str, LpConstraint]() self.name = name self.sense = sense self.sos1 = {} @@ -1338,11 +1398,14 @@ def __init__(self, name="NoName", sense=const.LpMinimize): self.sol_status = const.LpSolutionNoSolutionFound self.noOverlap = 1 self.solver = None + self.solverModel = None self.modifiedVariables = [] self.modifiedConstraints = [] self.resolveOK = False - self._variables = [] - self._variable_ids = {} # old school using dict.keys() for a set + self._variables: list[LpVariable] = [] + self._variable_ids: dict[int, LpVariable] = ( + {} + ) # old school using dict.keys() for a set self.dummyVar = None self.solutionTime = 0 self.solutionCpuTime = 0 @@ -1394,7 +1457,7 @@ def deepcopy(self): lpcopy = LpProblem(name=self.name, sense=self.sense) if self.objective is not None: lpcopy.objective = self.objective.copy() - lpcopy.constraints = {} + lpcopy.constraints = _DICT_TYPE[str, LpConstraint]() for k, v in self.constraints.items(): lpcopy.constraints[k] = v.copy() lpcopy.sos1 = self.sos1.copy() @@ -1417,6 +1480,7 @@ def toDict(self): "Duplicated names found in variables:\nto export the model, variable names need to be unique" ) self.fixObjective() + assert self.objective is not None variables = self.variables() return dict( objective=dict( @@ -1545,7 +1609,7 @@ def roundSolution(self, epsInt=1e-5, eps=1e-7): def unusedConstraintName(self): self.lastUnused += 1 - while 1: + while True: s = "_C%d" % self.lastUnused if s not in self.constraints: break @@ -1571,7 +1635,7 @@ def infeasibilityGap(self, mip=1): gap = max(abs(c.value()), gap) return gap - def addVariable(self, variable): + def addVariable(self, variable: LpVariable): """ Adds a variable to the problem before a constraint is added @@ -1581,7 +1645,7 @@ def addVariable(self, variable): self._variables.append(variable) self._variable_ids[variable.hash] = variable - def addVariables(self, variables): + def addVariables(self, variables: Iterable[LpVariable]): """ Adds variables to the problem before a constraint is added @@ -1590,7 +1654,7 @@ def addVariables(self, variables): for v in variables: self.addVariable(v) - def variables(self): + def variables(self) -> list[LpVariable]: """ Returns the problem variables @@ -1598,9 +1662,9 @@ def variables(self): :rtype: (list, :py:class:`LpVariable`) """ if self.objective: - self.addVariables(list(self.objective.keys())) + self.addVariables(self.objective.keys()) for c in self.constraints.values(): - self.addVariables(list(c.keys())) + self.addVariables(c.keys()) self._variables.sort(key=lambda v: v.name) return self._variables @@ -1609,7 +1673,7 @@ def variablesDict(self): if self.objective: for v in self.objective: variables[v.name] = v - for c in list(self.constraints.values()): + for c in self.constraints.values(): for v in c: variables[v.name] = v return variables @@ -1617,7 +1681,7 @@ def variablesDict(self): def add(self, constraint, name=None): self.addConstraint(constraint, name) - def addConstraint(self, constraint, name=None): + def addConstraint(self, constraint: LpConstraint, name=None): if not isinstance(constraint, LpConstraint): raise TypeError("Can only add LpConstraint objects") if name: @@ -1640,7 +1704,7 @@ def addConstraint(self, constraint, name=None): print("Warning: overlapping constraint names:", name) self.constraints[name] = constraint self.modifiedConstraints.append(constraint) - self.addVariables(list(constraint.keys())) + self.addVariables(constraint.keys()) def setObjective(self, obj): """ @@ -1854,17 +1918,17 @@ def get_dummyVar(self): def fixObjective(self): if self.objective is None: - self.objective = 0 - wasNone = 1 + self.objective = LpAffineExpression(0) + wasNone = True else: - wasNone = 0 - if not isinstance(self.objective, LpAffineExpression): - self.objective = LpAffineExpression(self.objective) + wasNone = False + if self.objective.isNumericalConstant(): dummyVar = self.get_dummyVar() self.objective += dummyVar else: dummyVar = None + return wasNone, dummyVar def restoreObjective(self, wasNone, dummyVar): @@ -2255,7 +2319,15 @@ def isViolated(self): return False -def lpSum(vector): +def lpSum( + vector: ( + Iterable[LpAffineExpression] + | Iterable[tuple[LpElement, float]] + | int + | float + | LpElement + ) +): """ Calculate the sum of a list of linear expressions