deb-python-pulp/src/pulp/pulp.py

2301 lines
76 KiB
Python

#! /usr/bin/env python
# PuLP : Python LP Modeler
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id: pulp.py 1791 2008-04-23 22:54:34Z smit023 $
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# 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], and GUROBI[4] to solve linear
problems.
See the examples directory for examples.
PuLP requires Python >= 2.5.
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/
Use LpVariable() to create new variables. To create a variable 0 <= x <= 3
>>> x = LpVariable("x", 0, 3)
To create a variable 0 <= y <= 1
>>> y = LpVariable("y", 0, 1)
Use LpProblem() to create new problems. Create "myProblem"
>>> prob = LpProblem("myProblem", LpMinimize)
Combine variables to create expressions and constraints and add them to the
problem.
>>> prob += x + y <= 2
If you add an expression (not a constraint), it will
become the objective.
>>> prob += -4*x + y
Choose a solver and solve the problem. ex:
>>> status = prob.solve(GLPK(msg = 0))
Display the status of the solution
>>> LpStatus[status]
'Optimal'
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
a1x1+a2x2 ...anxn (<=, =, >=) b
- LpConstraintVar -- Used to construct a 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, 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
Comments, bug reports, patches and suggestions are welcome.
pulp-or-discuss@googlegroups.com
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/
"""
import types
import string
import itertools
from .constants import *
from .solvers import *
from collections import Iterable
import logging
log = logging.getLogger(__name__)
try: # allow Python 2/3 compatibility
maketrans = str.maketrans
except AttributeError:
from string import maketrans
_DICT_TYPE = dict
if sys.platform not in ['cli']:
# iron python does not like an OrderedDict
try:
from odict import OrderedDict
_DICT_TYPE = OrderedDict
except ImportError:
pass
try:
#python 2.7 or 3.1
from collections import OrderedDict
_DICT_TYPE = OrderedDict
except ImportError:
pass
def setConfigInformation(**keywords):
"""
set the data in the configuration file
at the moment will only edit things in [locations]
the keyword value pairs come from the keywords dictionary
"""
#TODO: extend if we ever add another section in the config file
#read the old configuration
config = ConfigParser.SafeConfigParser()
config.read(config_filename)
#set the new keys
for (key,val) in keywords.items():
config.set("locations",key,val)
#write the new configuration
fp = open(config_filename,"w")
config.write(fp)
fp.close()
# Default solver selection
if PULP_CBC_CMD().available():
LpSolverDefault = PULP_CBC_CMD()
elif GLPK_CMD().available():
LpSolverDefault = GLPK_CMD()
elif COIN_CMD().available():
LpSolverDefault = COIN_CMD()
else:
LpSolverDefault = None
class LpElement(object):
"""Base class for LpVariable and LpConstraintVar
"""
#to remove illegal characters from the names
trans = maketrans("-+[] ->/","________")
def setName(self,name):
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, name):
self.name = name
# self.hash MUST be different for each variable
# else dict() will call the comparison operators that are overloaded
self.hash = id(self)
self.modified = True
def __hash__(self):
return self.hash
def __str__(self):
return self.name
def __repr__(self):
return self.name
def __neg__(self):
return - LpAffineExpression(self)
def __pos__(self):
return self
def __bool__(self):
return 1
def __add__(self, other):
return LpAffineExpression(self) + other
def __radd__(self, other):
return LpAffineExpression(self) + other
def __sub__(self, other):
return LpAffineExpression(self) - other
def __rsub__(self, other):
return other - LpAffineExpression(self)
def __mul__(self, other):
return LpAffineExpression(self) * other
def __rmul__(self, other):
return LpAffineExpression(self) * other
def __div__(self, other):
return LpAffineExpression(self)/other
def __rdiv__(self, other):
raise TypeError("Expressions cannot be divided by a variable")
def __le__(self, other):
return LpAffineExpression(self) <= other
def __ge__(self, other):
return LpAffineExpression(self) >= other
def __eq__(self, other):
return LpAffineExpression(self) == other
def __ne__(self, other):
if isinstance(other, LpVariable):
return self.name is not other.name
elif isinstance(other, LpAffineExpression):
if other.isAtomic():
return self is not other.atom()
else:
return 1
else:
return 1
class LpVariable(LpElement):
"""
This class models an LP Variable with the specified associated parameters
:param name: The name of the variable used in the output .lp file
:param lowBound: The lower bound on this variable's range.
Default is negative infinity
:param upBound: The upper bound on this variable's range.
Default is positive infinity
:param cat: The category this variable is in, Integer, Binary or
Continuous(default)
:param e: Used for column based modelling: relates to the variable's
existence in the objective function and constraints
"""
def __init__(self, name, lowBound = None, upBound = None,
cat = LpContinuous, e = None):
LpElement.__init__(self,name)
self.lowBound = lowBound
self.upBound = upBound
self.cat = cat
self.varValue = None
self.dj = None
self.init = 0
#code to add a variable to constraints for column based
# modelling
if cat == LpBinary:
self.lowBound = 0
self.upBound = 1
self.cat = LpInteger
if e:
self.add_expression(e)
def add_expression(self,e):
self.expression = e
self.addVariableToConstraints(e)
def matrix(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous,
indexStart = []):
if not isinstance(indexs, tuple): indexs = (indexs,)
if "%" not in name: name += "_%s" * len(indexs)
index = indexs[0]
indexs = indexs[1:]
if len(indexs) == 0:
return [LpVariable(name % tuple(indexStart + [i]),
lowBound, upBound, cat)
for i in index]
else:
return [LpVariable.matrix(name, indexs, lowBound,
upBound, cat, indexStart + [i])
for i in index]
matrix = classmethod(matrix)
def dicts(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous,
indexStart = []):
"""
Creates a dictionary of LP variables
This function creates a dictionary of LP Variables with the specified
associated parameters.
:param name: The prefix to the name of each LP variable created
:param indexs: A list of strings of the keys to the dictionary of LP
variables, and the main part of the variable name itself
:param lowbound: The lower bound on these variables' range. Default is
negative infinity
:param upBound: The upper bound on these variables' range. Default is
positive infinity
:param cat: The category these variables are in, Integer or
Continuous(default)
:return: A dictionary of LP Variables
"""
if not isinstance(indexs, tuple): indexs = (indexs,)
if "%" not in name: name += "_%s" * len(indexs)
index = indexs[0]
indexs = indexs[1:]
d = {}
if len(indexs) == 0:
for i in index:
d[i] = LpVariable(name % tuple(indexStart + [str(i)]), lowBound, upBound, cat)
else:
for i in index:
d[i] = LpVariable.dicts(name, indexs, lowBound, upBound, cat, indexStart + [i])
return d
dicts = classmethod(dicts)
def dict(self, name, indexs, lowBound = None, upBound = None, cat = LpContinuous):
if not isinstance(indexs, tuple): indexs = (indexs,)
if "%" not in name: name += "_%s" * len(indexs)
lists = indexs
if len(indexs)>1:
# Cartesian product
res = []
while len(lists):
first = lists[-1]
nres = []
if res:
if first:
for f in first:
nres.extend([[f]+r for r in res])
else:
nres = res
res = nres
else:
res = [[f] for f in first]
lists = lists[:-1]
index = [tuple(r) for r in res]
elif len(indexs) == 1:
index = indexs[0]
else:
return {}
d = {}
for i in index:
d[i] = self(name % i, lowBound, upBound, cat)
return d
dict = classmethod(dict)
def getLb(self):
return self.lowBound
def getUb(self):
return self.upBound
def bounds(self, low, up):
self.lowBound = low
self.upBound = up
self.modified = True
def positive(self):
self.bounds(0, None)
def value(self):
return self.varValue
def round(self, epsInt = 1e-5, eps = 1e-7):
if self.varValue is not None:
if self.upBound != None and self.varValue > self.upBound and self.varValue <= self.upBound + eps:
self.varValue = self.upBound
elif self.lowBound != None and self.varValue < self.lowBound and self.varValue >= self.lowBound - eps:
self.varValue = self.lowBound
if self.cat == LpInteger and abs(round(self.varValue) - self.varValue) <= epsInt:
self.varValue = round(self.varValue)
def roundedValue(self, eps = 1e-5):
if self.cat == LpInteger and self.varValue != None \
and abs(self.varValue - round(self.varValue)) <= eps:
return round(self.varValue)
else:
return self.varValue
def valueOrDefault(self):
if self.varValue != None:
return self.varValue
elif self.lowBound != None:
if self.upBound != None:
if 0 >= self.lowBound and 0 <= self.upBound:
return 0
else:
if self.lowBound >= 0:
return self.lowBound
else:
return self.upBound
else:
if 0 >= self.lowBound:
return 0
else:
return self.lowBound
elif self.upBound != None:
if 0 <= self.upBound:
return 0
else:
return self.upBound
else:
return 0
def valid(self, eps):
if self.varValue == None: return False
if self.upBound != None and self.varValue > self.upBound + eps:
return False
if self.lowBound != None and self.varValue < self.lowBound - eps:
return False
if self.cat == LpInteger and abs(round(self.varValue) - self.varValue) > eps:
return False
return True
def infeasibilityGap(self, mip = 1):
if self.varValue == None: raise ValueError("variable value is None")
if self.upBound != None and self.varValue > self.upBound:
return self.varValue - self.upBound
if self.lowBound != None and self.varValue < self.lowBound:
return self.varValue - self.lowBound
if mip and self.cat == LpInteger and round(self.varValue) - self.varValue != 0:
return round(self.varValue) - self.varValue
return 0
def isBinary(self):
return self.cat == LpInteger and self.lowBound == 0 and self.upBound == 1
def isInteger(self):
return self.cat == LpInteger
def isFree(self):
return self.lowBound == None and self.upBound == None
def isConstant(self):
return self.lowBound != None and self.upBound == self.lowBound
def isPositive(self):
return self.lowBound == 0 and self.upBound == None
def asCplexLpVariable(self):
if self.isFree(): return self.name + " free"
if self.isConstant(): return self.name + " = %.12g" % self.lowBound
if self.lowBound == None:
s= "-inf <= "
# Note: XPRESS and CPLEX do not interpret integer variables without
# explicit bounds
elif (self.lowBound == 0 and self.cat == LpContinuous):
s = ""
else:
s= "%.12g <= " % self.lowBound
s += self.name
if self.upBound != None:
s+= " <= %.12g" % self.upBound
return s
def asCplexLpAffineExpression(self, name, constant = 1):
return LpAffineExpression(self).asCplexLpAffineExpression(name, constant)
def __ne__(self, other):
if isinstance(other, LpElement):
return self.name is not other.name
elif isinstance(other, LpAffineExpression):
if other.isAtomic():
return self is not other.atom()
else:
return 1
else:
return 1
def addVariableToConstraints(self,e):
"""adds a variable to the constraints indicated by
the LpConstraintVars in e
"""
for constraint, coeff in e.items():
constraint.addVariable(self,coeff)
def setInitialValue(self,val):
"""sets the initial value of the Variable to val
may of may not be supported by the solver
"""
raise NotImplementedError
class LpAffineExpression(_DICT_TYPE):
"""
A linear combination of :class:`LpVariables<LpVariable>`.
Can be initialised with the following:
#. e = None: an empty Expression
#. e = dict: gives an expression with the values being the coefficients of the keys (order of terms is undetermined)
#. e = list or generator of 2-tuples: equivalent to dict.items()
#. e = LpElement: an expression of length 1 with the coefficient 1
#. e = other: the constant is initialised as e
Examples:
>>> f=LpAffineExpression(LpElement('x'))
>>> f
1*x + 0
>>> x_name = ['x_0', 'x_1', 'x_2']
>>> x = [LpVariable(x_name[i], lowBound = 0, upBound = 10) for i in range(3) ]
>>> c = LpAffineExpression([ (x[0],1), (x[1],-3), (x[2],4)])
>>> c
1*x_0 + -3*x_1 + 4*x_2 + 0
"""
#to remove illegal characters from the names
trans = maketrans("-+[] ","_____")
def setName(self,name):
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):
self.name = name
#TODO remove isinstance usage
if e is None:
e = {}
if isinstance(e, LpAffineExpression):
# Will not copy the name
self.constant = e.constant
super(LpAffineExpression, self).__init__(list(e.items()))
elif isinstance(e, dict):
self.constant = constant
super(LpAffineExpression, self).__init__(list(e.items()))
elif isinstance(e, Iterable):
self.constant = constant
super(LpAffineExpression, self).__init__(e)
elif isinstance(e,LpElement):
self.constant = 0
super(LpAffineExpression, self).__init__( [(e, 1)])
else:
self.constant = e
super(LpAffineExpression, self).__init__()
# Proxy functions for variables
def isAtomic(self):
return len(self) == 1 and self.constant == 0 and list(self.values())[0] == 1
def isNumericalConstant(self):
return len(self) == 0
def atom(self):
return list(self.keys())[0]
# Functions on expressions
def __bool__(self):
return (float(self.constant) != 0.0) or (len(self) > 0)
def value(self):
s = self.constant
for v,x in self.items():
if v.varValue is None:
return None
s += v.varValue * x
return s
def valueOrDefault(self):
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
else:
self[key] = value
def emptyCopy(self):
return LpAffineExpression()
def copy(self):
"""Make a copy of self except the name which is reset"""
# Will not copy the name
return LpAffineExpression(self)
def __str__(self, constant = 1):
s = ""
for v in self.sorted_keys():
val = self[v]
if val<0:
if s != "": s += " - "
else: s += "-"
val = -val
elif s != "": s += " + "
if val == 1: s += str(v)
else: s += str(val) + "*" + str(v)
if constant:
if s == "":
s = str(self.constant)
else:
if self.constant < 0: s += " - " + str(-self.constant)
elif self.constant > 0: s += " + " + str(self.constant)
elif s == "":
s = "0"
return s
def sorted_keys(self):
"""
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]
return result
def __repr__(self):
l = [str(self[v]) + "*" + str(v)
for v in self.sorted_keys()]
l.append(str(self.constant))
s = " + ".join(l)
return s
@staticmethod
def _count_characters(line):
#counts the characters in a list of strings
return sum(len(t) for t in line)
def asCplexVariablesOnly(self, name):
"""
helper for asCplexLpAffineExpression
"""
result = []
line = ["%s:" % name]
notFirst = 0
variables = self.sorted_keys()
for v in variables:
val = self[v]
if val < 0:
sign = " -"
val = -val
elif notFirst:
sign = " +"
else:
sign = ""
notFirst = 1
if val == 1:
term = "%s %s" %(sign, v.name)
else:
term = "%s %.12g %s" % (sign, val, v.name)
if self._count_characters(line) + len(term) > LpCplexLPLineSize:
result += ["".join(line)]
line = [term]
else:
line += [term]
return result, line
def asCplexLpAffineExpression(self, name, constant = 1):
"""
returns a string that represents the Affine Expression in lp format
"""
#refactored to use a list for speed in iron python
result, line = self.asCplexVariablesOnly(name)
if not self:
term = " %s" % self.constant
else:
term = ""
if constant:
if self.constant < 0:
term = " - %s" % (-self.constant)
elif self.constant > 0:
term = " + %s" % self.constant
if self._count_characters(line) + len(term) > LpCplexLPLineSize:
result += ["".join(line)]
line = [term]
else:
line += [term]
result += ["".join(line)]
result = "%s\n" % "\n".join(result)
return result
def addInPlace(self, other):
if other is 0: return self
if other is None: return self
if isinstance(other,LpElement):
self.addterm(other, 1)
elif isinstance(other,LpAffineExpression):
self.constant += other.constant
for v,x in other.items():
self.addterm(v, x)
elif isinstance(other,dict):
for e in other.values():
self.addInPlace(e)
elif (isinstance(other,list)
or isinstance(other, Iterable)):
for e in other:
self.addInPlace(e)
else:
self.constant += other
return self
def subInPlace(self, other):
if other is 0: return self
if other is None: return self
if isinstance(other,LpElement):
self.addterm(other, -1)
elif isinstance(other,LpAffineExpression):
self.constant -= other.constant
for v,x in other.items():
self.addterm(v, -x)
elif isinstance(other,dict):
for e in other.values():
self.subInPlace(e)
elif (isinstance(other,list)
or isinstance(other, Iterable)):
for e in other:
self.subInPlace(e)
else:
self.constant -= other
return self
def __neg__(self):
e = self.emptyCopy()
e.constant = - self.constant
for v,x in self.items():
e[v] = - x
return e
def __pos__(self):
return self
def __add__(self, other):
return self.copy().addInPlace(other)
def __radd__(self, other):
return self.copy().addInPlace(other)
def __iadd__(self, other):
return self.addInPlace(other)
def __sub__(self, other):
return self.copy().subInPlace(other)
def __rsub__(self, other):
return (-self).addInPlace(other)
def __isub__(self, other):
return (self).subInPlace(other)
def __mul__(self, other):
e = self.emptyCopy()
if isinstance(other,LpAffineExpression):
e.constant = self.constant * other.constant
if len(other):
if len(self):
raise TypeError("Non-constant expressions cannot be multiplied")
else:
c = self.constant
if c != 0:
for v,x in other.items():
e[v] = c * x
else:
c = other.constant
if c != 0:
for v,x in self.items():
e[v] = c * x
elif isinstance(other,LpVariable):
return self * LpAffineExpression(other)
else:
if other != 0:
e.constant = self.constant * other
for v,x in self.items():
e[v] = other * x
return e
def __rmul__(self, other):
return self * other
def __div__(self, other):
if isinstance(other,LpAffineExpression) or isinstance(other,LpVariable):
if len(other):
raise TypeError("Expressions cannot be divided by a non-constant expression")
other = other.constant
e = self.emptyCopy()
e.constant = self.constant / other
for v,x in self.items():
e[v] = x / other
return e
def __truediv__(self, other):
if isinstance(other,LpAffineExpression) or isinstance(other,LpVariable):
if len(other):
raise TypeError("Expressions cannot be divided by a non-constant expression")
other = other.constant
e = self.emptyCopy()
e.constant = self.constant / other
for v,x in self.items():
e[v] = x / other
return e
def __rdiv__(self, other):
e = self.emptyCopy()
if len(self):
raise TypeError("Expressions cannot be divided by a non-constant expression")
c = self.constant
if isinstance(other,LpAffineExpression):
e.constant = other.constant / c
for v,x in other.items():
e[v] = x / c
else:
e.constant = other / c
return e
def __le__(self, other):
return LpConstraint(self - other, LpConstraintLE)
def __ge__(self, other):
return LpConstraint(self - other, LpConstraintGE)
def __eq__(self, other):
return LpConstraint(self - other, LpConstraintEQ)
class LpConstraint(LpAffineExpression):
"""An LP constraint"""
def __init__(self, e = None, sense = LpConstraintEQ,
name = None, rhs = None):
"""
:param e: an instance of :class:`LpAffineExpression`
:param sense: one of :data:`~pulp.constants.LpConstraintEQ`, :data:`~pulp.constants.LpConstraintGE`, :data:`~pulp.constants.LpConstraintLE` (0, 1, -1 respectively)
:param name: identifying string
:param rhs: numerical value of constraint target
"""
LpAffineExpression.__init__(self, e, name = name)
if rhs is not None:
self.constant = - rhs
self.sense = sense
self.pi = None
self.slack = None
self.modified = True
def getLb(self):
if ( (self.sense == LpConstraintGE) or
(self.sense == LpConstraintEQ) ):
return -self.constant
else:
return None
def getUb(self):
if ( (self.sense == LpConstraintLE) or
(self.sense == LpConstraintEQ) ):
return -self.constant
else:
return None
def __str__(self):
s = LpAffineExpression.__str__(self, 0)
if self.sense is not None:
s += " " + LpConstraintSenses[self.sense] + " " + str(-self.constant)
return s
def asCplexLpConstraint(self, name):
"""
Returns a constraint as a string
"""
result, line = self.asCplexVariablesOnly(name)
if not list(self.keys()):
line += ["0"]
c = -self.constant
if c == 0:
c = 0 # Supress sign
term = " %s %.12g" % (LpConstraintSenses[self.sense], c)
if self._count_characters(line)+len(term) > LpCplexLPLineSize:
result += ["".join(line)]
line = [term]
else:
line += [term]
result += ["".join(line)]
result = "%s\n" % "\n".join(result)
return result
def changeRHS(self, RHS):
"""
alters the RHS of a constraint so that it can be modified in a resolve
"""
self.constant = -RHS
self.modified = True
def __repr__(self):
s = LpAffineExpression.__repr__(self)
if self.sense is not None:
s += " " + LpConstraintSenses[self.sense] + " 0"
return s
def copy(self):
"""Make a copy of self"""
return LpConstraint(self, self.sense)
def emptyCopy(self):
return LpConstraint(sense = self.sense)
def addInPlace(self, other):
if isinstance(other,LpConstraint):
if self.sense * other.sense >= 0:
LpAffineExpression.addInPlace(self, other)
self.sense |= other.sense
else:
LpAffineExpression.subInPlace(self, other)
self.sense |= - other.sense
elif isinstance(other,list):
for e in other:
self.addInPlace(e)
else:
LpAffineExpression.addInPlace(self, other)
#raise TypeError, "Constraints and Expressions cannot be added"
return self
def subInPlace(self, other):
if isinstance(other,LpConstraint):
if self.sense * other.sense <= 0:
LpAffineExpression.subInPlace(self, other)
self.sense |= - other.sense
else:
LpAffineExpression.addInPlace(self, other)
self.sense |= other.sense
elif isinstance(other,list):
for e in other:
self.subInPlace(e)
else:
LpAffineExpression.subInPlace(self, other)
#raise TypeError, "Constraints and Expressions cannot be added"
return self
def __neg__(self):
c = LpAffineExpression.__neg__(self)
c.sense = - c.sense
return c
def __add__(self, other):
return self.copy().addInPlace(other)
def __radd__(self, other):
return self.copy().addInPlace(other)
def __sub__(self, other):
return self.copy().subInPlace(other)
def __rsub__(self, other):
return (-self).addInPlace(other)
def __mul__(self, other):
if isinstance(other,LpConstraint):
c = LpAffineExpression.__mul__(self, 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)
def __rmul__(self, other):
return self * other
def __div__(self, other):
if isinstance(other,LpConstraint):
c = LpAffineExpression.__div__(self, 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)
def __rdiv__(self, other):
if isinstance(other,LpConstraint):
c = LpAffineExpression.__rdiv__(self, 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)
def valid(self, eps = 0):
val = self.value()
if self.sense == LpConstraintEQ: return abs(val) <= eps
else: return val * self.sense >= - eps
def makeElasticSubProblem(self, *args, **kwargs):
"""
Builds an elastic subproblem by adding variables to a hard constraint
uses FixedElasticSubProblem
"""
return FixedElasticSubProblem(self, *args, **kwargs)
class LpFractionConstraint(LpConstraint):
"""
Creates a constraint that enforces a fraction requirement a/b = c
"""
def __init__(self, numerator, denominator = None, sense = LpConstraintEQ,
RHS = 1.0, name = None,
complement = None):
"""
creates a fraction Constraint to model constraints of
the nature
numerator/denominator {==, >=, <=} RHS
numerator/(numerator + complement) {==, >=, <=} RHS
:param numerator: the top of the fraction
:param denominator: as described above
:param sense: the sense of the relation of the constraint
:param RHS: the target fraction value
:param complement: as described above
"""
self.numerator = numerator
if denominator is None and complement is not None:
self.complement = complement
self.denominator = numerator + complement
elif denominator is not None and complement is None:
self.denominator = denominator
self.complement = denominator - numerator
else:
self.denominator = denominator
self.complement = complement
lhs = self.numerator - RHS * self.denominator
LpConstraint.__init__(self, lhs,
sense = sense, rhs = 0, name = name)
self.RHS = RHS
def findLHSValue(self):
"""
Determines the value of the fraction in the constraint after solution
"""
if abs(value(self.denominator))>= EPS:
return value(self.numerator)/value(self.denominator)
else:
if abs(value(self.numerator))<= EPS:
#zero divided by zero will return 1
return 1.0
else:
raise ZeroDivisionError
def makeElasticSubProblem(self, *args, **kwargs):
"""
Builds an elastic subproblem by adding variables and splitting the
hard constraint
uses FractionElasticSubProblem
"""
return FractionElasticSubProblem(self, *args, **kwargs)
class LpConstraintVar(LpElement):
"""A Constraint that can be treated as a variable when constructing
a LpProblem by columns
"""
def __init__(self, name = None ,sense = None,
rhs = None, e = None):
LpElement.__init__(self,name)
self.constraint = LpConstraint(name = self.name, sense = sense,
rhs = rhs , e = e)
def addVariable(self, var, coeff):
"""
Adds a variable to the constraint with the
activity coeff
"""
self.constraint.addterm(var, coeff)
def value(self):
return self.constraint.value()
class LpProblem(object):
"""An LP Problem"""
def __init__(self, name = "NoName", sense = LpMinimize):
"""
Creates an LP Problem
This function creates a new LP Problem with the specified associated parameters
:param name: name of the problem used in the output .lp file
:param sense: of the LP problem objective. \
Either :data:`~pulp.constants.LpMinimize` (default) \
or :data:`~pulp.constants.LpMaximize`.
:return: An LP Problem
"""
self.objective = None
self.constraints = _DICT_TYPE()
self.name = name
self.sense = sense
self.sos1 = {}
self.sos2 = {}
self.status = LpStatusNotSolved
self.noOverlap = 1
self.solver = None
self.initialValues = {}
self.modifiedVariables = []
self.modifiedConstraints = []
self.resolveOK = False
self._variables = []
self._variable_ids = {} #old school using dict.keys() for a set
self.dummyVar = None
# locals
self.lastUnused = 0
def __repr__(self):
string = self.name+":\n"
if self.sense == 1:
string += "MINIMIZE\n"
else:
string += "MAXIMIZE\n"
string += repr(self.objective) +"\n"
if self.constraints:
string += "SUBJECT TO\n"
for n, c in self.constraints.items():
string += c.asCplexLpConstraint(n) +"\n"
string += "VARIABLES\n"
for v in self.variables():
string += v.asCplexLpVariable() + " " + LpCategories[v.cat] + "\n"
return string
def copy(self):
"""Make a copy of self. Expressions are copied by reference"""
lpcopy = LpProblem(name = self.name, sense = self.sense)
lpcopy.objective = self.objective
lpcopy.constraints = self.constraints.copy()
lpcopy.sos1 = self.sos1.copy()
lpcopy.sos2 = self.sos2.copy()
return lpcopy
def deepcopy(self):
"""Make a copy of self. Expressions are copied by value"""
lpcopy = LpProblem(name = self.name, sense = self.sense)
if self.objective is not None:
lpcopy.objective = self.objective.copy()
lpcopy.constraints = {}
for k,v in self.constraints.items():
lpcopy.constraints[k] = v.copy()
lpcopy.sos1 = self.sos1.copy()
lpcopy.sos2 = self.sos2.copy()
return lpcopy
def normalisedNames(self):
constraintsNames = {}
i = 0
for k in self.constraints:
constraintsNames[k] = "C%07d" % i
i += 1
variablesNames = {}
i = 0
for k in self.variables():
variablesNames[k.name] = "X%07d" % i
i += 1
return constraintsNames, variablesNames, "OBJ"
def isMIP(self):
for v in self.variables():
if v.cat == LpInteger: return 1
return 0
def roundSolution(self, epsInt = 1e-5, eps = 1e-7):
"""
Rounds the lp variables
Inputs:
- none
Side Effects:
- The lp variables are rounded
"""
for v in self.variables():
v.round(epsInt, eps)
def unusedConstraintName(self):
self.lastUnused += 1
while 1:
s = "_C%d" % self.lastUnused
if s not in self.constraints: break
self.lastUnused += 1
return s
def valid(self, eps = 0):
for v in self.variables():
if not v.valid(eps): return False
for c in self.constraints.values():
if not c.valid(eps): return False
else:
return True
def infeasibilityGap(self, mip = 1):
gap = 0
for v in self.variables():
gap = max(abs(v.infeasibilityGap(mip)), gap)
for c in self.constraints.values():
if not c.valid(0):
gap = max(abs(c.value()), gap)
return gap
def addVariable(self, variable):
"""
Adds a variable to the problem before a constraint is added
@param variable: the variable to be added
"""
if id(variable) not in self._variable_ids:
self._variables.append(variable)
self._variable_ids[id(variable)] = variable
def addVariables(self, variables):
"""
Adds variables to the problem before a constraint is added
@param variables: the variables to be added
"""
for v in variables:
self.addVariable(v)
def variables(self):
"""
Returns a list of the problem variables
Inputs:
- none
Returns:
- A list of the problem variables
"""
if self.objective:
self.addVariables(list(self.objective.keys()))
for c in self.constraints.values():
self.addVariables(list(c.keys()))
variables = self._variables
#sort the varibles DSU
variables = [[v.name, v] for v in variables]
variables.sort()
variables = [v for _, v in variables]
return variables
def variablesDict(self):
variables = {}
if self.objective:
for v in self.objective:
variables[v.name] = v
for c in list(self.constraints.values()):
for v in c:
variables[v.name] = v
return variables
def add(self, constraint, name = None):
self.addConstraint(constraint, name)
def addConstraint(self, constraint, name = None):
if not isinstance(constraint, LpConstraint):
raise TypeError("Can only add LpConstraint objects")
if name:
constraint.name = name
try:
if constraint.name:
name = constraint.name
else:
name = self.unusedConstraintName()
except AttributeError:
raise TypeError("Can only add LpConstraint objects")
#removed as this test fails for empty constraints
# if len(constraint) == 0:
# if not constraint.valid():
# raise ValueError, "Cannot add false constraints"
if name in self.constraints:
if self.noOverlap:
raise PulpError("overlapping constraint names: " + name)
else:
print("Warning: overlapping constraint names:", name)
self.constraints[name] = constraint
self.modifiedConstraints.append(constraint)
self.addVariables(list(constraint.keys()))
def setObjective(self,obj):
"""
Sets the input variable as the objective function. Used in Columnwise Modelling
:param obj: the objective function of type :class:`LpConstraintVar`
Side Effects:
- The objective function is set
"""
if isinstance(obj, LpVariable):
# allows the user to add a LpVariable as an objective
obj = obj + 0.0
try:
obj = obj.constraint
name = obj.name
except AttributeError:
name = None
self.objective = obj
self.objective.name = name
self.resolveOK = False
def __iadd__(self, other):
if isinstance(other, tuple):
other, name = other
else:
name = None
if other is True:
return self
if isinstance(other, LpConstraintVar):
self.addConstraint(other.constraint)
elif isinstance(other, LpConstraint):
self.addConstraint(other, name)
elif isinstance(other, LpAffineExpression):
self.objective = other
self.objective.name = name
elif isinstance(other, LpVariable) or isinstance(other, (int, float)):
self.objective = LpAffineExpression(other)
self.objective.name = name
else:
raise TypeError("Can only add LpConstraintVar, LpConstraint, LpAffineExpression or True objects")
return self
def extend(self, other, use_objective = True):
"""
extends an LpProblem by adding constraints either from a dictionary
a tuple or another LpProblem object.
@param use_objective: determines whether the objective is imported from
the other problem
For dictionaries the constraints will be named with the keys
For tuples an unique name will be generated
For LpProblems the name of the problem will be added to the constraints
name
"""
if isinstance(other, dict):
for name in other:
self.constraints[name] = other[name]
elif isinstance(other, LpProblem):
for v in set(other.variables()).difference(self.variables()):
v.name = other.name + v.name
for name,c in other.constraints.items():
c.name = other.name + name
self.addConstraint(c)
if use_objective:
self.objective += other.objective
else:
for c in other:
if isinstance(c,tuple):
name = c[0]
c = c[1]
else:
name = None
if not name: name = c.name
if not name: name = self.unusedConstraintName()
self.constraints[name] = c
def coefficients(self, translation = None):
coefs = []
if translation == None:
for c in self.constraints:
cst = self.constraints[c]
coefs.extend([(v.name, c, cst[v]) for v in cst])
else:
for c in self.constraints:
ctr = translation[c]
cst = self.constraints[c]
coefs.extend([(translation[v.name], ctr, cst[v]) for v in cst])
return coefs
def writeMPS(self, filename, mpsSense = 0, rename = 0, mip = 1):
wasNone, dummyVar = self.fixObjective()
f = open(filename, "w")
if mpsSense == 0: mpsSense = self.sense
cobj = self.objective
if mpsSense != self.sense:
n = cobj.name
cobj = - cobj
cobj.name = n
if rename:
constraintsNames, variablesNames, cobj.name = self.normalisedNames()
f.write("*SENSE:"+LpSenses[mpsSense]+"\n")
n = self.name
if rename: n = "MODEL"
f.write("NAME "+n+"\n")
vs = self.variables()
# constraints
f.write("ROWS\n")
objName = cobj.name
if not objName: objName = "OBJ"
f.write(" N %s\n" % objName)
mpsConstraintType = {LpConstraintLE:"L", LpConstraintEQ:"E", LpConstraintGE:"G"}
for k,c in self.constraints.items():
if rename: k = constraintsNames[k]
f.write(" "+mpsConstraintType[c.sense]+" "+k+"\n")
# matrix
f.write("COLUMNS\n")
# Creation of a dict of dict:
# coefs[nomVariable][nomContrainte] = coefficient
coefs = {}
for k,c in self.constraints.items():
if rename: k = constraintsNames[k]
for v in c:
n = v.name
if rename: n = variablesNames[n]
if n in coefs:
coefs[n][k] = c[v]
else:
coefs[n] = {k:c[v]}
for v in vs:
if mip and v.cat == LpInteger:
f.write(" MARK 'MARKER' 'INTORG'\n")
n = v.name
if rename: n = variablesNames[n]
if n in coefs:
cv = coefs[n]
# Most of the work is done here
for k in cv: f.write(" %-8s %-8s % .12e\n" % (n,k,cv[k]))
# objective function
if v in cobj: f.write(" %-8s %-8s % .12e\n" % (n,objName,cobj[v]))
if mip and v.cat == LpInteger:
f.write(" MARK 'MARKER' 'INTEND'\n")
# right hand side
f.write("RHS\n")
for k,c in self.constraints.items():
c = -c.constant
if rename: k = constraintsNames[k]
if c == 0: c = 0
f.write(" RHS %-8s % .12e\n" % (k,c))
# bounds
f.write("BOUNDS\n")
for v in vs:
n = v.name
if rename: n = variablesNames[n]
if v.lowBound != None and v.lowBound == v.upBound:
f.write(" FX BND %-8s % .12e\n" % (n, v.lowBound))
elif v.lowBound == 0 and v.upBound == 1 and mip and v.cat == LpInteger:
f.write(" BV BND %-8s\n" % n)
else:
if v.lowBound != None:
# In MPS files, variables with no bounds (i.e. >= 0)
# are assumed BV by COIN and CPLEX.
# So we explicitly write a 0 lower bound in this case.
if v.lowBound != 0 or (mip and v.cat == LpInteger and v.upBound == None):
f.write(" LO BND %-8s % .12e\n" % (n, v.lowBound))
else:
if v.upBound != None:
f.write(" MI BND %-8s\n" % n)
else:
f.write(" FR BND %-8s\n" % n)
if v.upBound != None:
f.write(" UP BND %-8s % .12e\n" % (n, v.upBound))
f.write("ENDATA\n")
f.close()
self.restoreObjective(wasNone, dummyVar)
# returns the variables, in writing order
if rename == 0:
return vs
else:
return vs, variablesNames, constraintsNames, cobj.name
def writeLP(self, filename, writeSOS = 1, mip = 1):
"""
Write the given Lp problem to a .lp file.
This function writes the specifications (objective function,
constraints, variables) of the defined Lp problem to a file.
:param filename: the name of the file to be created.
Side Effects:
- The file is created.
"""
f = open(filename, "w")
f.write("\\* "+self.name+" *\\\n")
if self.sense == 1:
f.write("Minimize\n")
else:
f.write("Maximize\n")
wasNone, objectiveDummyVar = self.fixObjective()
objName = self.objective.name
if not objName: objName = "OBJ"
f.write(self.objective.asCplexLpAffineExpression(objName, constant = 0))
f.write("Subject To\n")
ks = list(self.constraints.keys())
ks.sort()
for k in ks:
constraint = self.constraints[k]
if not list(constraint.keys()):
#empty constraint add the dummyVar
dummyVar = self.get_dummyVar()
constraint += dummyVar
#set this dummyvar to zero so infeasible problems are not made feasible
f.write((dummyVar == 0.0).asCplexLpConstraint("_dummy"))
f.write(constraint.asCplexLpConstraint(k))
vs = self.variables()
# check if any names are longer than 100 characters
long_names = [v.name for v in vs if len(v.name) > 100]
if long_names:
raise PulpError('Variable names too long for Lp format\n'
+ str(long_names))
# check for repeated names
repeated_names = {}
for v in vs:
repeated_names[v.name] = repeated_names.get(v.name, 0) + 1
repeated_names = [(key, value) for key, value in list(repeated_names.items())
if value >= 2]
if repeated_names:
raise PulpError('Repeated variable names in Lp format\n'
+ str(repeated_names))
# Bounds on non-"positive" variables
# Note: XPRESS and CPLEX do not interpret integer variables without
# explicit bounds
if mip:
vg = [v for v in vs if not (v.isPositive() and v.cat == LpContinuous) \
and not v.isBinary()]
else:
vg = [v for v in vs if not v.isPositive()]
if vg:
f.write("Bounds\n")
for v in vg:
f.write("%s\n" % v.asCplexLpVariable())
# Integer non-binary variables
if mip:
vg = [v for v in vs if v.cat == LpInteger and not v.isBinary()]
if vg:
f.write("Generals\n")
for v in vg: f.write("%s\n" % v.name)
# Binary variables
vg = [v for v in vs if v.isBinary()]
if vg:
f.write("Binaries\n")
for v in vg: f.write("%s\n" % v.name)
# Special Ordered Sets
if writeSOS and (self.sos1 or self.sos2):
f.write("SOS\n")
if self.sos1:
for sos in self.sos1.values():
f.write("S1:: \n")
for v,val in sos.items():
f.write(" %s: %.12g\n" % (v.name, val))
if self.sos2:
for sos in self.sos2.values():
f.write("S2:: \n")
for v,val in sos.items():
f.write(" %s: %.12g\n" % (v.name, val))
f.write("End\n")
f.close()
self.restoreObjective(wasNone, objectiveDummyVar)
def assignVarsVals(self, values):
variables = self.variablesDict()
for name in values:
if name != '__dummy':
variables[name].varValue = values[name]
def assignVarsDj(self,values):
variables = self.variablesDict()
for name in values:
if name != '__dummy':
variables[name].dj = values[name]
def assignConsPi(self, values):
for name in values:
self.constraints[name].pi = values[name]
def assignConsSlack(self, values, activity=False):
for name in values:
if activity:
#reports the activitynot the slack
self.constraints[name].slack = -1 * (
self.constraints[name].constant + float(values[name]))
else:
self.constraints[name].slack = float(values[name])
def get_dummyVar(self):
if self.dummyVar is None:
self.dummyVar = LpVariable("__dummy", 0, 0)
return self.dummyVar
def fixObjective(self):
if self.objective is None:
self.objective = 0
wasNone = 1
else:
wasNone = 0
if not isinstance(self.objective, LpAffineExpression):
self.objective = LpAffineExpression(self.objective)
if self.objective.isNumericalConstant():
dummyVar = self.get_dummyVar()
self.objective += dummyVar
else:
dummyVar = None
return wasNone, dummyVar
def restoreObjective(self, wasNone, dummyVar):
if wasNone:
self.objective = None
elif not dummyVar is None:
self.objective -= dummyVar
def solve(self, solver = None, **kwargs):
"""
Solve the given Lp problem.
This function changes the problem to make it suitable for solving
then calls the solver.actualSolve() method to find the solution
:param solver: Optional: the specific solver to be used, defaults to the
default solver.
Side Effects:
- The attributes of the problem object are changed in
:meth:`~pulp.solver.LpSolver.actualSolve()` to reflect the Lp solution
"""
if not(solver): solver = self.solver
if not(solver): solver = LpSolverDefault
wasNone, dummyVar = self.fixObjective()
#time it
self.solutionTime = -clock()
status = solver.actualSolve(self, **kwargs)
self.solutionTime += clock()
self.restoreObjective(wasNone, dummyVar)
self.solver = solver
return status
def sequentialSolve(self, objectives, absoluteTols = None,
relativeTols = None, solver = None, debug = False):
"""
Solve the given Lp problem with several objective functions.
This function sequentially changes the objective of the problem
and then adds the objective function as a constraint
:param objectives: the list of objectives to be used to solve the problem
:param absoluteTols: the list of absolute tolerances to be applied to
the constraints should be +ve for a minimise objective
:param relativeTols: the list of relative tolerances applied to the constraints
:param solver: the specific solver to be used, defaults to the default solver.
"""
#TODO Add a penalty variable to make problems elastic
#TODO add the ability to accept different status values i.e. infeasible etc
if not(solver): solver = self.solver
if not(solver): solver = LpSolverDefault
if not(absoluteTols):
absoluteTols = [0] * len(objectives)
if not(relativeTols):
relativeTols = [1] * len(objectives)
#time it
self.solutionTime = -clock()
statuses = []
for i,(obj,absol,rel) in enumerate(zip(objectives,
absoluteTols, relativeTols)):
self.setObjective(obj)
status = solver.actualSolve(self)
statuses.append(status)
if debug: self.writeLP("%sSequence.lp"%i)
if self.sense == LpMinimize:
self += obj <= value(obj)*rel + absol,"%s_Sequence_Objective"%i
elif self.sense == LpMaximize:
self += obj >= value(obj)*rel + absol,"%s_Sequence_Objective"%i
self.solutionTime += clock()
self.solver = solver
return statuses
def resolve(self, solver = None, **kwargs):
"""
resolves an Problem using the same solver as previously
"""
if not(solver): solver = self.solver
if self.resolveOK:
return self.solver.actualResolve(self, **kwargs)
else:
return self.solve(solver = solver, **kwargs)
def setSolver(self,solver = LpSolverDefault):
"""Sets the Solver for this problem useful if you are using
resolve
"""
self.solver = solver
def setInitial(self,values):
self.initialValues = values
def numVariables(self):
return len(self._variable_ids)
def numConstraints(self):
return len(self.constraints)
def getSense(self):
return self.sense
class FixedElasticSubProblem(LpProblem):
"""
Contains the subproblem generated by converting a fixed constraint
:math:`\sum_{i}a_i x_i = b` into an elastic constraint.
:param constraint: The LpConstraint that the elastic constraint is based on
:param penalty: penalty applied for violation (+ve or -ve) of the constraints
:param proportionFreeBound:
the proportional bound (+ve and -ve) on
constraint violation that is free from penalty
:param proportionFreeBoundList: the proportional bound on \
constraint violation that is free from penalty, expressed as a list\
where [-ve, +ve]
"""
def __init__(self, constraint, penalty = None,
proportionFreeBound = None,
proportionFreeBoundList = None):
subProblemName = "%s_elastic_SubProblem" % constraint.name
LpProblem.__init__(self, subProblemName, LpMinimize)
self.objective = LpAffineExpression()
self.constraint = constraint
self.constant = constraint.constant
self.RHS = - constraint.constant
self.objective = LpAffineExpression()
self += constraint, "_Constraint"
#create and add these variables but disabled
self.freeVar = LpVariable("_free_bound",
upBound = 0, lowBound = 0)
self.upVar = LpVariable("_pos_penalty_var",
upBound = 0, lowBound = 0)
self.lowVar = LpVariable("_neg_penalty_var",
upBound = 0, lowBound = 0)
constraint.addInPlace(self.freeVar + self.lowVar + self.upVar)
if proportionFreeBound:
proportionFreeBoundList = [proportionFreeBound, proportionFreeBound]
if proportionFreeBoundList:
#add a costless variable
self.freeVar.upBound = abs(constraint.constant *
proportionFreeBoundList[0])
self.freeVar.lowBound = -abs(constraint.constant *
proportionFreeBoundList[1])
# Note the reversal of the upbound and lowbound due to the nature of the
# variable
if penalty is not None:
#activate these variables
self.upVar.upBound = None
self.lowVar.lowBound = None
self.objective = penalty*self.upVar - penalty*self.lowVar
def _findValue(self, attrib):
"""
safe way to get the value of a variable that may not exist
"""
var = getattr(self, attrib, 0)
if var:
if value(var) is not None:
return value(var)
else:
return 0.0
else:
return 0.0
def isViolated(self):
"""
returns true if the penalty variables are non-zero
"""
upVar = self._findValue("upVar")
lowVar = self._findValue("lowVar")
freeVar = self._findValue("freeVar")
result = abs(upVar + lowVar) >= EPS
if result:
log.debug("isViolated %s, upVar %s, lowVar %s, freeVar %s result %s"%(
self.name, upVar, lowVar, freeVar, result))
log.debug("isViolated value lhs %s constant %s"%(
self.findLHSValue(), self.RHS))
return result
def findDifferenceFromRHS(self):
"""
The amount the actual value varies from the RHS (sense: LHS - RHS)
"""
return self.findLHSValue() - self.RHS
def findLHSValue(self):
"""
for elastic constraints finds the LHS value of the constraint without
the free variable and or penalty variable assumes the constant is on the
rhs
"""
upVar = self._findValue("upVar")
lowVar = self._findValue("lowVar")
freeVar = self._findValue("freeVar")
return self.constraint.value() - self.constant - \
upVar - lowVar - freeVar
def deElasticize(self):
""" de-elasticize constraint """
self.upVar.upBound = 0
self.lowVar.lowBound = 0
def reElasticize(self):
"""
Make the Subproblem elastic again after deElasticize
"""
self.upVar.lowBound = 0
self.upVar.upBound = None
self.lowVar.upBound = 0
self.lowVar.lowBound = None
def alterName(self, name):
"""
Alters the name of anonymous parts of the problem
"""
self.name = "%s_elastic_SubProblem" % name
if hasattr(self, 'freeVar'):
self.freeVar.name = self.name + "_free_bound"
if hasattr(self, 'upVar'):
self.upVar.name = self.name + "_pos_penalty_var"
if hasattr(self, 'lowVar'):
self.lowVar.name = self.name + "_neg_penalty_var"
class FractionElasticSubProblem(FixedElasticSubProblem):
"""
Contains the subproblem generated by converting a Fraction constraint
numerator/(numerator+complement) = b
into an elastic constraint
:param name: The name of the elastic subproblem
:param penalty: penalty applied for violation (+ve or -ve) of the constraints
:param proportionFreeBound: the proportional bound (+ve and -ve) on
constraint violation that is free from penalty
:param proportionFreeBoundList: the proportional bound on
constraint violation that is free from penalty, expressed as a list
where [-ve, +ve]
"""
def __init__(self, name, numerator, RHS, sense,
complement = None,
denominator = None,
penalty = None,
proportionFreeBound = None,
proportionFreeBoundList = None):
subProblemName = "%s_elastic_SubProblem" % name
self.numerator = numerator
if denominator is None and complement is not None:
self.complement = complement
self.denominator = numerator + complement
elif denominator is not None and complement is None:
self.denominator = denominator
self.complement = denominator - numerator
else:
raise PulpError('only one of denominator and complement must be specified')
self.RHS = RHS
self.lowTarget = self.upTarget = None
LpProblem.__init__(self, subProblemName, LpMinimize)
self.freeVar = LpVariable("_free_bound",
upBound = 0, lowBound = 0)
self.upVar = LpVariable("_pos_penalty_var",
upBound = 0, lowBound = 0)
self.lowVar = LpVariable("_neg_penalty_var",
upBound = 0, lowBound = 0)
if proportionFreeBound:
proportionFreeBoundList = [proportionFreeBound, proportionFreeBound]
if proportionFreeBoundList:
upProportionFreeBound, lowProportionFreeBound = \
proportionFreeBoundList
else:
upProportionFreeBound, lowProportionFreeBound = (0, 0)
#create an objective
self += LpAffineExpression()
#There are three cases if the constraint.sense is ==, <=, >=
if sense in [LpConstraintEQ, LpConstraintLE]:
#create a constraint the sets the upper bound of target
self.upTarget = RHS + upProportionFreeBound
self.upConstraint = LpFractionConstraint(self.numerator,
self.complement,
LpConstraintLE,
self.upTarget,
denominator = self.denominator)
if penalty is not None:
self.lowVar.lowBound = None
self.objective += -1* penalty * self.lowVar
self.upConstraint += self.lowVar
self += self.upConstraint, '_upper_constraint'
if sense in [LpConstraintEQ, LpConstraintGE]:
#create a constraint the sets the lower bound of target
self.lowTarget = RHS - lowProportionFreeBound
self.lowConstraint = LpFractionConstraint(self.numerator,
self.complement,
LpConstraintGE,
self.lowTarget,
denominator = self.denominator)
if penalty is not None:
self.upVar.upBound = None
self.objective += penalty * self.upVar
self.lowConstraint += self.upVar
self += self.lowConstraint, '_lower_constraint'
def findLHSValue(self):
"""
for elastic constraints finds the LHS value of the constraint without
the free variable and or penalty variable assumes the constant is on the
rhs
"""
# uses code from LpFractionConstraint
if abs(value(self.denominator))>= EPS:
return value(self.numerator)/value(self.denominator)
else:
if abs(value(self.numerator))<= EPS:
#zero divided by zero will return 1
return 1.0
else:
raise ZeroDivisionError
def isViolated(self):
"""
returns true if the penalty variables are non-zero
"""
if abs(value(self.denominator))>= EPS:
if self.lowTarget is not None:
if self.lowTarget > self.findLHSValue():
return True
if self.upTarget is not None:
if self.findLHSValue() > self.upTarget:
return True
else:
#if the denominator is zero the constraint is satisfied
return False
class LpVariableDict(dict):
"""An LP variable generator"""
def __init__(self, name, data = {}, lowBound = None, upBound = None, cat = LpContinuous):
self.name = name
dict.__init__(self, data)
def __getitem__(self, key):
if key in self:
return dict.__getitem__(self, key)
else:
self[key] = LpVariable(self.name % key, lowBound, upBound, cat)
return self[key]
# Utility functions
def lpSum(vector):
"""
Calculate the sum of a list of linear expressions
:param vector: A list of linear expressions
"""
return LpAffineExpression().addInPlace(vector)
def lpDot(v1, v2):
"""Calculate the dot product of two lists of linear expressions"""
if not isiterable(v1) and not isiterable(v2):
return v1 * v2
elif not isiterable(v1):
return lpDot([v1]*len(v2),v2)
elif not isiterable(v2):
return lpDot(v1,[v2]*len(v1))
else:
return lpSum([lpDot(e1,e2) for e1,e2 in zip(v1,v2)])
def isNumber(x):
"""Returns true if x is an int or a float"""
return isinstance(x, (int, float))
def value(x):
"""Returns the value of the variable/expression x, or x if it is a number"""
if isNumber(x): return x
else: return x.value()
def valueOrDefault(x):
"""Returns the value of the variable/expression x, or x if it is a number
Variable without value (None) are affected a possible value (within their
bounds)."""
if isNumber(x): return x
else: return x.valueOrDefault()
def combination(orgset, k = None):
"""
returns an iterator that lists the combinations of orgset of
length k
:param orgset: the list to be iterated
:param k: the cardinality of the subsets
:return: an iterator of the subsets
example:
>>> c = combination([1,2,3,4],2)
>>> for s in c:
... print(s)
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)
"""
try:
from itertools import combination as _it_combination
return _it_combination(orgset, k)
except ImportError:
return __combination(orgset,k)
def __combination(orgset,k):
"""
fall back if probstat is not installed note it is GPL so cannot
be included
"""
if k == 1:
for i in orgset:
yield (i,)
elif k>1:
for i,x in enumerate(orgset):
#iterates though to near the end
for s in __combination(orgset[i+1:],k-1):
yield (x,) + s
def permutation(orgset, k = None):
"""
returns an iterator that lists the permutations of orgset of
length k
:param orgset: the list to be iterated
:param k: the cardinality of the subsets
:return: an iterator of the subsets
example:
>>> c = permutation([1,2,3,4],2)
>>> for s in c:
... print(s)
(1, 2)
(1, 3)
(1, 4)
(2, 1)
(2, 3)
(2, 4)
(3, 1)
(3, 2)
(3, 4)
(4, 1)
(4, 2)
(4, 3)
"""
try:
from itertools import permutation as _it_permutation
return _it_permutation(orgset, k)
except ImportError:
return __permutation(orgset, k)
def __permutation(orgset, k):
"""
fall back if probstat is not installed note it is GPL so cannot
be included
"""
if k == 1:
for i in orgset:
yield (i,)
elif k>1:
for i,x in enumerate(orgset):
#iterates though to near the end
for s in __permutation(orgset[:i] + orgset[i+1:],k-1):
yield (x,)+ s
def allpermutations(orgset,k):
"""
returns all permutations of orgset with up to k items
:param orgset: the list to be iterated
:param k: the maxcardinality of the subsets
:return: an iterator of the subsets
example:
>>> c = allpermutations([1,2,3,4],2)
>>> for s in c:
... print(s)
(1,)
(2,)
(3,)
(4,)
(1, 2)
(1, 3)
(1, 4)
(2, 1)
(2, 3)
(2, 4)
(3, 1)
(3, 2)
(3, 4)
(4, 1)
(4, 2)
(4, 3)
"""
return itertools.chain(*[permutation(orgset,i) for i in range(1,k+1)])
def allcombinations(orgset,k):
"""
returns all permutations of orgset with up to k items
:param orgset: the list to be iterated
:param k: the maxcardinality of the subsets
:return: an iterator of the subsets
example:
>>> c = allcombinations([1,2,3,4],2)
>>> for s in c:
... print(s)
(1,)
(2,)
(3,)
(4,)
(1, 2)
(1, 3)
(1, 4)
(2, 3)
(2, 4)
(3, 4)
"""
return itertools.chain(*[combination(orgset,i) for i in range(1,k+1)])
def makeDict(headers, array, default = None):
"""
makes a list into a dictionary with the headings given in headings
headers is a list of header lists
array is a list with the data
"""
result, defdict = __makeDict(headers, array, default)
return result
def __makeDict(headers, array, default = None):
#this is a recursive function so end the recursion as follows
result ={}
returndefaultvalue = None
if len(headers) == 1:
result.update(dict(zip(headers[0],array)))
defaultvalue = default
else:
for i,h in enumerate(headers[0]):
result[h],defaultvalue = __makeDict(headers[1:],array[i],default)
if default != None:
f = lambda :defaultvalue
defresult = collections.defaultdict(f)
defresult.update(result)
result = defresult
returndefaultvalue = collections.defaultdict(f)
return result, returndefaultvalue
def splitDict(Data):
"""
Split a dictionary with lists as the data, into smaller dictionaries
:param Data: A dictionary with lists as the values
:return: A tuple of dictionaries each containing the data separately,
with the same dictionary keys
"""
# find the maximum number of items in the dictionary
maxitems = max([len(values) for values in Data.values()])
output =[dict() for i in range(maxitems)]
for key, values in Data.items():
for i, val in enumerate(values):
output[i][key] = val
return tuple(output)
def read_table(data, coerce_type, transpose=False):
'''
Reads in data from a simple table and forces it to be a particular type
This is a helper function that allows data to be easily constained in a
simple script
::return: a dictionary of with the keys being a tuple of the strings
in the first row and colum of the table
::param data: the multiline string containing the table data
::param coerce_type: the type that the table data is converted to
::param transpose: reverses the data if needed
Example:
>>> table_data = """
... L1 L2 L3 L4 L5 L6
... C1 6736 42658 70414 45170 184679 111569
... C2 217266 227190 249640 203029 153531 117487
... C3 35936 28768 126316 2498 130317 74034
... C4 73446 52077 108368 75011 49827 62850
... C5 174664 177461 151589 153300 59916 135162
... C6 186302 189099 147026 164938 149836 286307
... """
>>> table = read_table(table_data, int)
>>> table[("C1","L1")]
6736
>>> table[("C6","L5")]
149836
'''
lines = data.splitlines()
headings = lines[1].split()
result = {}
for row in lines[2:]:
items = row.split()
for i, item in enumerate(items[1:]):
if transpose:
key = (headings[i], items[0])
else:
key = (items[0], headings[i])
result[key] = coerce_type(item)
return result
def configSolvers():
"""
Configure the path the the solvers on the command line
Designed to configure the file locations of the solvers from the
command line after installation
"""
configlist = [(cplex_dll_path,"cplexpath","CPLEX: "),
(coinMP_path, "coinmppath","CoinMP dll (windows only): ")]
print("Please type the full path including filename and extension \n" +
"for each solver available")
configdict = {}
for (default, key, msg) in configlist:
value = input(msg + "[" + str(default) +"]")
if value:
configdict[key] = value
setConfigInformation(**configdict)
def pulpTestAll():
from .tests import pulpTestSolver
solvers = [PULP_CBC_CMD,
CPLEX_DLL,
CPLEX_CMD,
CPLEX_PY,
COIN_CMD,
COINMP_DLL,
GLPK_CMD,
XPRESS,
GUROBI,
GUROBI_CMD,
PYGLPK,
YAPOSIB
]
failed = False
for s in solvers:
if s().available():
try:
pulpTestSolver(s)
print("* Solver %s passed." % s)
except Exception as e:
print(e)
print("* Solver", s, "failed.")
failed = True
else:
print("Solver %s unavailable" % s)
if failed:
raise PulpError("Tests Failed")
def pulpDoctest():
"""
runs all doctests
"""
import doctest
if __name__ != '__main__':
from . import pulp
doctest.testmod(pulp)
else:
doctest.testmod()
if __name__ == '__main__':
# Tests
pulpTestAll()
pulpDoctest()