feat: 初始化Easy Patch插件及依赖文件

- 添加Blender插件核心文件:__init__.py、ui.py、property.py、preference.py
- 添加插件工具模块:g.py、loop.py、generate_loop.py、const.py、op.py
- 添加翻译工具:utils/trans.py
- 添加PuLP线性规划库及其依赖文件,包括CBC求解器二进制文件
- 添加.gitignore和VSCode配置文件
This commit is contained in:
2026-03-03 19:24:57 +08:00
commit ab91b120e6
44 changed files with 17551 additions and 0 deletions

41
utils/pulp/__init__.py Normal file
View File

@@ -0,0 +1,41 @@
# PuLP : Python LP Modeler
# Version 1.20
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id: __init__.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.
"""
Module file that imports all of the pulp functions
Copyright 2007- Stuart Mitchell (s.mitchell@auckland.ac.nz)
"""
from .constants import VERSION
from .pulp import *
from .apis import *
from .utilities import *
from .constants import *
from .tests import pulpTestAll
__doc__ = pulp.__doc__
__version__ = VERSION

165
utils/pulp/apis/__init__.py Normal file
View File

@@ -0,0 +1,165 @@
from .coin_api import *
from .cplex_api import *
from .gurobi_api import *
from .glpk_api import *
from .choco_api import *
from .mipcl_api import *
from .mosek_api import *
from .scip_api import *
from .xpress_api import *
from .highs_api import *
from .copt_api import *
from .core import *
_all_solvers = [
GLPK_CMD,
PYGLPK,
CPLEX_CMD,
CPLEX_PY,
GUROBI,
GUROBI_CMD,
MOSEK,
XPRESS,
XPRESS_CMD,
XPRESS_PY,
PULP_CBC_CMD,
COIN_CMD,
COINMP_DLL,
CHOCO_CMD,
MIPCL_CMD,
SCIP_CMD,
FSCIP_CMD,
SCIP_PY,
HiGHS,
HiGHS_CMD,
COPT,
COPT_DLL,
COPT_CMD,
]
import json
# 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
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 = Parser()
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()
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 getSolver(solver, *args, **kwargs):
"""
Instantiates a solver from its name
:param str solver: solver name to create
:param args: additional arguments to the solver
:param kwargs: additional keyword arguments to the solver
:return: solver of type :py:class:`LpSolver`
"""
mapping = {k.name: k for k in _all_solvers}
try:
return mapping[solver](*args, **kwargs)
except KeyError:
raise PulpSolverError(
"The solver {} does not exist in PuLP.\nPossible options are: \n{}".format(
solver, mapping.keys()
)
)
def getSolverFromDict(data):
"""
Instantiates a solver from a dictionary with its data
:param dict data: a dictionary with, at least an "solver" key with the name
of the solver to create
:return: a solver of type :py:class:`LpSolver`
:raises PulpSolverError: if the dictionary does not have the "solver" key
:rtype: LpSolver
"""
solver = data.pop("solver", None)
if solver is None:
raise PulpSolverError("The json file has no solver attribute.")
return getSolver(solver, **data)
def getSolverFromJson(filename):
"""
Instantiates a solver from a json file with its data
:param str filename: name of the json file to read
:return: a solver of type :py:class:`LpSolver`
:rtype: LpSolver
"""
with open(filename) as f:
data = json.load(f)
return getSolverFromDict(data)
def listSolvers(onlyAvailable=False):
"""
List the names of all the existing solvers in PuLP
:param bool onlyAvailable: if True, only show the available solvers
:return: list of solver names
:rtype: list
"""
result = []
for s in _all_solvers:
solver = s()
if (not onlyAvailable) or solver.available():
result.append(solver.name)
del solver
return result
# DEPRECATED aliases:
get_solver = getSolver
get_solver_from_json = getSolverFromJson
get_solver_from_dict = getSolverFromDict
list_solvers = listSolvers

View File

@@ -0,0 +1,158 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver_CMD, subprocess, PulpSolverError
import os
from .. import constants
import warnings
class CHOCO_CMD(LpSolver_CMD):
"""The CHOCO_CMD solver"""
name = "CHOCO_CMD"
def __init__(
self,
path=None,
keepFiles=False,
mip=True,
msg=True,
options=None,
timeLimit=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param list options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
path=path,
keepFiles=keepFiles,
)
def defaultPath(self):
return self.executableExtension("choco-parsers-with-dependencies.jar")
def available(self):
"""True if the solver is available"""
java_path = self.executableExtension("java")
return self.executable(self.path) and self.executable(java_path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
java_path = self.executableExtension("java")
if not self.executable(java_path):
raise PulpSolverError(
"PuLP: java needs to be installed and accesible in order to use CHOCO_CMD"
)
if not os.path.exists(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpMps, tmpLp, tmpSol = self.create_tmp_files(lp.name, "mps", "lp", "sol")
# just to report duplicated variables:
lp.checkDuplicateVars()
lp.writeMPS(tmpMps, mpsSense=lp.sense)
try:
os.remove(tmpSol)
except:
pass
cmd = java_path + ' -cp "' + self.path + '" org.chocosolver.parser.mps.ChocoMPS'
if self.timeLimit is not None:
cmd += f" -tl {self.timeLimit}" * 1000
cmd += " " + " ".join([f"{key} {value}" for key, value in self.options])
cmd += f" {tmpMps}"
if lp.sense == constants.LpMaximize:
cmd += " -max"
if lp.isMIP():
if not self.mip:
warnings.warn("CHOCO_CMD cannot solve the relaxation of a problem")
# we always get the output to a file.
# if not, we cannot read it afterwards
# (we thus ignore the self.msg parameter)
pipe = open(tmpSol, "w")
return_code = subprocess.call(cmd, stdout=pipe, stderr=pipe, shell=True)
if return_code != 0:
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
if not os.path.exists(tmpSol):
status = constants.LpStatusNotSolved
status_sol = constants.LpSolutionNoSolutionFound
values = None
else:
status, values, status_sol = self.readsol(tmpSol)
self.delete_tmp_files(tmpMps, tmpLp, tmpSol)
lp.assignStatus(status, status_sol)
if status not in [constants.LpStatusInfeasible, constants.LpStatusNotSolved]:
lp.assignVarsVals(values)
return status
@staticmethod
def readsol(filename):
"""Read a Choco solution file"""
# TODO: figure out the unbounded status in choco solver
chocoStatus = {
"OPTIMUM FOUND": constants.LpStatusOptimal,
"SATISFIABLE": constants.LpStatusOptimal,
"UNSATISFIABLE": constants.LpStatusInfeasible,
"UNKNOWN": constants.LpStatusNotSolved,
}
chocoSolStatus = {
"OPTIMUM FOUND": constants.LpSolutionOptimal,
"SATISFIABLE": constants.LpSolutionIntegerFeasible,
"UNSATISFIABLE": constants.LpSolutionInfeasible,
"UNKNOWN": constants.LpSolutionNoSolutionFound,
}
status = constants.LpStatusNotSolved
sol_status = constants.LpSolutionNoSolutionFound
values = {}
with open(filename) as f:
content = f.readlines()
content = [l.strip() for l in content if l[:2] not in ["o ", "c "]]
if not len(content):
return status, values, sol_status
if content[0][:2] == "s ":
status_str = content[0][2:]
status = chocoStatus[status_str]
sol_status = chocoSolStatus[status_str]
for line in content[1:]:
name, value = line.split()
values[name] = float(value)
return status, values, sol_status

873
utils/pulp/apis/coin_api.py Normal file
View File

@@ -0,0 +1,873 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
from .core import cbc_path, pulp_cbc_path, coinMP_path, devnull, operating_system
import os
from .. import constants
from tempfile import mktemp
import ctypes
import warnings
class COIN_CMD(LpSolver_CMD):
"""The COIN CLP/CBC LP solver
now only uses cbc
"""
name = "COIN_CMD"
def defaultPath(self):
return self.executableExtension(cbc_path)
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
fracGap=None,
maxSeconds=None,
gapRel=None,
gapAbs=None,
presolve=None,
cuts=None,
strong=None,
options=None,
warmStart=False,
keepFiles=False,
path=None,
threads=None,
logPath=None,
timeMode="elapsed",
mip_start=False,
maxNodes=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int threads: sets the maximum number of threads
:param list options: list of additional options to pass to solver
:param bool warmStart: if True, the solver will use the current value of variables as a start
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param str logPath: path to the log file
:param bool presolve: if True, adds presolve on
:param bool cuts: if True, adds gomory on knapsack on probing on
:param bool strong: if True, adds strong
:param float fracGap: deprecated for gapRel
:param float maxSeconds: deprecated for timeLimit
:param str timeMode: "elapsed": count wall-time to timeLimit; "cpu": count cpu-time
:param bool mip_start: deprecated for warmStart
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
"""
if fracGap is not None:
warnings.warn("Parameter fracGap is being depreciated for gapRel")
if gapRel is not None:
warnings.warn("Parameter gapRel and fracGap passed, using gapRel")
else:
gapRel = fracGap
if maxSeconds is not None:
warnings.warn("Parameter maxSeconds is being depreciated for timeLimit")
if timeLimit is not None:
warnings.warn(
"Parameter timeLimit and maxSeconds passed, using timeLimit"
)
else:
timeLimit = maxSeconds
if mip_start:
warnings.warn("Parameter mip_start is being depreciated for warmStart")
if warmStart:
warnings.warn(
"Parameter mipStart and mip_start passed, using warmStart"
)
else:
warmStart = mip_start
LpSolver_CMD.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
presolve=presolve,
cuts=cuts,
strong=strong,
options=options,
warmStart=warmStart,
path=path,
keepFiles=keepFiles,
threads=threads,
gapAbs=gapAbs,
logPath=logPath,
timeMode=timeMode,
maxNodes=maxNodes,
)
def copy(self):
"""Make a copy of self"""
aCopy = LpSolver_CMD.copy(self)
aCopy.optionsDict = self.optionsDict
return aCopy
def actualSolve(self, lp, **kwargs):
"""Solve a well formulated lp problem"""
return self.solve_CBC(lp, **kwargs)
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def solve_CBC(self, lp, use_mps=True):
"""Solve a MIP problem using CBC"""
if not self.executable(self.path):
raise PulpSolverError(
f"Pulp: cannot execute {self.path} cwd: {os.getcwd()}"
)
tmpLp, tmpMps, tmpSol, tmpMst = self.create_tmp_files(
lp.name, "lp", "mps", "sol", "mst"
)
if use_mps:
vs, variablesNames, constraintsNames, objectiveName = lp.writeMPS(
tmpMps, rename=1
)
cmds = " " + tmpMps + " "
if lp.sense == constants.LpMaximize:
cmds += "-max "
else:
vs = lp.writeLP(tmpLp)
# In the Lp we do not create new variable or constraint names:
variablesNames = {v.name: v.name for v in vs}
constraintsNames = {c: c for c in lp.constraints}
cmds = " " + tmpLp + " "
if self.optionsDict.get("warmStart", False):
self.writesol(tmpMst, lp, vs, variablesNames, constraintsNames)
cmds += f"-mips {tmpMst} "
if self.timeLimit is not None:
cmds += f"-sec {self.timeLimit} "
options = self.options + self.getOptions()
for option in options:
cmds += "-" + option + " "
if self.mip:
cmds += "-branch "
else:
cmds += "-initialSolve "
cmds += "-printingOptions all "
cmds += "-solution " + tmpSol + " "
if self.msg:
pipe = None
else:
pipe = open(os.devnull, "w")
logPath = self.optionsDict.get("logPath")
if logPath:
if self.msg:
warnings.warn(
"`logPath` argument replaces `msg=1`. The output will be redirected to the log file."
)
pipe = open(self.optionsDict["logPath"], "w")
log.debug(self.path + cmds)
args = []
args.append(self.path)
args.extend(cmds[1:].split())
if not self.msg and operating_system == "win":
# Prevent flashing windows if used from a GUI application
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
cbc = subprocess.Popen(
args, stdout=pipe, stderr=pipe, stdin=devnull, startupinfo=startupinfo
)
else:
cbc = subprocess.Popen(args, stdout=pipe, stderr=pipe, stdin=devnull)
if cbc.wait() != 0:
if pipe:
pipe.close()
raise PulpSolverError(
"Pulp: Error while trying to execute, use msg=True for more details"
+ self.path
)
if pipe:
pipe.close()
if not os.path.exists(tmpSol):
raise PulpSolverError("Pulp: Error while executing " + self.path)
(
status,
values,
reducedCosts,
shadowPrices,
slacks,
sol_status,
) = self.readsol_MPS(tmpSol, lp, vs, variablesNames, constraintsNames)
lp.assignVarsVals(values)
lp.assignVarsDj(reducedCosts)
lp.assignConsPi(shadowPrices)
lp.assignConsSlack(slacks, activity=True)
lp.assignStatus(status, sol_status)
self.delete_tmp_files(tmpMps, tmpLp, tmpSol, tmpMst)
return status
def getOptions(self):
params_eq = dict(
gapRel="ratio {}",
gapAbs="allow {}",
threads="threads {}",
presolve="presolve on",
strong="strong {}",
cuts="gomory on knapsack on probing on",
timeMode="timeMode {}",
maxNodes="maxNodes {}",
)
return [
v.format(self.optionsDict[k])
for k, v in params_eq.items()
if self.optionsDict.get(k) is not None
]
def readsol_MPS(
self, filename, lp, vs, variablesNames, constraintsNames, objectiveName=None
):
"""
Read a CBC solution file generated from an mps or lp file (possible different names)
"""
values = {v.name: 0 for v in vs}
reverseVn = {v: k for k, v in variablesNames.items()}
reverseCn = {v: k for k, v in constraintsNames.items()}
reducedCosts = {}
shadowPrices = {}
slacks = {}
status, sol_status = self.get_status(filename)
with open(filename) as f:
for l in f:
if len(l) <= 2:
break
l = l.split()
# incase the solution is infeasible
if l[0] == "**":
l = l[1:]
vn = l[1]
val = l[2]
dj = l[3]
if vn in reverseVn:
values[reverseVn[vn]] = float(val)
reducedCosts[reverseVn[vn]] = float(dj)
if vn in reverseCn:
slacks[reverseCn[vn]] = float(val)
shadowPrices[reverseCn[vn]] = float(dj)
return status, values, reducedCosts, shadowPrices, slacks, sol_status
def writesol(self, filename, lp, vs, variablesNames, constraintsNames):
"""
Writes a CBC solution file generated from an mps / lp file (possible different names)
returns True on success
"""
values = {v.name: v.value() if v.value() is not None else 0 for v in vs}
value_lines = []
value_lines += [
(i, v, values[k], 0) for i, (k, v) in enumerate(variablesNames.items())
]
lines = ["Stopped on time - objective value 0\n"]
lines += ["{:>7} {} {:>15} {:>23}\n".format(*tup) for tup in value_lines]
with open(filename, "w") as f:
f.writelines(lines)
return True
def readsol_LP(self, filename, lp, vs):
"""
Read a CBC solution file generated from an lp (good names)
returns status, values, reducedCosts, shadowPrices, slacks, sol_status
"""
variablesNames = {v.name: v.name for v in vs}
constraintsNames = {c: c for c in lp.constraints}
return self.readsol_MPS(filename, lp, vs, variablesNames, constraintsNames)
def get_status(self, filename):
cbcStatus = {
"Optimal": constants.LpStatusOptimal,
"Infeasible": constants.LpStatusInfeasible,
"Integer": constants.LpStatusInfeasible,
"Unbounded": constants.LpStatusUnbounded,
"Stopped": constants.LpStatusNotSolved,
}
cbcSolStatus = {
"Optimal": constants.LpSolutionOptimal,
"Infeasible": constants.LpSolutionInfeasible,
"Unbounded": constants.LpSolutionUnbounded,
"Stopped": constants.LpSolutionNoSolutionFound,
}
with open(filename) as f:
statusstrs = f.readline().split()
status = cbcStatus.get(statusstrs[0], constants.LpStatusUndefined)
sol_status = cbcSolStatus.get(
statusstrs[0], constants.LpSolutionNoSolutionFound
)
# here we could use some regex expression.
# Not sure what's more desirable
if status == constants.LpStatusNotSolved and len(statusstrs) >= 5:
if statusstrs[4] == "objective":
status = constants.LpStatusOptimal
sol_status = constants.LpSolutionIntegerFeasible
return status, sol_status
COIN = COIN_CMD
class PULP_CBC_CMD(COIN_CMD):
"""
This solver uses a precompiled version of cbc provided with the package
"""
name = "PULP_CBC_CMD"
pulp_cbc_path = pulp_cbc_path
try:
if os.name != "nt":
if not os.access(pulp_cbc_path, os.X_OK):
import stat
os.chmod(pulp_cbc_path, stat.S_IXUSR + stat.S_IXOTH)
except: # probably due to incorrect permissions
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp, callback=None):
"""Solve a well formulated lp problem"""
raise PulpSolverError(
"PULP_CBC_CMD: Not Available (check permissions on %s)"
% self.pulp_cbc_path
)
else:
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
fracGap=None,
maxSeconds=None,
gapRel=None,
gapAbs=None,
presolve=None,
cuts=None,
strong=None,
options=None,
warmStart=False,
keepFiles=False,
path=None,
threads=None,
logPath=None,
mip_start=False,
timeMode="elapsed",
):
if path is not None:
raise PulpSolverError("Use COIN_CMD if you want to set a path")
# check that the file is executable
COIN_CMD.__init__(
self,
path=self.pulp_cbc_path,
mip=mip,
msg=msg,
timeLimit=timeLimit,
fracGap=fracGap,
maxSeconds=maxSeconds,
gapRel=gapRel,
gapAbs=gapAbs,
presolve=presolve,
cuts=cuts,
strong=strong,
options=options,
warmStart=warmStart,
keepFiles=keepFiles,
threads=threads,
logPath=logPath,
mip_start=mip_start,
timeMode=timeMode,
)
def COINMP_DLL_load_dll(path):
"""
function that loads the DLL useful for debugging installation problems
"""
if os.name == "nt":
lib = ctypes.windll.LoadLibrary(str(path[-1]))
else:
# linux hack to get working
mode = ctypes.RTLD_GLOBAL
for libpath in path[:-1]:
# RTLD_LAZY = 0x00001
ctypes.CDLL(libpath, mode=mode)
lib = ctypes.CDLL(path[-1], mode=mode)
return lib
class COINMP_DLL(LpSolver):
"""
The COIN_MP LP MIP solver (via a DLL or linux so)
:param timeLimit: The number of seconds before forcing the solver to exit
:param epgap: The fractional mip tolerance
"""
name = "COINMP_DLL"
try:
lib = COINMP_DLL_load_dll(coinMP_path)
except (ImportError, OSError):
@classmethod
def available(cls):
"""True if the solver is available"""
return False
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
raise PulpSolverError("COINMP_DLL: Not Available")
else:
COIN_INT_LOGLEVEL = 7
COIN_REAL_MAXSECONDS = 16
COIN_REAL_MIPMAXSEC = 19
COIN_REAL_MIPFRACGAP = 34
lib.CoinGetInfinity.restype = ctypes.c_double
lib.CoinGetVersionStr.restype = ctypes.c_char_p
lib.CoinGetSolutionText.restype = ctypes.c_char_p
lib.CoinGetObjectValue.restype = ctypes.c_double
lib.CoinGetMipBestBound.restype = ctypes.c_double
def __init__(
self,
cuts=1,
presolve=1,
dual=1,
crash=0,
scale=1,
rounding=1,
integerPresolve=1,
strong=5,
epgap=None,
*args,
**kwargs,
):
LpSolver.__init__(self, *args, **kwargs)
self.fracGap = None
if epgap is not None:
self.fracGap = float(epgap)
if self.timeLimit is not None:
self.timeLimit = float(self.timeLimit)
# Todo: these options are not yet implemented
self.cuts = cuts
self.presolve = presolve
self.dual = dual
self.crash = crash
self.scale = scale
self.rounding = rounding
self.integerPresolve = integerPresolve
self.strong = strong
def copy(self):
"""Make a copy of self"""
aCopy = LpSolver.copy(self)
aCopy.cuts = self.cuts
aCopy.presolve = self.presolve
aCopy.dual = self.dual
aCopy.crash = self.crash
aCopy.scale = self.scale
aCopy.rounding = self.rounding
aCopy.integerPresolve = self.integerPresolve
aCopy.strong = self.strong
return aCopy
@classmethod
def available(cls):
"""True if the solver is available"""
return True
def getSolverVersion(self):
"""
returns a solver version string
example:
>>> COINMP_DLL().getSolverVersion() # doctest: +ELLIPSIS
'...'
"""
return self.lib.CoinGetVersionStr()
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
# TODO alter so that msg parameter is handled correctly
self.debug = 0
# initialise solver
self.lib.CoinInitSolver("")
# create problem
self.hProb = hProb = self.lib.CoinCreateProblem(lp.name)
# set problem options
self.lib.CoinSetIntOption(
hProb, self.COIN_INT_LOGLEVEL, ctypes.c_int(self.msg)
)
if self.timeLimit:
if self.mip:
self.lib.CoinSetRealOption(
hProb, self.COIN_REAL_MIPMAXSEC, ctypes.c_double(self.timeLimit)
)
else:
self.lib.CoinSetRealOption(
hProb,
self.COIN_REAL_MAXSECONDS,
ctypes.c_double(self.timeLimit),
)
if self.fracGap:
# Hopefully this is the bound gap tolerance
self.lib.CoinSetRealOption(
hProb, self.COIN_REAL_MIPFRACGAP, ctypes.c_double(self.fracGap)
)
# CoinGetInfinity is needed for varibles with no bounds
coinDblMax = self.lib.CoinGetInfinity()
if self.debug:
print("Before getCoinMPArrays")
(
numVars,
numRows,
numels,
rangeCount,
objectSense,
objectCoeffs,
objectConst,
rhsValues,
rangeValues,
rowType,
startsBase,
lenBase,
indBase,
elemBase,
lowerBounds,
upperBounds,
initValues,
colNames,
rowNames,
columnType,
n2v,
n2c,
) = self.getCplexStyleArrays(lp)
self.lib.CoinLoadProblem(
hProb,
numVars,
numRows,
numels,
rangeCount,
objectSense,
objectConst,
objectCoeffs,
lowerBounds,
upperBounds,
rowType,
rhsValues,
rangeValues,
startsBase,
lenBase,
indBase,
elemBase,
colNames,
rowNames,
"Objective",
)
if lp.isMIP() and self.mip:
self.lib.CoinLoadInteger(hProb, columnType)
if self.msg == 0:
self.lib.CoinRegisterMsgLogCallback(
hProb, ctypes.c_char_p(""), ctypes.POINTER(ctypes.c_int)()
)
self.coinTime = -clock()
self.lib.CoinOptimizeProblem(hProb, 0)
self.coinTime += clock()
# TODO: check Integer Feasible status
CoinLpStatus = {
0: constants.LpStatusOptimal,
1: constants.LpStatusInfeasible,
2: constants.LpStatusInfeasible,
3: constants.LpStatusNotSolved,
4: constants.LpStatusNotSolved,
5: constants.LpStatusNotSolved,
-1: constants.LpStatusUndefined,
}
solutionStatus = self.lib.CoinGetSolutionStatus(hProb)
solutionText = self.lib.CoinGetSolutionText(hProb)
objectValue = self.lib.CoinGetObjectValue(hProb)
# get the solution values
NumVarDoubleArray = ctypes.c_double * numVars
NumRowsDoubleArray = ctypes.c_double * numRows
cActivity = NumVarDoubleArray()
cReducedCost = NumVarDoubleArray()
cSlackValues = NumRowsDoubleArray()
cShadowPrices = NumRowsDoubleArray()
self.lib.CoinGetSolutionValues(
hProb,
ctypes.byref(cActivity),
ctypes.byref(cReducedCost),
ctypes.byref(cSlackValues),
ctypes.byref(cShadowPrices),
)
variablevalues = {}
variabledjvalues = {}
constraintpivalues = {}
constraintslackvalues = {}
if lp.isMIP() and self.mip:
lp.bestBound = self.lib.CoinGetMipBestBound(hProb)
for i in range(numVars):
variablevalues[self.n2v[i].name] = cActivity[i]
variabledjvalues[self.n2v[i].name] = cReducedCost[i]
lp.assignVarsVals(variablevalues)
lp.assignVarsDj(variabledjvalues)
# put pi and slack variables against the constraints
for i in range(numRows):
constraintpivalues[self.n2c[i]] = cShadowPrices[i]
constraintslackvalues[self.n2c[i]] = cSlackValues[i]
lp.assignConsPi(constraintpivalues)
lp.assignConsSlack(constraintslackvalues)
self.lib.CoinFreeSolver()
status = CoinLpStatus[self.lib.CoinGetSolutionStatus(hProb)]
lp.assignStatus(status)
return status
if COINMP_DLL.available():
COIN = COINMP_DLL
yaposib = None
class YAPOSIB(LpSolver):
"""
COIN OSI (via its python interface)
Copyright Christophe-Marie Duquesne 2012
The yaposib variables are available (after a solve) in var.solverVar
The yaposib constraints are available in constraint.solverConstraint
The Model is in prob.solverModel
"""
name = "YAPOSIB"
try:
# import the model into the global scope
global yaposib
import yaposib
except ImportError:
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp, callback=None):
"""Solve a well formulated lp problem"""
raise PulpSolverError("YAPOSIB: Not Available")
else:
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
epgap=None,
solverName=None,
**solverParams,
):
"""
Initializes the yaposib solver.
@param mip: if False the solver will solve a MIP as
an LP
@param msg: displays information from the solver to
stdout
@param timeLimit: not supported
@param epgap: not supported
@param solverParams: not supported
"""
LpSolver.__init__(self, mip, msg)
if solverName:
self.solverName = solverName
else:
self.solverName = yaposib.available_solvers()[0]
def findSolutionValues(self, lp):
model = lp.solverModel
solutionStatus = model.status
yaposibLpStatus = {
"optimal": constants.LpStatusOptimal,
"undefined": constants.LpStatusUndefined,
"abandoned": constants.LpStatusInfeasible,
"infeasible": constants.LpStatusInfeasible,
"limitreached": constants.LpStatusInfeasible,
}
# populate pulp solution values
for var in lp.variables():
var.varValue = var.solverVar.solution
var.dj = var.solverVar.reducedcost
# put pi and slack variables against the constraints
for constr in lp.constraints.values():
constr.pi = constr.solverConstraint.dual
constr.slack = -constr.constant - constr.solverConstraint.activity
if self.msg:
print("yaposib status=", solutionStatus)
lp.resolveOK = True
for var in lp.variables():
var.isModified = False
status = yaposibLpStatus.get(solutionStatus, constants.LpStatusUndefined)
lp.assignStatus(status)
return status
def available(self):
"""True if the solver is available"""
return True
def callSolver(self, lp, callback=None):
"""Solves the problem with yaposib"""
savestdout = None
if self.msg == 0:
# close stdout to get rid of messages
tempfile = open(mktemp(), "w")
savestdout = os.dup(1)
os.close(1)
if os.dup(tempfile.fileno()) != 1:
raise PulpSolverError("couldn't redirect stdout - dup() error")
self.solveTime = -clock()
lp.solverModel.solve(self.mip)
self.solveTime += clock()
if self.msg == 0:
# reopen stdout
os.close(1)
os.dup(savestdout)
os.close(savestdout)
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into a yaposib model
"""
log.debug("create the yaposib model")
lp.solverModel = yaposib.Problem(self.solverName)
prob = lp.solverModel
prob.name = lp.name
log.debug("set the sense of the problem")
if lp.sense == constants.LpMaximize:
prob.obj.maximize = True
log.debug("add the variables to the problem")
for var in lp.variables():
col = prob.cols.add(yaposib.vec([]))
col.name = var.name
if not var.lowBound is None:
col.lowerbound = var.lowBound
if not var.upBound is None:
col.upperbound = var.upBound
if var.cat == constants.LpInteger:
col.integer = True
prob.obj[col.index] = lp.objective.get(var, 0.0)
var.solverVar = col
log.debug("add the Constraints to the problem")
for name, constraint in lp.constraints.items():
row = prob.rows.add(
yaposib.vec(
[
(var.solverVar.index, value)
for var, value in constraint.items()
]
)
)
if constraint.sense == constants.LpConstraintLE:
row.upperbound = -constraint.constant
elif constraint.sense == constants.LpConstraintGE:
row.lowerbound = -constraint.constant
elif constraint.sense == constants.LpConstraintEQ:
row.upperbound = -constraint.constant
row.lowerbound = -constraint.constant
else:
raise PulpSolverError("Detected an invalid constraint type")
row.name = name
constraint.solverConstraint = row
def actualSolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
creates a yaposib model, variables and constraints and attaches
them to the lp model which it then solves
"""
self.buildSolverModel(lp)
# set the initial solution
log.debug("Solve the model using yaposib")
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp.variables():
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
def actualResolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
uses the old solver and modifies the rhs of the modified
constraints
"""
log.debug("Resolve the model using yaposib")
for constraint in lp.constraints.values():
row = constraint.solverConstraint
if constraint.modified:
if constraint.sense == constants.LpConstraintLE:
row.upperbound = -constraint.constant
elif constraint.sense == constants.LpConstraintGE:
row.lowerbound = -constraint.constant
elif constraint.sense == constants.LpConstraintEQ:
row.upperbound = -constraint.constant
row.lowerbound = -constraint.constant
else:
raise PulpSolverError("Detected an invalid constraint type")
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp.variables():
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus

1090
utils/pulp/apis/copt_api.py Normal file

File diff suppressed because it is too large Load Diff

493
utils/pulp/apis/core.py Normal file
View File

@@ -0,0 +1,493 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
"""
This file contains the solver classes for PuLP
Note that the solvers that require a compiled extension may not work in
the current version
"""
import os
import platform
import shutil
import sys
import ctypes
from time import monotonic as clock
import configparser
from typing import Union
Parser = configparser.ConfigParser
from .. import sparse
from .. import constants as const
import logging
try:
import ujson as json
except ImportError:
import json
log = logging.getLogger(__name__)
import subprocess
devnull = subprocess.DEVNULL
to_string = lambda _obj: str(_obj).encode()
from uuid import uuid4
class PulpSolverError(const.PulpError):
"""
Pulp Solver-related exceptions
"""
pass
# import configuration information
def initialize(filename, operating_system="linux", arch="64"):
"""reads the configuration file to initialise the module"""
here = os.path.dirname(filename)
config = Parser({"here": here, "os": operating_system, "arch": arch})
config.read(filename)
try:
cplex_dll_path = config.get("locations", "CplexPath")
except configparser.Error:
cplex_dll_path = "libcplex110.so"
try:
try:
ilm_cplex_license = (
config.get("licenses", "ilm_cplex_license")
.decode("string-escape")
.replace('"', "")
)
except AttributeError:
ilm_cplex_license = config.get("licenses", "ilm_cplex_license").replace(
'"', ""
)
except configparser.Error:
ilm_cplex_license = ""
try:
ilm_cplex_license_signature = config.getint(
"licenses", "ilm_cplex_license_signature"
)
except configparser.Error:
ilm_cplex_license_signature = 0
try:
coinMP_path = config.get("locations", "CoinMPPath").split(", ")
except configparser.Error:
coinMP_path = ["libCoinMP.so"]
try:
gurobi_path = config.get("locations", "GurobiPath")
except configparser.Error:
gurobi_path = "/opt/gurobi201/linux32/lib/python2.5"
try:
cbc_path = config.get("locations", "CbcPath")
except configparser.Error:
cbc_path = "cbc"
try:
glpk_path = config.get("locations", "GlpkPath")
except configparser.Error:
glpk_path = "glpsol"
try:
pulp_cbc_path = config.get("locations", "PulpCbcPath")
except configparser.Error:
pulp_cbc_path = "cbc"
try:
scip_path = config.get("locations", "ScipPath")
except configparser.Error:
scip_path = "scip"
try:
fscip_path = config.get("locations", "FscipPath")
except configparser.Error:
fscip_path = "fscip"
for i, path in enumerate(coinMP_path):
if not os.path.dirname(path):
# if no pathname is supplied assume the file is in the same directory
coinMP_path[i] = os.path.join(os.path.dirname(config_filename), path)
return (
cplex_dll_path,
ilm_cplex_license,
ilm_cplex_license_signature,
coinMP_path,
gurobi_path,
cbc_path,
glpk_path,
pulp_cbc_path,
scip_path,
fscip_path,
)
# pick up the correct config file depending on operating system
PULPCFGFILE = "pulp.cfg"
is_64bits = sys.maxsize > 2**32
if is_64bits:
arch = "64"
if platform.machine().lower() in ["aarch64", "arm64"]:
arch = "arm64"
else:
arch = "32"
operating_system = None
if sys.platform in ["win32", "cli"]:
operating_system = "win"
PULPCFGFILE += ".win"
elif sys.platform in ["darwin"]:
operating_system = "osx"
arch = "64"
PULPCFGFILE += ".osx"
else:
operating_system = "linux"
PULPCFGFILE += ".linux"
DIRNAME = os.path.dirname(__file__)
config_filename = os.path.normpath(os.path.join(DIRNAME, "..", PULPCFGFILE))
(
cplex_dll_path,
ilm_cplex_license,
ilm_cplex_license_signature,
coinMP_path,
gurobi_path,
cbc_path,
glpk_path,
pulp_cbc_path,
scip_path,
fscip_path,
) = initialize(config_filename, operating_system, arch)
class LpSolver:
"""A generic LP Solver"""
name = "LpSolver"
def __init__(
self, mip=True, msg=True, options=None, timeLimit=None, *args, **kwargs
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param list options:
:param float timeLimit: maximum time for solver (in seconds)
:param args:
:param kwargs: optional named options to pass to each solver,
e.g. gapRel=0.1, gapAbs=10, logPath="",
"""
if options is None:
options = []
self.mip = mip
self.msg = msg
self.options = options
self.timeLimit = timeLimit
# here we will store all other relevant information including:
# gapRel, gapAbs, maxMemory, maxNodes, threads, logPath, timeMode
self.optionsDict = {k: v for k, v in kwargs.items() if v is not None}
def available(self):
"""True if the solver is available"""
raise NotImplementedError
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
raise NotImplementedError
def actualResolve(self, lp, **kwargs):
"""
uses existing problem information and solves the problem
If it is not implemented in the solver
just solve again
"""
self.actualSolve(lp, **kwargs)
def copy(self):
"""Make a copy of self"""
aCopy = self.__class__()
aCopy.mip = self.mip
aCopy.msg = self.msg
aCopy.options = self.options
return aCopy
def solve(self, lp):
"""Solve the problem lp"""
# Always go through the solve method of LpProblem
return lp.solve(self)
# TODO: Not sure if this code should be here or in a child class
def getCplexStyleArrays(
self, lp, senseDict=None, LpVarCategories=None, LpObjSenses=None, infBound=1e20
):
"""returns the arrays suitable to pass to a cdll Cplex
or other solvers that are similar
Copyright (c) Stuart Mitchell 2007
"""
if senseDict is None:
senseDict = {
const.LpConstraintEQ: "E",
const.LpConstraintLE: "L",
const.LpConstraintGE: "G",
}
if LpVarCategories is None:
LpVarCategories = {const.LpContinuous: "C", const.LpInteger: "I"}
if LpObjSenses is None:
LpObjSenses = {const.LpMaximize: -1, const.LpMinimize: 1}
import ctypes
rangeCount = 0
variables = list(lp.variables())
numVars = len(variables)
# associate each variable with a ordinal
self.v2n = {variables[i]: i for i in range(numVars)}
self.vname2n = {variables[i].name: i for i in range(numVars)}
self.n2v = {i: variables[i] for i in range(numVars)}
# objective values
objSense = LpObjSenses[lp.sense]
NumVarDoubleArray = ctypes.c_double * numVars
objectCoeffs = NumVarDoubleArray()
# print "Get objective Values"
for v, val in lp.objective.items():
objectCoeffs[self.v2n[v]] = val
# values for variables
objectConst = ctypes.c_double(0.0)
NumVarStrArray = ctypes.c_char_p * numVars
colNames = NumVarStrArray()
lowerBounds = NumVarDoubleArray()
upperBounds = NumVarDoubleArray()
initValues = NumVarDoubleArray()
for v in lp.variables():
colNames[self.v2n[v]] = to_string(v.name)
initValues[self.v2n[v]] = 0.0
if v.lowBound != None:
lowerBounds[self.v2n[v]] = v.lowBound
else:
lowerBounds[self.v2n[v]] = -infBound
if v.upBound != None:
upperBounds[self.v2n[v]] = v.upBound
else:
upperBounds[self.v2n[v]] = infBound
# values for constraints
numRows = len(lp.constraints)
NumRowDoubleArray = ctypes.c_double * numRows
NumRowStrArray = ctypes.c_char_p * numRows
NumRowCharArray = ctypes.c_char * numRows
rhsValues = NumRowDoubleArray()
rangeValues = NumRowDoubleArray()
rowNames = NumRowStrArray()
rowType = NumRowCharArray()
self.c2n = {}
self.n2c = {}
i = 0
for c in lp.constraints:
rhsValues[i] = -lp.constraints[c].constant
# for ranged constraints a<= constraint >=b
rangeValues[i] = 0.0
rowNames[i] = to_string(c)
rowType[i] = to_string(senseDict[lp.constraints[c].sense])
self.c2n[c] = i
self.n2c[i] = c
i = i + 1
# return the coefficient matrix as a series of vectors
coeffs = lp.coefficients()
sparseMatrix = sparse.Matrix(list(range(numRows)), list(range(numVars)))
for var, row, coeff in coeffs:
sparseMatrix.add(self.c2n[row], self.vname2n[var], coeff)
(
numels,
mystartsBase,
mylenBase,
myindBase,
myelemBase,
) = sparseMatrix.col_based_arrays()
elemBase = ctypesArrayFill(myelemBase, ctypes.c_double)
indBase = ctypesArrayFill(myindBase, ctypes.c_int)
startsBase = ctypesArrayFill(mystartsBase, ctypes.c_int)
lenBase = ctypesArrayFill(mylenBase, ctypes.c_int)
# MIP Variables
NumVarCharArray = ctypes.c_char * numVars
columnType = NumVarCharArray()
if lp.isMIP():
for v in lp.variables():
columnType[self.v2n[v]] = to_string(LpVarCategories[v.cat])
self.addedVars = numVars
self.addedRows = numRows
return (
numVars,
numRows,
numels,
rangeCount,
objSense,
objectCoeffs,
objectConst,
rhsValues,
rangeValues,
rowType,
startsBase,
lenBase,
indBase,
elemBase,
lowerBounds,
upperBounds,
initValues,
colNames,
rowNames,
columnType,
self.n2v,
self.n2c,
)
def toDict(self):
data = dict(solver=self.name)
for k in ["mip", "msg", "keepFiles"]:
try:
data[k] = getattr(self, k)
except AttributeError:
pass
for k in ["timeLimit", "options"]:
# with these ones, we only export if it has some content:
try:
value = getattr(self, k)
if value:
data[k] = value
except AttributeError:
pass
data.update(self.optionsDict)
return data
to_dict = toDict
def toJson(self, filename, *args, **kwargs):
with open(filename, "w") as f:
json.dump(self.toDict(), f, *args, **kwargs)
to_json = toJson
class LpSolver_CMD(LpSolver):
"""A generic command line LP Solver"""
name = "LpSolver_CMD"
def __init__(self, path=None, keepFiles=False, *args, **kwargs):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param list options: list of additional options to pass to solver (format depends on the solver)
:param float timeLimit: maximum time for solver (in seconds)
:param str path: a path to the solver binary
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param args: parameters to pass to :py:class:`LpSolver`
:param kwargs: parameters to pass to :py:class:`LpSolver`
"""
LpSolver.__init__(self, *args, **kwargs)
if path is None:
self.path = self.defaultPath()
else:
self.path = path
self.keepFiles = keepFiles
self.setTmpDir()
def copy(self):
"""Make a copy of self"""
aCopy = LpSolver.copy(self)
aCopy.path = self.path
aCopy.keepFiles = self.keepFiles
aCopy.tmpDir = self.tmpDir
return aCopy
def setTmpDir(self):
"""Set the tmpDir attribute to a reasonnable location for a temporary
directory"""
if os.name != "nt":
# On unix use /tmp by default
self.tmpDir = os.environ.get("TMPDIR", "/tmp")
self.tmpDir = os.environ.get("TMP", self.tmpDir)
else:
# On Windows use the current directory
self.tmpDir = os.environ.get("TMPDIR", "")
self.tmpDir = os.environ.get("TMP", self.tmpDir)
self.tmpDir = os.environ.get("TEMP", self.tmpDir)
if not os.path.isdir(self.tmpDir):
self.tmpDir = ""
elif not os.access(self.tmpDir, os.F_OK + os.W_OK):
self.tmpDir = ""
def create_tmp_files(self, name, *args):
if self.keepFiles:
prefix = name
else:
prefix = os.path.join(self.tmpDir, uuid4().hex)
return (f"{prefix}-pulp.{n}" for n in args)
def silent_remove(self, file: Union[str, bytes, os.PathLike]) -> None:
try:
os.remove(file)
except FileNotFoundError:
pass
def delete_tmp_files(self, *args):
if self.keepFiles:
return
for file in args:
self.silent_remove(file)
def defaultPath(self):
raise NotImplementedError
@staticmethod
def executableExtension(name):
if os.name != "nt":
return name
else:
return name + ".exe"
@staticmethod
def executable(command):
"""Checks that the solver command is executable,
And returns the actual path to it."""
return shutil.which(command)
def ctypesArrayFill(myList, type=ctypes.c_double):
"""
Creates a c array with ctypes from a python list
type is the type of the c array
"""
ctype = type * len(myList)
cList = ctype()
for i, elem in enumerate(myList):
cList[i] = elem
return cList

View File

@@ -0,0 +1,579 @@
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
from .. import constants
import os
import warnings
class CPLEX_CMD(LpSolver_CMD):
"""The CPLEX LP solver"""
name = "CPLEX_CMD"
def __init__(
self,
timelimit=None,
mip=True,
msg=True,
timeLimit=None,
gapRel=None,
gapAbs=None,
options=None,
warmStart=False,
keepFiles=False,
path=None,
threads=None,
logPath=None,
maxMemory=None,
maxNodes=None,
mip_start=False,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int threads: sets the maximum number of threads
:param list options: list of additional options to pass to solver
:param bool warmStart: if True, the solver will use the current value of variables as a start
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param str logPath: path to the log file
:param float maxMemory: max memory to use during the solving. Stops the solving when reached.
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
:param bool mip_start: deprecated for warmStart
:param float timelimit: deprecated for timeLimit
"""
if timelimit is not None:
warnings.warn("Parameter timelimit is being depreciated for timeLimit")
if timeLimit is not None:
warnings.warn(
"Parameter timeLimit and timelimit passed, using timeLimit "
)
else:
timeLimit = timelimit
if mip_start:
warnings.warn("Parameter mip_start is being depreciated for warmStart")
if warmStart:
warnings.warn(
"Parameter mipStart and mip_start passed, using warmStart"
)
else:
warmStart = mip_start
LpSolver_CMD.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
maxMemory=maxMemory,
maxNodes=maxNodes,
warmStart=warmStart,
path=path,
keepFiles=keepFiles,
threads=threads,
gapAbs=gapAbs,
logPath=logPath,
)
def defaultPath(self):
return self.executableExtension("cplex")
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol, tmpMst = self.create_tmp_files(lp.name, "lp", "sol", "mst")
vs = lp.writeLP(tmpLp, writeSOS=1)
try:
os.remove(tmpSol)
except:
pass
if not self.msg:
cplex = subprocess.Popen(
self.path,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
else:
cplex = subprocess.Popen(self.path, stdin=subprocess.PIPE)
cplex_cmds = "read " + tmpLp + "\n"
if self.optionsDict.get("warmStart", False):
self.writesol(filename=tmpMst, vs=vs)
cplex_cmds += "read " + tmpMst + "\n"
cplex_cmds += "set advance 1\n"
if self.timeLimit is not None:
cplex_cmds += "set timelimit " + str(self.timeLimit) + "\n"
options = self.options + self.getOptions()
for option in options:
cplex_cmds += option + "\n"
if lp.isMIP():
if self.mip:
cplex_cmds += "mipopt\n"
cplex_cmds += "change problem fixed\n"
else:
cplex_cmds += "change problem lp\n"
cplex_cmds += "optimize\n"
cplex_cmds += "write " + tmpSol + "\n"
cplex_cmds += "quit\n"
cplex_cmds = cplex_cmds.encode("UTF-8")
cplex.communicate(cplex_cmds)
if cplex.returncode != 0:
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
if not os.path.exists(tmpSol):
status = constants.LpStatusInfeasible
values = reducedCosts = shadowPrices = slacks = solStatus = None
else:
(
status,
values,
reducedCosts,
shadowPrices,
slacks,
solStatus,
) = self.readsol(tmpSol)
self.delete_tmp_files(tmpLp, tmpMst, tmpSol)
if self.optionsDict.get("logPath") != "cplex.log":
self.delete_tmp_files("cplex.log")
if status != constants.LpStatusInfeasible:
lp.assignVarsVals(values)
lp.assignVarsDj(reducedCosts)
lp.assignConsPi(shadowPrices)
lp.assignConsSlack(slacks)
lp.assignStatus(status, solStatus)
return status
def getOptions(self):
# CPLEX parameters: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.6.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/settingParams.html
# CPLEX status: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/macros/Solution_status_codes.html
params_eq = dict(
logPath="set logFile {}",
gapRel="set mip tolerances mipgap {}",
gapAbs="set mip tolerances absmipgap {}",
maxMemory="set mip limits treememory {}",
threads="set threads {}",
maxNodes="set mip limits nodes {}",
)
return [
v.format(self.optionsDict[k])
for k, v in params_eq.items()
if k in self.optionsDict and self.optionsDict[k] is not None
]
def readsol(self, filename):
"""Read a CPLEX solution file"""
# CPLEX solution codes: http://www-eio.upc.es/lceio/manuals/cplex-11/html/overviewcplex/statuscodes.html
try:
import xml.etree.ElementTree as et
except ImportError:
import elementtree.ElementTree as et
solutionXML = et.parse(filename).getroot()
solutionheader = solutionXML.find("header")
statusString = solutionheader.get("solutionStatusString")
statusValue = solutionheader.get("solutionStatusValue")
cplexStatus = {
"1": constants.LpStatusOptimal, # optimal
"101": constants.LpStatusOptimal, # mip optimal
"102": constants.LpStatusOptimal, # mip optimal tolerance
"104": constants.LpStatusOptimal, # max solution limit
"105": constants.LpStatusOptimal, # node limit feasible
"107": constants.LpStatusOptimal, # time lim feasible
"109": constants.LpStatusOptimal, # fail but feasible
"113": constants.LpStatusOptimal, # abort feasible
}
if statusValue not in cplexStatus:
raise PulpSolverError(
"Unknown status returned by CPLEX: \ncode: '{}', string: '{}'".format(
statusValue, statusString
)
)
status = cplexStatus[statusValue]
# we check for integer feasible status to differentiate from optimal in solution status
cplexSolStatus = {
"104": constants.LpSolutionIntegerFeasible, # max solution limit
"105": constants.LpSolutionIntegerFeasible, # node limit feasible
"107": constants.LpSolutionIntegerFeasible, # time lim feasible
"109": constants.LpSolutionIntegerFeasible, # fail but feasible
"111": constants.LpSolutionIntegerFeasible, # memory limit feasible
"113": constants.LpSolutionIntegerFeasible, # abort feasible
}
solStatus = cplexSolStatus.get(statusValue)
shadowPrices = {}
slacks = {}
constraints = solutionXML.find("linearConstraints")
for constraint in constraints:
name = constraint.get("name")
slack = constraint.get("slack")
shadowPrice = constraint.get("dual")
try:
# See issue #508
shadowPrices[name] = float(shadowPrice)
except TypeError:
shadowPrices[name] = None
slacks[name] = float(slack)
values = {}
reducedCosts = {}
for variable in solutionXML.find("variables"):
name = variable.get("name")
value = variable.get("value")
values[name] = float(value)
reducedCost = variable.get("reducedCost")
try:
# See issue #508
reducedCosts[name] = float(reducedCost)
except TypeError:
reducedCosts[name] = None
return status, values, reducedCosts, shadowPrices, slacks, solStatus
def writesol(self, filename, vs):
"""Writes a CPLEX solution file"""
try:
import xml.etree.ElementTree as et
except ImportError:
import elementtree.ElementTree as et
root = et.Element("CPLEXSolution", version="1.2")
attrib_head = dict()
attrib_quality = dict()
et.SubElement(root, "header", attrib=attrib_head)
et.SubElement(root, "header", attrib=attrib_quality)
variables = et.SubElement(root, "variables")
values = [(v.name, v.value()) for v in vs if v.value() is not None]
for index, (name, value) in enumerate(values):
attrib_vars = dict(name=name, value=str(value), index=str(index))
et.SubElement(variables, "variable", attrib=attrib_vars)
mst = et.ElementTree(root)
mst.write(filename, encoding="utf-8", xml_declaration=True)
return True
class CPLEX_PY(LpSolver):
"""
The CPLEX LP/MIP solver (via a Python Binding)
This solver wraps the python api of cplex.
It has been tested against cplex 12.3.
For api functions that have not been wrapped in this solver please use
the base cplex classes
"""
name = "CPLEX_PY"
try:
global cplex
import cplex
except Exception as e:
err = e
"""The CPLEX LP/MIP solver from python. Something went wrong!!!!"""
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
raise PulpSolverError(f"CPLEX_PY: Not Available:\n{self.err}")
else:
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
gapRel=None,
warmStart=False,
logPath=None,
epgap=None,
logfilename=None,
threads=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param bool warmStart: if True, the solver will use the current value of variables as a start
:param str logPath: path to the log file
:param float epgap: deprecated for gapRel
:param str logfilename: deprecated for logPath
:param int threads: number of threads to be used by CPLEX to solve a problem (default None uses all available)
"""
if epgap is not None:
warnings.warn("Parameter epgap is being depreciated for gapRel")
if gapRel is not None:
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
else:
gapRel = epgap
if logfilename is not None:
warnings.warn("Parameter logfilename is being depreciated for logPath")
if logPath is not None:
warnings.warn(
"Parameter logPath and logfilename passed, using logPath"
)
else:
logPath = logfilename
LpSolver.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
warmStart=warmStart,
logPath=logPath,
threads=threads,
)
def available(self):
"""True if the solver is available"""
return True
def actualSolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
creates a cplex model, variables and constraints and attaches
them to the lp model which it then solves
"""
self.buildSolverModel(lp)
# set the initial solution
log.debug("Solve the Model using cplex")
self.callSolver(lp)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp._variables:
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into a cplex model
"""
model_variables = lp.variables()
self.n2v = {var.name: var for var in model_variables}
if len(self.n2v) != len(model_variables):
raise PulpSolverError(
"Variables must have unique names for cplex solver"
)
log.debug("create the cplex model")
self.solverModel = lp.solverModel = cplex.Cplex()
log.debug("set the name of the problem")
if not self.mip:
self.solverModel.set_problem_name(lp.name)
log.debug("set the sense of the problem")
if lp.sense == constants.LpMaximize:
lp.solverModel.objective.set_sense(
lp.solverModel.objective.sense.maximize
)
obj = [float(lp.objective.get(var, 0.0)) for var in model_variables]
def cplex_var_lb(var):
if var.lowBound is not None:
return float(var.lowBound)
else:
return -cplex.infinity
lb = [cplex_var_lb(var) for var in model_variables]
def cplex_var_ub(var):
if var.upBound is not None:
return float(var.upBound)
else:
return cplex.infinity
ub = [cplex_var_ub(var) for var in model_variables]
colnames = [var.name for var in model_variables]
def cplex_var_types(var):
if var.cat == constants.LpInteger:
return "I"
else:
return "C"
ctype = [cplex_var_types(var) for var in model_variables]
ctype = "".join(ctype)
lp.solverModel.variables.add(
obj=obj, lb=lb, ub=ub, types=ctype, names=colnames
)
rows = []
senses = []
rhs = []
rownames = []
for name, constraint in lp.constraints.items():
# build the expression
expr = [(var.name, float(coeff)) for var, coeff in constraint.items()]
if not expr:
# if the constraint is empty
rows.append(([], []))
else:
rows.append(list(zip(*expr)))
if constraint.sense == constants.LpConstraintLE:
senses.append("L")
elif constraint.sense == constants.LpConstraintGE:
senses.append("G")
elif constraint.sense == constants.LpConstraintEQ:
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
)
log.debug("set the type of the problem")
if not self.mip:
self.solverModel.set_problem_type(cplex.Cplex.problem_type.LP)
log.debug("set the logging")
if not self.msg:
self.setlogfile(None)
logPath = self.optionsDict.get("logPath")
if logPath is not None:
if self.msg:
warnings.warn(
"`logPath` argument replaces `msg=1`. The output will be redirected to the log file."
)
self.setlogfile(open(logPath, "w"))
gapRel = self.optionsDict.get("gapRel")
if gapRel is not None:
self.changeEpgap(gapRel)
if self.timeLimit is not None:
self.setTimeLimit(self.timeLimit)
self.setThreads(self.optionsDict.get("threads", None))
if self.optionsDict.get("warmStart", False):
# We assume "auto" for the effort_level
effort = self.solverModel.MIP_starts.effort_level.auto
start = [
(k, v.value()) for k, v in self.n2v.items() if v.value() is not None
]
if not start:
warnings.warn("No variable with value found: mipStart aborted")
return
ind, val = zip(*start)
self.solverModel.MIP_starts.add(
cplex.SparsePair(ind=ind, val=val), effort, "1"
)
def setlogfile(self, fileobj):
"""
sets the logfile for cplex output
"""
self.solverModel.set_error_stream(fileobj)
self.solverModel.set_log_stream(fileobj)
self.solverModel.set_warning_stream(fileobj)
self.solverModel.set_results_stream(fileobj)
def setThreads(self, threads=None):
"""
Change cplex thread count used (None is default which uses all available resources)
"""
self.solverModel.parameters.threads.set(threads or 0)
def changeEpgap(self, epgap=10**-4):
"""
Change cplex solver integer bound gap tolerence
"""
self.solverModel.parameters.mip.tolerances.mipgap.set(epgap)
def setTimeLimit(self, timeLimit=0.0):
"""
Make cplex limit the time it takes --added CBM 8/28/09
"""
self.solverModel.parameters.timelimit.set(timeLimit)
def callSolver(self, isMIP):
"""Solves the problem with cplex"""
# solve the problem
self.solveTime = -clock()
self.solverModel.solve()
self.solveTime += clock()
def findSolutionValues(self, lp):
CplexLpStatus = {
lp.solverModel.solution.status.MIP_optimal: constants.LpStatusOptimal,
lp.solverModel.solution.status.optimal: constants.LpStatusOptimal,
lp.solverModel.solution.status.optimal_tolerance: constants.LpStatusOptimal,
lp.solverModel.solution.status.infeasible: constants.LpStatusInfeasible,
lp.solverModel.solution.status.infeasible_or_unbounded: constants.LpStatusInfeasible,
lp.solverModel.solution.status.MIP_infeasible: constants.LpStatusInfeasible,
lp.solverModel.solution.status.MIP_infeasible_or_unbounded: constants.LpStatusInfeasible,
lp.solverModel.solution.status.unbounded: constants.LpStatusUnbounded,
lp.solverModel.solution.status.MIP_unbounded: constants.LpStatusUnbounded,
lp.solverModel.solution.status.abort_dual_obj_limit: constants.LpStatusNotSolved,
lp.solverModel.solution.status.abort_iteration_limit: constants.LpStatusNotSolved,
lp.solverModel.solution.status.abort_obj_limit: constants.LpStatusNotSolved,
lp.solverModel.solution.status.abort_relaxed: constants.LpStatusNotSolved,
lp.solverModel.solution.status.abort_time_limit: constants.LpStatusNotSolved,
lp.solverModel.solution.status.abort_user: constants.LpStatusNotSolved,
lp.solverModel.solution.status.MIP_abort_feasible: constants.LpStatusOptimal,
lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpStatusOptimal,
lp.solverModel.solution.status.MIP_time_limit_infeasible: constants.LpStatusInfeasible,
}
lp.cplex_status = lp.solverModel.solution.get_status()
status = CplexLpStatus.get(lp.cplex_status, constants.LpStatusUndefined)
CplexSolStatus = {
lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpSolutionIntegerFeasible,
lp.solverModel.solution.status.MIP_abort_feasible: constants.LpSolutionIntegerFeasible,
lp.solverModel.solution.status.MIP_feasible: constants.LpSolutionIntegerFeasible,
}
# TODO: I did not find the following status: CPXMIP_NODE_LIM_FEAS, CPXMIP_MEM_LIM_FEAS
sol_status = CplexSolStatus.get(lp.cplex_status)
lp.assignStatus(status, sol_status)
var_names = [var.name for var in lp._variables]
con_names = [con for con in lp.constraints]
try:
objectiveValue = lp.solverModel.solution.get_objective_value()
variablevalues = dict(
zip(var_names, lp.solverModel.solution.get_values(var_names))
)
lp.assignVarsVals(variablevalues)
constraintslackvalues = dict(
zip(con_names, lp.solverModel.solution.get_linear_slacks(con_names))
)
lp.assignConsSlack(constraintslackvalues)
if lp.solverModel.get_problem_type() == cplex.Cplex.problem_type.LP:
variabledjvalues = dict(
zip(
var_names,
lp.solverModel.solution.get_reduced_costs(var_names),
)
)
lp.assignVarsDj(variabledjvalues)
constraintpivalues = dict(
zip(
con_names,
lp.solverModel.solution.get_dual_values(con_names),
)
)
lp.assignConsPi(constraintpivalues)
except cplex.exceptions.CplexSolverError:
# raises this error when there is no solution
pass
# put pi and slack variables against the constraints
# TODO: clear up the name of self.n2c
if self.msg:
print("Cplex status=", lp.cplex_status)
lp.resolveOK = True
for var in lp._variables:
var.isModified = False
return status
def actualResolve(self, lp, **kwargs):
"""
looks at which variables have been modified and changes them
"""
raise NotImplementedError("Resolves in CPLEX_PY not yet implemented")
CPLEX = CPLEX_CMD

409
utils/pulp/apis/glpk_api.py Normal file
View File

@@ -0,0 +1,409 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock
from .core import glpk_path, operating_system, log
import os
from .. import constants
class GLPK_CMD(LpSolver_CMD):
"""The GLPK LP solver"""
name = "GLPK_CMD"
def __init__(
self,
path=None,
keepFiles=False,
mip=True,
msg=True,
options=None,
timeLimit=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param list options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
path=path,
keepFiles=keepFiles,
)
def defaultPath(self):
return self.executableExtension(glpk_path)
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol = self.create_tmp_files(lp.name, "lp", "sol")
lp.writeLP(tmpLp, writeSOS=0)
proc = ["glpsol", "--cpxlp", tmpLp, "-o", tmpSol]
if self.timeLimit:
proc.extend(["--tmlim", str(self.timeLimit)])
if not self.mip:
proc.append("--nomip")
proc.extend(self.options)
self.solution_time = clock()
if not self.msg:
proc[0] = self.path
pipe = open(os.devnull, "w")
if operating_system == "win":
# Prevent flashing windows if used from a GUI application
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
rc = subprocess.call(
proc, stdout=pipe, stderr=pipe, startupinfo=startupinfo
)
else:
rc = subprocess.call(proc, stdout=pipe, stderr=pipe)
if rc:
raise PulpSolverError(
"PuLP: Error while trying to execute " + self.path
)
pipe.close()
else:
if os.name != "nt":
rc = os.spawnvp(os.P_WAIT, self.path, proc)
else:
rc = os.spawnv(os.P_WAIT, self.executable(self.path), proc)
if rc == 127:
raise PulpSolverError(
"PuLP: Error while trying to execute " + self.path
)
self.solution_time += clock()
if not os.path.exists(tmpSol):
raise PulpSolverError("PuLP: Error while executing " + self.path)
status, values = self.readsol(tmpSol)
lp.assignVarsVals(values)
lp.assignStatus(status)
self.delete_tmp_files(tmpLp, tmpSol)
return status
def readsol(self, filename):
"""Read a GLPK solution file"""
with open(filename) as f:
f.readline()
rows = int(f.readline().split()[1])
cols = int(f.readline().split()[1])
f.readline()
statusString = f.readline()[12:-1]
glpkStatus = {
"INTEGER OPTIMAL": constants.LpStatusOptimal,
"INTEGER NON-OPTIMAL": constants.LpStatusOptimal,
"OPTIMAL": constants.LpStatusOptimal,
"INFEASIBLE (FINAL)": constants.LpStatusInfeasible,
"INTEGER UNDEFINED": constants.LpStatusUndefined,
"UNBOUNDED": constants.LpStatusUnbounded,
"UNDEFINED": constants.LpStatusUndefined,
"INTEGER EMPTY": constants.LpStatusInfeasible,
}
if statusString not in glpkStatus:
raise PulpSolverError("Unknown status returned by GLPK")
status = glpkStatus[statusString]
isInteger = statusString in [
"INTEGER NON-OPTIMAL",
"INTEGER OPTIMAL",
"INTEGER UNDEFINED",
"INTEGER EMPTY",
]
values = {}
for i in range(4):
f.readline()
for i in range(rows):
line = f.readline().split()
if len(line) == 2:
f.readline()
for i in range(3):
f.readline()
for i in range(cols):
line = f.readline().split()
name = line[1]
if len(line) == 2:
line = [0, 0] + f.readline().split()
if isInteger:
if line[2] == "*":
value = int(float(line[3]))
else:
value = float(line[2])
else:
value = float(line[3])
values[name] = value
return status, values
GLPK = GLPK_CMD
# get the glpk name in global scope
glpk = None
class PYGLPK(LpSolver):
"""
The glpk LP/MIP solver (via its python interface)
Copyright Christophe-Marie Duquesne 2012
The glpk variables are available (after a solve) in var.solverVar
The glpk constraints are available in constraint.solverConstraint
The Model is in prob.solverModel
"""
name = "PYGLPK"
try:
# import the model into the global scope
global glpk
import glpk.glpkpi as glpk
except:
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp, callback=None):
"""Solve a well formulated lp problem"""
raise PulpSolverError("GLPK: Not Available")
else:
def __init__(
self, mip=True, msg=True, timeLimit=None, epgap=None, **solverParams
):
"""
Initializes the glpk solver.
@param mip: if False the solver will solve a MIP as an LP
@param msg: displays information from the solver to stdout
@param timeLimit: not handled
@param epgap: not handled
@param solverParams: not handled
"""
LpSolver.__init__(self, mip, msg)
if not self.msg:
glpk.glp_term_out(glpk.GLP_OFF)
def findSolutionValues(self, lp):
prob = lp.solverModel
if self.mip and self.hasMIPConstraints(lp.solverModel):
solutionStatus = glpk.glp_mip_status(prob)
else:
solutionStatus = glpk.glp_get_status(prob)
glpkLpStatus = {
glpk.GLP_OPT: constants.LpStatusOptimal,
glpk.GLP_UNDEF: constants.LpStatusUndefined,
glpk.GLP_FEAS: constants.LpStatusOptimal,
glpk.GLP_INFEAS: constants.LpStatusInfeasible,
glpk.GLP_NOFEAS: constants.LpStatusInfeasible,
glpk.GLP_UNBND: constants.LpStatusUnbounded,
}
# populate pulp solution values
for var in lp.variables():
if self.mip and self.hasMIPConstraints(lp.solverModel):
var.varValue = glpk.glp_mip_col_val(prob, var.glpk_index)
else:
var.varValue = glpk.glp_get_col_prim(prob, var.glpk_index)
var.dj = glpk.glp_get_col_dual(prob, var.glpk_index)
# put pi and slack variables against the constraints
for constr in lp.constraints.values():
if self.mip and self.hasMIPConstraints(lp.solverModel):
row_val = glpk.glp_mip_row_val(prob, constr.glpk_index)
else:
row_val = glpk.glp_get_row_prim(prob, constr.glpk_index)
constr.slack = -constr.constant - row_val
constr.pi = glpk.glp_get_row_dual(prob, constr.glpk_index)
lp.resolveOK = True
for var in lp.variables():
var.isModified = False
status = glpkLpStatus.get(solutionStatus, constants.LpStatusUndefined)
lp.assignStatus(status)
return status
def available(self):
"""True if the solver is available"""
return True
def hasMIPConstraints(self, solverModel):
return (
glpk.glp_get_num_int(solverModel) > 0
or glpk.glp_get_num_bin(solverModel) > 0
)
def callSolver(self, lp, callback=None):
"""Solves the problem with glpk"""
self.solveTime = -clock()
glpk.glp_adv_basis(lp.solverModel, 0)
glpk.glp_simplex(lp.solverModel, None)
if self.mip and self.hasMIPConstraints(lp.solverModel):
status = glpk.glp_get_status(lp.solverModel)
if status in (glpk.GLP_OPT, glpk.GLP_UNDEF, glpk.GLP_FEAS):
glpk.glp_intopt(lp.solverModel, None)
self.solveTime += clock()
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into a glpk model
"""
log.debug("create the glpk model")
prob = glpk.glp_create_prob()
glpk.glp_set_prob_name(prob, lp.name)
log.debug("set the sense of the problem")
if lp.sense == constants.LpMaximize:
glpk.glp_set_obj_dir(prob, glpk.GLP_MAX)
log.debug("add the constraints to the problem")
glpk.glp_add_rows(prob, len(list(lp.constraints.keys())))
for i, v in enumerate(lp.constraints.items(), start=1):
name, constraint = v
glpk.glp_set_row_name(prob, i, name)
if constraint.sense == constants.LpConstraintLE:
glpk.glp_set_row_bnds(
prob, i, glpk.GLP_UP, 0.0, -constraint.constant
)
elif constraint.sense == constants.LpConstraintGE:
glpk.glp_set_row_bnds(
prob, i, glpk.GLP_LO, -constraint.constant, 0.0
)
elif constraint.sense == constants.LpConstraintEQ:
glpk.glp_set_row_bnds(
prob, i, glpk.GLP_FX, -constraint.constant, -constraint.constant
)
else:
raise PulpSolverError("Detected an invalid constraint type")
constraint.glpk_index = i
log.debug("add the variables to the problem")
glpk.glp_add_cols(prob, len(lp.variables()))
for j, var in enumerate(lp.variables(), start=1):
glpk.glp_set_col_name(prob, j, var.name)
lb = 0.0
ub = 0.0
t = glpk.GLP_FR
if not var.lowBound is None:
lb = var.lowBound
t = glpk.GLP_LO
if not var.upBound is None:
ub = var.upBound
t = glpk.GLP_UP
if not var.upBound is None and not var.lowBound is None:
if ub == lb:
t = glpk.GLP_FX
else:
t = glpk.GLP_DB
glpk.glp_set_col_bnds(prob, j, t, lb, ub)
if var.cat == constants.LpInteger:
glpk.glp_set_col_kind(prob, j, glpk.GLP_IV)
assert glpk.glp_get_col_kind(prob, j) == glpk.GLP_IV
var.glpk_index = j
log.debug("set the objective function")
for var in lp.variables():
value = lp.objective.get(var)
if value:
glpk.glp_set_obj_coef(prob, var.glpk_index, value)
log.debug("set the problem matrix")
for constraint in lp.constraints.values():
l = len(list(constraint.items()))
ind = glpk.intArray(l + 1)
val = glpk.doubleArray(l + 1)
for j, v in enumerate(constraint.items(), start=1):
var, value = v
ind[j] = var.glpk_index
val[j] = value
glpk.glp_set_mat_row(prob, constraint.glpk_index, l, ind, val)
lp.solverModel = prob
# glpk.glp_write_lp(prob, None, "glpk.lp")
def actualSolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
creates a glpk model, variables and constraints and attaches
them to the lp model which it then solves
"""
self.buildSolverModel(lp)
# set the initial solution
log.debug("Solve the Model using glpk")
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp.variables():
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
def actualResolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
uses the old solver and modifies the rhs of the modified
constraints
"""
prob = lp.solverModel
log.debug("Resolve the Model using glpk")
for constraint in lp.constraints.values():
i = constraint.glpk_index
if constraint.modified:
if constraint.sense == constants.LpConstraintLE:
glpk.glp_set_row_bnds(
prob, i, glpk.GLP_UP, 0.0, -constraint.constant
)
elif constraint.sense == constants.LpConstraintGE:
glpk.glp_set_row_bnds(
prob, i, glpk.GLP_LO, -constraint.constant, 0.0
)
elif constraint.sense == constants.LpConstraintEQ:
glpk.glp_set_row_bnds(
prob,
i,
glpk.GLP_FX,
-constraint.constant,
-constraint.constant,
)
else:
raise PulpSolverError("Detected an invalid constraint type")
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp.variables():
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus

View File

@@ -0,0 +1,566 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
from .core import gurobi_path
import os
import sys
from .. import constants
import warnings
# to import the gurobipy name into the module scope
gp = None
class GUROBI(LpSolver):
"""
The Gurobi LP/MIP solver (via its python interface)
The Gurobi variables are available (after a solve) in var.solverVar
Constraints in constraint.solverConstraint
and the Model is in prob.solverModel
"""
name = "GUROBI"
env = None
try:
sys.path.append(gurobi_path)
# to import the name into the module scope
global gp
import gurobipy as gp
except: # FIXME: Bug because gurobi returns
# a gurobi exception on failed imports
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp, callback=None):
"""Solve a well formulated lp problem"""
raise PulpSolverError("GUROBI: Not Available")
else:
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
epgap=None,
gapRel=None,
warmStart=False,
logPath=None,
env=None,
envOptions=None,
manageEnv=False,
**solverParams,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param bool warmStart: if True, the solver will use the current value of variables as a start
:param str logPath: path to the log file
:param float epgap: deprecated for gapRel
:param gp.Env env: Gurobi environment to use. Default None.
:param dict envOptions: environment options.
:param bool manageEnv: if False, assume the environment is handled by the user.
If ``manageEnv`` is set to True, the ``GUROBI`` object creates a
local Gurobi environment and manages all associated Gurobi
resources. Importantly, this enables Gurobi licenses to be freed
and connections terminated when the ``.close()`` function is called
(this function always disposes of the Gurobi model, and the
environment)::
solver = GUROBI(manageEnv=True)
prob.solve(solver)
solver.close() # Must be called to free Gurobi resources.
# All Gurobi models and environments are freed
``manageEnv=True`` is required when setting license or connection
parameters. The ``envOptions`` argument is used to pass parameters
to the Gurobi environment. For example, to connect to a Gurobi
Cluster Manager::
options = {
"CSManager": "<url>",
"CSAPIAccessID": "<access-id>",
"CSAPISecret": "<api-key>",
}
solver = GUROBI(manageEnv=True, envOptions=options)
solver.close()
# Compute server connection terminated
Alternatively, one can also pass a ``gp.Env`` object. In this case,
to be safe, one should still call ``.close()`` to dispose of the
model::
with gp.Env(params=options) as env:
# Pass environment as a parameter
solver = GUROBI(env=env)
prob.solve(solver)
solver.close()
# Still call `close` as this disposes the model which is required to correctly free env
If ``manageEnv`` is set to False (the default), the ``GUROBI``
object uses the global default Gurobi environment which will be
freed once the object is deleted. In this case, one can still call
``.close()`` to dispose of the model::
solver = GUROBI()
prob.solve(solver)
# The global default environment and model remain active
solver.close()
# Only the global default environment remains active
"""
self.env = env
self.env_options = envOptions if envOptions else {}
self.manage_env = False if self.env is not None else manageEnv
self.solver_params = solverParams
self.model = None
self.init_gurobi = False # whether env and model have been initialised
if epgap is not None:
warnings.warn("Parameter epgap is being depreciated for gapRel")
if gapRel is not None:
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
else:
gapRel = epgap
LpSolver.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
gapRel=gapRel,
logPath=logPath,
warmStart=warmStart,
)
# set the output of gurobi
if not self.msg:
if self.manage_env:
self.env_options["OutputFlag"] = 0
else:
self.env_options["OutputFlag"] = 0
self.solver_params["OutputFlag"] = 0
def __del__(self):
self.close()
def close(self):
"""
Must be called when internal Gurobi model and/or environment
requires disposing. The environment (default or otherwise) will be
disposed only if ``manageEnv`` is set to True.
"""
if not self.init_gurobi:
return
self.model.dispose()
if self.manage_env:
self.env.dispose()
def findSolutionValues(self, lp):
model = lp.solverModel
solutionStatus = model.Status
GRB = gp.GRB
# TODO: check status for Integer Feasible
gurobiLpStatus = {
GRB.OPTIMAL: constants.LpStatusOptimal,
GRB.INFEASIBLE: constants.LpStatusInfeasible,
GRB.INF_OR_UNBD: constants.LpStatusInfeasible,
GRB.UNBOUNDED: constants.LpStatusUnbounded,
GRB.ITERATION_LIMIT: constants.LpStatusNotSolved,
GRB.NODE_LIMIT: constants.LpStatusNotSolved,
GRB.TIME_LIMIT: constants.LpStatusNotSolved,
GRB.SOLUTION_LIMIT: constants.LpStatusNotSolved,
GRB.INTERRUPTED: constants.LpStatusNotSolved,
GRB.NUMERIC: constants.LpStatusNotSolved,
}
if self.msg:
print("Gurobi status=", solutionStatus)
lp.resolveOK = True
for var in lp._variables:
var.isModified = False
status = gurobiLpStatus.get(solutionStatus, constants.LpStatusUndefined)
lp.assignStatus(status)
if model.SolCount >= 1:
# populate pulp solution values
for var, value in zip(
lp._variables, model.getAttr(GRB.Attr.X, model.getVars())
):
var.varValue = value
# populate pulp constraints slack
for constr, value in zip(
lp.constraints.values(),
model.getAttr(GRB.Attr.Slack, model.getConstrs()),
):
constr.slack = value
# put pi and slack variables against the constraints
if not model.IsMIP:
for var, value in zip(
lp._variables, model.getAttr(GRB.Attr.RC, model.getVars())
):
var.dj = value
for constr, value in zip(
lp.constraints.values(),
model.getAttr(GRB.Attr.Pi, model.getConstrs()),
):
constr.pi = value
return status
def available(self):
"""True if the solver is available"""
try:
with gp.Env(params=self.env_options):
pass
except gurobipy.GurobiError as e:
warnings.warn(f"GUROBI error: {e}.")
return False
return True
def initGurobi(self):
if self.init_gurobi:
return
else:
self.init_gurobi = True
try:
if self.manage_env:
self.env = gp.Env(params=self.env_options)
self.model = gp.Model(env=self.env)
# Environment handled by user or default Env
else:
self.model = gp.Model(env=self.env)
# Set solver parameters
for param, value in self.solver_params.items():
self.model.setParam(param, value)
except gp.GurobiError as e:
raise e
def callSolver(self, lp, callback=None):
"""Solves the problem with gurobi"""
# solve the problem
self.solveTime = -clock()
lp.solverModel.optimize(callback=callback)
self.solveTime += clock()
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into a gurobi model
"""
log.debug("create the gurobi model")
self.initGurobi()
self.model.ModelName = lp.name
lp.solverModel = self.model
log.debug("set the sense of the problem")
if lp.sense == constants.LpMaximize:
lp.solverModel.setAttr("ModelSense", -1)
if self.timeLimit:
lp.solverModel.setParam("TimeLimit", self.timeLimit)
gapRel = self.optionsDict.get("gapRel")
logPath = self.optionsDict.get("logPath")
if gapRel:
lp.solverModel.setParam("MIPGap", gapRel)
if logPath:
lp.solverModel.setParam("LogFile", logPath)
log.debug("add the variables to the problem")
lp.solverModel.update()
nvars = lp.solverModel.NumVars
for var in lp.variables():
lowBound = var.lowBound
if lowBound is None:
lowBound = -gp.GRB.INFINITY
upBound = var.upBound
if upBound is None:
upBound = gp.GRB.INFINITY
obj = lp.objective.get(var, 0.0)
varType = gp.GRB.CONTINUOUS
if var.cat == constants.LpInteger and self.mip:
varType = gp.GRB.INTEGER
# only add variable once, ow new variable will be created.
if not hasattr(var, "solverVar") or nvars == 0:
var.solverVar = lp.solverModel.addVar(
lowBound, upBound, vtype=varType, obj=obj, name=var.name
)
if self.optionsDict.get("warmStart", False):
# Once lp.variables() has been used at least once in the building of the model.
# we can use the lp._variables with the cache.
for var in lp._variables:
if var.varValue is not None:
var.solverVar.start = var.varValue
lp.solverModel.update()
log.debug("add the Constraints to the problem")
for name, constraint in lp.constraints.items():
# build the expression
expr = gp.LinExpr(
list(constraint.values()), [v.solverVar for v in constraint.keys()]
)
if constraint.sense == constants.LpConstraintLE:
constraint.solverConstraint = lp.solverModel.addConstr(
expr <= -constraint.constant, name=name
)
elif constraint.sense == constants.LpConstraintGE:
constraint.solverConstraint = lp.solverModel.addConstr(
expr >= -constraint.constant, name=name
)
elif constraint.sense == constants.LpConstraintEQ:
constraint.solverConstraint = lp.solverModel.addConstr(
expr == -constraint.constant, name=name
)
else:
raise PulpSolverError("Detected an invalid constraint type")
lp.solverModel.update()
def actualSolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
creates a gurobi model, variables and constraints and attaches
them to the lp model which it then solves
"""
self.buildSolverModel(lp)
# set the initial solution
log.debug("Solve the Model using gurobi")
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp._variables:
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
def actualResolve(self, lp, callback=None):
"""
Solve a well formulated lp problem
uses the old solver and modifies the rhs of the modified constraints
"""
log.debug("Resolve the Model using gurobi")
for constraint in lp.constraints.values():
if constraint.modified:
constraint.solverConstraint.setAttr(
gp.GRB.Attr.RHS, -constraint.constant
)
lp.solverModel.update()
self.callSolver(lp, callback=callback)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
for var in lp._variables:
var.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
class GUROBI_CMD(LpSolver_CMD):
"""The GUROBI_CMD solver"""
name = "GUROBI_CMD"
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
gapRel=None,
gapAbs=None,
options=None,
warmStart=False,
keepFiles=False,
path=None,
threads=None,
logPath=None,
mip_start=False,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int threads: sets the maximum number of threads
:param list options: list of additional options to pass to solver
:param bool warmStart: if True, the solver will use the current value of variables as a start
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param str logPath: path to the log file
:param bool mip_start: deprecated for warmStart
"""
if mip_start:
warnings.warn("Parameter mip_start is being depreciated for warmStart")
if warmStart:
warnings.warn(
"Parameter warmStart and mip_start passed, using warmStart"
)
else:
warmStart = mip_start
LpSolver_CMD.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
warmStart=warmStart,
path=path,
keepFiles=keepFiles,
threads=threads,
gapAbs=gapAbs,
logPath=logPath,
)
def defaultPath(self):
return self.executableExtension("gurobi_cl")
def available(self):
"""True if the solver is available"""
if not self.executable(self.path):
return False
# we execute gurobi once to check the return code.
# this is to test that the license is active
result = subprocess.Popen(
self.path, stdout=subprocess.PIPE, universal_newlines=True
)
out, err = result.communicate()
if result.returncode == 0:
# normal execution
return True
# error: we display the gurobi message
warnings.warn(f"GUROBI error: {out}.")
return False
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol, tmpMst = self.create_tmp_files(lp.name, "lp", "sol", "mst")
vs = lp.writeLP(tmpLp, writeSOS=1)
try:
os.remove(tmpSol)
except:
pass
cmd = self.path
options = self.options + self.getOptions()
if self.timeLimit is not None:
options.append(("TimeLimit", self.timeLimit))
cmd += " " + " ".join([f"{key}={value}" for key, value in options])
cmd += f" ResultFile={tmpSol}"
if self.optionsDict.get("warmStart", False):
self.writesol(filename=tmpMst, vs=vs)
cmd += f" InputFile={tmpMst}"
if lp.isMIP():
if not self.mip:
warnings.warn("GUROBI_CMD does not allow a problem to be relaxed")
cmd += f" {tmpLp}"
if self.msg:
pipe = None
else:
pipe = open(os.devnull, "w")
return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe)
# Close the pipe now if we used it.
if pipe is not None:
pipe.close()
if return_code != 0:
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
if not os.path.exists(tmpSol):
# TODO: the status should be infeasible here, I think
status = constants.LpStatusNotSolved
values = reducedCosts = shadowPrices = slacks = None
else:
# TODO: the status should be infeasible here, I think
status, values, reducedCosts, shadowPrices, slacks = self.readsol(tmpSol)
self.delete_tmp_files(tmpLp, tmpMst, tmpSol, "gurobi.log")
if status != constants.LpStatusInfeasible:
lp.assignVarsVals(values)
lp.assignVarsDj(reducedCosts)
lp.assignConsPi(shadowPrices)
lp.assignConsSlack(slacks)
lp.assignStatus(status)
return status
def readsol(self, filename):
"""Read a Gurobi solution file"""
with open(filename) as my_file:
try:
next(my_file) # skip the objective value
except StopIteration:
# Empty file not solved
status = constants.LpStatusNotSolved
return status, {}, {}, {}, {}
# We have no idea what the status is assume optimal
# TODO: check status for Integer Feasible
status = constants.LpStatusOptimal
shadowPrices = {}
slacks = {}
shadowPrices = {}
slacks = {}
values = {}
reducedCosts = {}
for line in my_file:
if line[0] != "#": # skip comments
name, value = line.split()
values[name] = float(value)
return status, values, reducedCosts, shadowPrices, slacks
def writesol(self, filename, vs):
"""Writes a GUROBI solution file"""
values = [(v.name, v.value()) for v in vs if v.value() is not None]
rows = []
for name, value in values:
rows.append(f"{name} {value}")
with open(filename, "w") as f:
f.write("\n".join(rows))
return True
def getOptions(self):
# GUROBI parameters: http://www.gurobi.com/documentation/7.5/refman/parameters.html#sec:Parameters
params_eq = dict(
logPath="LogFile",
gapRel="MIPGap",
gapAbs="MIPGapAbs",
threads="Threads",
)
return [
(v, self.optionsDict[k])
for k, v in params_eq.items()
if k in self.optionsDict and self.optionsDict[k] is not None
]

View File

@@ -0,0 +1,409 @@
# PuLP : Python LP Modeler
# Version 2.4
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
# Modified by Sam Mathew (@samiit on Github)
# Users would need to install HiGHS on their machine and provide the path to the executable. Please look at this thread: https://github.com/ERGO-Code/HiGHS/issues/527#issuecomment-894852288
# More instructions on: https://www.highs.dev
from typing import List
from .core import LpSolver, LpSolver_CMD, subprocess, PulpSolverError
import os, sys
from .. import constants
class HiGHS_CMD(LpSolver_CMD):
"""The HiGHS_CMD solver"""
name: str = "HiGHS_CMD"
SOLUTION_STYLE: int = 0
def __init__(
self,
path=None,
keepFiles=False,
mip=True,
msg=True,
options=None,
timeLimit=None,
gapRel=None,
gapAbs=None,
threads=None,
logPath=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param list[str] options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary (you can get binaries for your platform from https://github.com/JuliaBinaryWrappers/HiGHS_jll.jl/releases, or else compile from source - https://highs.dev)
:param int threads: sets the maximum number of threads
:param str logPath: path to the log file
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
gapRel=gapRel,
gapAbs=gapAbs,
options=options,
path=path,
keepFiles=keepFiles,
threads=threads,
logPath=logPath,
)
def defaultPath(self):
return self.executableExtension("highs")
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
lp.checkDuplicateVars()
tmpMps, tmpSol, tmpOptions, tmpLog = self.create_tmp_files(
lp.name, "mps", "sol", "HiGHS", "HiGHS_log"
)
lp.writeMPS(tmpMps, with_objsense=True)
file_options: List[str] = []
file_options.append(f"solution_file={tmpSol}")
file_options.append("write_solution_to_file=true")
file_options.append(f"write_solution_style={HiGHS_CMD.SOLUTION_STYLE}")
if not self.msg:
file_options.append("log_to_console=false")
if "threads" in self.optionsDict:
file_options.append(f"threads={self.optionsDict['threads']}")
if "gapRel" in self.optionsDict:
file_options.append(f"mip_rel_gap={self.optionsDict['gapRel']}")
if "gapAbs" in self.optionsDict:
file_options.append(f"mip_abs_gap={self.optionsDict['gapAbs']}")
if "logPath" in self.optionsDict:
highs_log_file = self.optionsDict["logPath"]
else:
highs_log_file = tmpLog
file_options.append(f"log_file={highs_log_file}")
command: List[str] = []
command.append(self.path)
command.append(tmpMps)
command.append(f"--options_file={tmpOptions}")
if self.timeLimit is not None:
command.append(f"--time_limit={self.timeLimit}")
if not self.mip:
command.append("--solver=simplex")
if "threads" in self.optionsDict:
command.append("--parallel=on")
options = iter(self.options)
for option in options:
# assumption: all cli and file options require an argument which is provided after the equal sign (=)
if "=" not in option:
option += f"={next(options)}"
# identify cli options by a leading dash (-) and treat other options as file options
if option.startswith("-"):
command.append(option)
else:
file_options.append(option)
with open(tmpOptions, "w") as options_file:
options_file.write("\n".join(file_options))
process = subprocess.run(command, stdout=sys.stdout, stderr=sys.stderr)
# HiGHS return code semantics (see: https://github.com/ERGO-Code/HiGHS/issues/527#issuecomment-946575028)
# - -1: error
# - 0: success
# - 1: warning
if process.returncode == -1:
raise PulpSolverError("Error while executing HiGHS")
with open(highs_log_file, "r") as log_file:
lines = log_file.readlines()
lines = [line.strip().split() for line in lines]
# LP
model_line = [line for line in lines if line[:2] == ["Model", "status"]]
if len(model_line) > 0:
model_status = " ".join(model_line[0][3:]) # Model status: ...
else:
# ILP
model_line = [line for line in lines if "Status" in line][0]
model_status = " ".join(model_line[1:])
sol_line = [line for line in lines if line[:2] == ["Solution", "status"]]
sol_line = sol_line[0] if len(sol_line) > 0 else ["Not solved"]
sol_status = sol_line[-1]
if model_status.lower() == "optimal": # optimal
status, status_sol = (
constants.LpStatusOptimal,
constants.LpSolutionOptimal,
)
elif sol_status.lower() == "feasible": # feasible
# Following the PuLP convention
status, status_sol = (
constants.LpStatusOptimal,
constants.LpSolutionIntegerFeasible,
)
elif model_status.lower() == "infeasible": # infeasible
status, status_sol = (
constants.LpStatusInfeasible,
constants.LpSolutionNoSolutionFound,
)
elif model_status.lower() == "unbounded": # unbounded
status, status_sol = (
constants.LpStatusUnbounded,
constants.LpSolutionNoSolutionFound,
)
else: # no solution
status, status_sol = (
constants.LpStatusNotSolved,
constants.LpSolutionNoSolutionFound,
)
if not os.path.exists(tmpSol) or os.stat(tmpSol).st_size == 0:
status_sol = constants.LpSolutionNoSolutionFound
values = None
elif status_sol == constants.LpSolutionNoSolutionFound:
values = None
else:
values = self.readsol(lp.variables(), tmpSol)
self.delete_tmp_files(tmpMps, tmpSol, tmpOptions, tmpLog)
lp.assignStatus(status, status_sol)
if status == constants.LpStatusOptimal:
lp.assignVarsVals(values)
return status
@staticmethod
def readsol(variables, filename):
"""Read a HiGHS solution file"""
with open(filename) as file:
lines = file.readlines()
begin, end = None, None
for index, line in enumerate(lines):
if begin is None and line.startswith("# Columns"):
begin = index + 1
if end is None and line.startswith("# Rows"):
end = index
if begin is None or end is None:
raise PulpSolverError("Cannot read HiGHS solver output")
values = {}
for line in lines[begin:end]:
name, value = line.split()
values[name] = float(value)
return values
class HiGHS(LpSolver):
name = "HiGHS"
try:
global highspy
import highspy
except:
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp, callback=None):
"""Solve a well formulated lp problem"""
raise PulpSolverError("HiGHS: Not Available")
else:
# Note(maciej): It was surprising to me that higshpy wasn't logging out of the box,
# even with the different logging options set. This callback seems to work, but there
# are probably better ways of doing this ¯\_(ツ)_/¯
DEFAULT_CALLBACK = lambda logType, logMsg, callbackValue: print(
f"[{logType.name}] {logMsg}"
)
DEFAULT_CALLBACK_VALUE = ""
def __init__(
self,
mip=True,
msg=True,
callbackTuple=None,
gapAbs=None,
gapRel=None,
threads=None,
timeLimit=None,
**solverParams,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param tuple callbackTuple: Tuple of log callback function (see DEFAULT_CALLBACK above for definition)
and callbackValue (tag embedded in every callback)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int threads: sets the maximum number of threads
:param float timeLimit: maximum time for solver (in seconds)
:param dict solverParams: list of named options to pass directly to the HiGHS solver
"""
super().__init__(mip=mip, msg=msg, timeLimit=timeLimit, **solverParams)
self.callbackTuple = callbackTuple
self.gapAbs = gapAbs
self.gapRel = gapRel
self.threads = threads
def available(self):
return True
def callSolver(self, lp):
lp.solverModel.run()
def createAndConfigureSolver(self, lp):
lp.solverModel = highspy.Highs()
if self.msg or self.callbackTuple:
callbackTuple = self.callbackTuple or (
HiGHS.DEFAULT_CALLBACK,
HiGHS.DEFAULT_CALLBACK_VALUE,
)
lp.solverModel.setLogCallback(*callbackTuple)
if self.gapRel is not None:
lp.solverModel.setOptionValue("mip_rel_gap", self.gapRel)
if self.gapAbs is not None:
lp.solverModel.setOptionValue("mip_abs_gap", self.gapAbs)
if self.threads is not None:
lp.solverModel.setOptionValue("threads", self.threads)
if self.timeLimit is not None:
lp.solverModel.setOptionValue("time_limit", float(self.timeLimit))
# set remaining parameter values
for key, value in self.optionsDict.items():
lp.solverModel.setOptionValue(key, value)
def buildSolverModel(self, lp):
inf = highspy.kHighsInf
obj_mult = -1 if lp.sense == constants.LpMaximize else 1
for i, var in enumerate(lp.variables()):
lb = var.lowBound
ub = var.upBound
lp.solverModel.addCol(
obj_mult * lp.objective.get(var, 0.0),
-inf if lb is None else lb,
inf if ub is None else ub,
0,
[],
[],
)
var.index = i
if var.cat == constants.LpInteger and self.mip:
lp.solverModel.changeColIntegrality(
var.index, highspy.HighsVarType.kInteger
)
for constraint in lp.constraints.values():
non_zero_constraint_items = [
(var.index, coefficient)
for var, coefficient in constraint.items()
if coefficient != 0
]
if len(non_zero_constraint_items) == 0:
indices, coefficients = [], []
else:
indices, coefficients = zip(*non_zero_constraint_items)
lb = constraint.getLb()
ub = constraint.getUb()
lp.solverModel.addRow(
-inf if lb is None else lb,
inf if ub is None else ub,
len(indices),
indices,
coefficients,
)
def findSolutionValues(self, lp):
status = lp.solverModel.getModelStatus()
solution = lp.solverModel.getSolution()
HighsModelStatus = highspy.HighsModelStatus
status_dict = {
HighsModelStatus.kNotset: constants.LpStatusNotSolved,
HighsModelStatus.kLoadError: constants.LpStatusNotSolved,
HighsModelStatus.kModelError: constants.LpStatusNotSolved,
HighsModelStatus.kPresolveError: constants.LpStatusNotSolved,
HighsModelStatus.kSolveError: constants.LpStatusNotSolved,
HighsModelStatus.kPostsolveError: constants.LpStatusNotSolved,
HighsModelStatus.kModelEmpty: constants.LpStatusNotSolved,
HighsModelStatus.kOptimal: constants.LpStatusOptimal,
HighsModelStatus.kInfeasible: constants.LpStatusInfeasible,
HighsModelStatus.kUnboundedOrInfeasible: constants.LpStatusInfeasible,
HighsModelStatus.kUnbounded: constants.LpStatusUnbounded,
HighsModelStatus.kObjectiveBound: constants.LpStatusNotSolved,
HighsModelStatus.kObjectiveTarget: constants.LpStatusNotSolved,
HighsModelStatus.kTimeLimit: constants.LpStatusNotSolved,
HighsModelStatus.kIterationLimit: constants.LpStatusNotSolved,
HighsModelStatus.kUnknown: constants.LpStatusNotSolved,
}
col_values = list(solution.col_value)
for var in lp.variables():
var.varValue = col_values[var.index]
return status_dict[status]
def actualSolve(self, lp):
self.createAndConfigureSolver(lp)
self.buildSolverModel(lp)
self.callSolver(lp)
solutionStatus = self.findSolutionValues(lp)
for var in lp.variables():
var.modified = False
for constraint in lp.constraints.values():
constraint.modifier = False
return solutionStatus
def actualResolve(self, lp, **kwargs):
raise PulpSolverError("HiGHS: Resolving is not supported")

View File

@@ -0,0 +1,154 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver_CMD, subprocess, PulpSolverError
import os
from .. import constants
import warnings
class MIPCL_CMD(LpSolver_CMD):
"""The MIPCL_CMD solver"""
name = "MIPCL_CMD"
def __init__(
self,
path=None,
keepFiles=False,
mip=True,
msg=True,
options=None,
timeLimit=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param list options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
path=path,
keepFiles=keepFiles,
)
def defaultPath(self):
return self.executableExtension("mps_mipcl")
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpMps, tmpSol = self.create_tmp_files(lp.name, "mps", "sol")
if lp.sense == constants.LpMaximize:
# we swap the objectives
# because it does not handle maximization.
warnings.warn(
"MIPCL_CMD does not allow maximization, "
"we will minimize the inverse of the objective function."
)
lp += -lp.objective
lp.checkDuplicateVars()
lp.checkLengthVars(52)
lp.writeMPS(tmpMps, mpsSense=lp.sense)
# just to report duplicated variables:
try:
os.remove(tmpSol)
except:
pass
cmd = self.path
cmd += f" {tmpMps}"
cmd += f" -solfile {tmpSol}"
if self.timeLimit is not None:
cmd += f" -time {self.timeLimit}"
for option in self.options:
cmd += " " + option
if lp.isMIP():
if not self.mip:
warnings.warn("MIPCL_CMD cannot solve the relaxation of a problem")
if self.msg:
pipe = None
else:
pipe = open(os.devnull, "w")
return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe)
# We need to undo the objective swap before finishing
if lp.sense == constants.LpMaximize:
lp += -lp.objective
if return_code != 0:
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
if not os.path.exists(tmpSol):
status = constants.LpStatusNotSolved
status_sol = constants.LpSolutionNoSolutionFound
values = None
else:
status, values, status_sol = self.readsol(tmpSol)
self.delete_tmp_files(tmpMps, tmpSol)
lp.assignStatus(status, status_sol)
if status not in [constants.LpStatusInfeasible, constants.LpStatusNotSolved]:
lp.assignVarsVals(values)
return status
@staticmethod
def readsol(filename):
"""Read a MIPCL solution file"""
with open(filename) as f:
content = f.readlines()
content = [l.strip() for l in content]
values = {}
if not len(content):
return (
constants.LpStatusNotSolved,
values,
constants.LpSolutionNoSolutionFound,
)
first_line = content[0]
if first_line == "=infeas=":
return constants.LpStatusInfeasible, values, constants.LpSolutionInfeasible
objective, value = first_line.split()
# this is a workaround.
# Not sure if it always returns this limit when unbounded.
if abs(float(value)) >= 9.999999995e10:
return constants.LpStatusUnbounded, values, constants.LpSolutionUnbounded
for line in content[1:]:
name, value = line.split()
values[name] = float(value)
# I'm not sure how this solver announces the optimality
# of a solution so we assume it is integer feasible
return constants.LpStatusOptimal, values, constants.LpSolutionIntegerFeasible

View File

@@ -0,0 +1,345 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver, PulpSolverError
from .. import constants
import sys
from typing import Optional
class MOSEK(LpSolver):
"""Mosek lp and mip solver (via Mosek Optimizer API)."""
name = "MOSEK"
try:
global mosek
import mosek
env = mosek.Env()
except ImportError:
def available(self):
"""True if Mosek is available."""
return False
def actualSolve(self, lp, callback=None):
"""Solves a well-formulated lp problem."""
raise PulpSolverError("MOSEK : Not Available")
else:
def __init__(
self,
mip=True,
msg=True,
timeLimit: Optional[float] = None,
options: Optional[dict] = None,
task_file_name="",
sol_type=mosek.soltype.bas,
):
"""Initializes the Mosek solver.
Keyword arguments:
@param mip: If False, then solve MIP as LP.
@param msg: Enable Mosek log output.
@param float timeLimit: maximum time for solver (in seconds)
@param options: Accepts a dictionary of Mosek solver parameters. Ignore to
use default parameter values. Eg: options = {mosek.dparam.mio_max_time:30}
sets the maximum time spent by the Mixed Integer optimizer to 30 seconds.
Equivalently, one could also write: options = {"MSK_DPAR_MIO_MAX_TIME":30}
which uses the generic parameter name as used within the solver, instead of
using an object from the Mosek Optimizer API (Python), as before.
@param task_file_name: Writes a Mosek task file of the given name. By default,
no task file will be written. Eg: task_file_name = "eg1.opf".
@param sol_type: Mosek supports three types of solutions: mosek.soltype.bas
(Basic solution, default), mosek.soltype.itr (Interior-point
solution) and mosek.soltype.itg (Integer solution).
For a full list of Mosek parameters (for the Mosek Optimizer API) and supported task file
formats, please see https://docs.mosek.com/9.1/pythonapi/parameters.html#doc-all-parameter-list.
"""
self.mip = mip
self.msg = msg
self.timeLimit = timeLimit
self.task_file_name = task_file_name
self.solution_type = sol_type
if options is None:
options = {}
self.options = options
if self.timeLimit is not None:
timeLimit_keys = {"MSK_DPAR_MIO_MAX_TIME", mosek.dparam.mio_max_time}
if not timeLimit_keys.isdisjoint(self.options.keys()):
raise ValueError(
"timeLimit parameter has been provided trough `timeLimit` and `options`."
)
self.options["MSK_DPAR_MIO_MAX_TIME"] = self.timeLimit
def available(self):
"""True if Mosek is available."""
return True
def setOutStream(self, text):
"""Sets the log-output stream."""
sys.stdout.write(text)
sys.stdout.flush()
def buildSolverModel(self, lp, inf=1e20):
"""Translate the problem into a Mosek task object."""
self.cons = lp.constraints
self.numcons = len(self.cons)
self.cons_dict = {}
i = 0
for c in self.cons:
self.cons_dict[c] = i
i = i + 1
self.vars = list(lp.variables())
self.numvars = len(self.vars)
self.var_dict = {}
# Checking for repeated names
lp.checkDuplicateVars()
self.task = MOSEK.env.Task()
self.task.appendcons(self.numcons)
self.task.appendvars(self.numvars)
if self.msg:
self.task.set_Stream(mosek.streamtype.log, self.setOutStream)
# Adding variables
for i in range(self.numvars):
vname = self.vars[i].name
self.var_dict[vname] = i
self.task.putvarname(i, vname)
# Variable type (Default: Continuous)
if self.mip & (self.vars[i].cat == constants.LpInteger):
self.task.putvartype(i, mosek.variabletype.type_int)
self.solution_type = mosek.soltype.itg
# Variable bounds
vbkey = mosek.boundkey.fr
vup = inf
vlow = -inf
if self.vars[i].lowBound != None:
vlow = self.vars[i].lowBound
if self.vars[i].upBound != None:
vup = self.vars[i].upBound
vbkey = mosek.boundkey.ra
else:
vbkey = mosek.boundkey.lo
elif self.vars[i].upBound != None:
vup = self.vars[i].upBound
vbkey = mosek.boundkey.up
self.task.putvarbound(i, vbkey, vlow, vup)
# Objective coefficient for the current variable.
self.task.putcj(i, lp.objective.get(self.vars[i], 0.0))
# Coefficient matrix
self.A_rows, self.A_cols, self.A_vals = zip(
*[
[self.cons_dict[row], self.var_dict[col], coeff]
for col, row, coeff in lp.coefficients()
]
)
self.task.putaijlist(self.A_rows, self.A_cols, self.A_vals)
# Constraints
self.constraint_data_list = []
for c in self.cons:
cname = self.cons[c].name
if cname != None:
self.task.putconname(self.cons_dict[c], cname)
else:
self.task.putconname(self.cons_dict[c], c)
csense = self.cons[c].sense
cconst = -self.cons[c].constant
clow = -inf
cup = inf
# Constraint bounds
if csense == constants.LpConstraintEQ:
cbkey = mosek.boundkey.fx
clow = cconst
cup = cconst
elif csense == constants.LpConstraintGE:
cbkey = mosek.boundkey.lo
clow = cconst
elif csense == constants.LpConstraintLE:
cbkey = mosek.boundkey.up
cup = cconst
else:
raise PulpSolverError("Invalid constraint type.")
self.constraint_data_list.append([self.cons_dict[c], cbkey, clow, cup])
self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list = zip(
*self.constraint_data_list
)
self.task.putconboundlist(
self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list
)
# Objective sense
if lp.sense == constants.LpMaximize:
self.task.putobjsense(mosek.objsense.maximize)
else:
self.task.putobjsense(mosek.objsense.minimize)
def findSolutionValues(self, lp):
"""
Read the solution values and status from the Mosek task object. Note: Since the status
map from mosek.solsta to LpStatus is not exact, it is recommended that one enables the
log output and then refer to Mosek documentation for a better understanding of the
solution (especially in the case of mip problems).
"""
self.solsta = self.task.getsolsta(self.solution_type)
self.solution_status_dict = {
mosek.solsta.optimal: constants.LpStatusOptimal,
mosek.solsta.prim_infeas_cer: constants.LpStatusInfeasible,
mosek.solsta.dual_infeas_cer: constants.LpStatusUnbounded,
mosek.solsta.unknown: constants.LpStatusUndefined,
mosek.solsta.integer_optimal: constants.LpStatusOptimal,
mosek.solsta.prim_illposed_cer: constants.LpStatusNotSolved,
mosek.solsta.dual_illposed_cer: constants.LpStatusNotSolved,
mosek.solsta.prim_feas: constants.LpStatusNotSolved,
mosek.solsta.dual_feas: constants.LpStatusNotSolved,
mosek.solsta.prim_and_dual_feas: constants.LpStatusNotSolved,
}
# Variable values.
try:
self.xx = [0.0] * self.numvars
self.task.getxx(self.solution_type, self.xx)
for var in lp.variables():
var.varValue = self.xx[self.var_dict[var.name]]
except mosek.Error:
pass
# Constraint slack variables.
try:
self.xc = [0.0] * self.numcons
self.task.getxc(self.solution_type, self.xc)
for con in lp.constraints:
lp.constraints[con].slack = -(
self.cons[con].constant + self.xc[self.cons_dict[con]]
)
except mosek.Error:
pass
# Reduced costs.
if self.solution_type != mosek.soltype.itg:
try:
self.x_rc = [0.0] * self.numvars
self.task.getreducedcosts(
self.solution_type, 0, self.numvars, self.x_rc
)
for var in lp.variables():
var.dj = self.x_rc[self.var_dict[var.name]]
except mosek.Error:
pass
# Constraint Pi variables.
try:
self.y = [0.0] * self.numcons
self.task.gety(self.solution_type, self.y)
for con in lp.constraints:
lp.constraints[con].pi = self.y[self.cons_dict[con]]
except mosek.Error:
pass
def putparam(self, par, val):
"""
Pass the values of valid parameters to Mosek.
"""
if isinstance(par, mosek.dparam):
self.task.putdouparam(par, val)
elif isinstance(par, mosek.iparam):
self.task.putintparam(par, val)
elif isinstance(par, mosek.sparam):
self.task.putstrparam(par, val)
elif isinstance(par, str):
if par.startswith("MSK_DPAR_"):
self.task.putnadouparam(par, val)
elif par.startswith("MSK_IPAR_"):
self.task.putnaintparam(par, val)
elif par.startswith("MSK_SPAR_"):
self.task.putnastrparam(par, val)
else:
raise PulpSolverError(
"Invalid MOSEK parameter: '{}'. Check MOSEK documentation for a list of valid parameters.".format(
par
)
)
def actualSolve(self, lp):
"""
Solve a well-formulated lp problem.
"""
self.buildSolverModel(lp)
# Set solver parameters
for msk_par in self.options:
self.putparam(msk_par, self.options[msk_par])
# Task file
if self.task_file_name:
self.task.writedata(self.task_file_name)
# Optimize
self.task.optimize()
# Mosek solver log (default: standard output stream)
if self.msg:
self.task.solutionsummary(mosek.streamtype.msg)
self.findSolutionValues(lp)
lp.assignStatus(self.solution_status_dict[self.solsta])
for var in lp.variables():
var.modified = False
for con in lp.constraints.values():
con.modified = False
return lp.status
def actualResolve(self, lp, inf=1e20, **kwargs):
"""
Modify constraints and re-solve an lp. The Mosek task object created in the first solve is used.
"""
for c in self.cons:
if self.cons[c].modified:
csense = self.cons[c].sense
cconst = -self.cons[c].constant
clow = -inf
cup = inf
# Constraint bounds
if csense == constants.LpConstraintEQ:
cbkey = mosek.boundkey.fx
clow = cconst
cup = cconst
elif csense == constants.LpConstraintGE:
cbkey = mosek.boundkey.lo
clow = cconst
elif csense == constants.LpConstraintLE:
cbkey = mosek.boundkey.up
cup = cconst
else:
raise PulpSolverError("Invalid constraint type.")
self.task.putconbound(self.cons_dict[c], cbkey, clow, cup)
# Re-solve
self.task.optimize()
self.findSolutionValues(lp)
lp.assignStatus(self.solution_status_dict[self.solsta])
for var in lp.variables():
var.modified = False
for con in lp.constraints.values():
con.modified = False
return lp.status

679
utils/pulp/apis/scip_api.py Normal file
View File

@@ -0,0 +1,679 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
import operator
import os
import sys
import warnings
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError
from .core import scip_path, fscip_path
from .. import constants
from typing import Dict, List, Optional, Tuple
class SCIP_CMD(LpSolver_CMD):
"""The SCIP optimization solver"""
name = "SCIP_CMD"
def __init__(
self,
path=None,
mip=True,
keepFiles=False,
msg=True,
options=None,
timeLimit=None,
gapRel=None,
gapAbs=None,
maxNodes=None,
logPath=None,
threads=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param list options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
:param int threads: sets the maximum number of threads
:param str logPath: path to the log file
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
options=options,
path=path,
keepFiles=keepFiles,
timeLimit=timeLimit,
gapRel=gapRel,
gapAbs=gapAbs,
maxNodes=maxNodes,
threads=threads,
logPath=logPath,
)
SCIP_STATUSES = {
"unknown": constants.LpStatusUndefined,
"user interrupt": constants.LpStatusNotSolved,
"node limit reached": constants.LpStatusNotSolved,
"total node limit reached": constants.LpStatusNotSolved,
"stall node limit reached": constants.LpStatusNotSolved,
"time limit reached": constants.LpStatusNotSolved,
"memory limit reached": constants.LpStatusNotSolved,
"gap limit reached": constants.LpStatusOptimal,
"solution limit reached": constants.LpStatusNotSolved,
"solution improvement limit reached": constants.LpStatusNotSolved,
"restart limit reached": constants.LpStatusNotSolved,
"optimal solution found": constants.LpStatusOptimal,
"infeasible": constants.LpStatusInfeasible,
"unbounded": constants.LpStatusUnbounded,
"infeasible or unbounded": constants.LpStatusNotSolved,
}
NO_SOLUTION_STATUSES = {
constants.LpStatusInfeasible,
constants.LpStatusUnbounded,
constants.LpStatusNotSolved,
}
def defaultPath(self):
return self.executableExtension(scip_path)
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol, tmpOptions = self.create_tmp_files(lp.name, "lp", "sol", "set")
lp.writeLP(tmpLp)
file_options: List[str] = []
if self.timeLimit is not None:
file_options.append(f"limits/time={self.timeLimit}")
if "gapRel" in self.optionsDict:
file_options.append(f"limits/gap={self.optionsDict['gapRel']}")
if "gapAbs" in self.optionsDict:
file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}")
if "maxNodes" in self.optionsDict:
file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}")
if "threads" in self.optionsDict and int(self.optionsDict["threads"]) > 1:
warnings.warn(
"SCIP can only run with a single thread - use FSCIP_CMD for a parallel version of SCIP"
)
if not self.mip:
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
command: List[str] = []
command.append(self.path)
command.extend(["-s", tmpOptions])
if not self.msg:
command.append("-q")
if "logPath" in self.optionsDict:
command.extend(["-l", self.optionsDict["logPath"]])
options = iter(self.options)
for option in options:
# identify cli options by a leading dash (-) and treat other options as file options
if option.startswith("-"):
# assumption: all cli options require an argument which is provided as a separate parameter
argument = next(options)
command.extend([option, argument])
else:
# assumption: all file options require an argument which is provided after the equal sign (=)
if "=" not in option:
argument = next(options)
option += f"={argument}"
file_options.append(option)
# append scip commands after parsing self.options to allow the user to specify additional -c arguments
command.extend(["-c", f'read "{tmpLp}"'])
command.extend(["-c", "optimize"])
command.extend(["-c", f'write solution "{tmpSol}"'])
command.extend(["-c", "quit"])
with open(tmpOptions, "w") as options_file:
options_file.write("\n".join(file_options))
subprocess.check_call(command, stdout=sys.stdout, stderr=sys.stderr)
if not os.path.exists(tmpSol):
raise PulpSolverError("PuLP: Error while executing " + self.path)
status, values = self.readsol(tmpSol)
# Make sure to add back in any 0-valued variables SCIP leaves out.
finalVals = {}
for v in lp.variables():
finalVals[v.name] = values.get(v.name, 0.0)
lp.assignVarsVals(finalVals)
lp.assignStatus(status)
self.delete_tmp_files(tmpLp, tmpSol, tmpOptions)
return status
@staticmethod
def readsol(filename):
"""Read a SCIP solution file"""
with open(filename) as f:
# First line must contain 'solution status: <something>'
try:
line = f.readline()
comps = line.split(": ")
assert comps[0] == "solution status"
assert len(comps) == 2
except Exception:
raise PulpSolverError(f"Can't get SCIP solver status: {line!r}")
status = SCIP_CMD.SCIP_STATUSES.get(
comps[1].strip(), constants.LpStatusUndefined
)
values = {}
if status in SCIP_CMD.NO_SOLUTION_STATUSES:
return status, values
# Look for an objective value. If we can't find one, stop.
try:
line = f.readline()
comps = line.split(": ")
assert comps[0] == "objective value"
assert len(comps) == 2
float(comps[1].strip())
except Exception:
raise PulpSolverError(f"Can't get SCIP solver objective: {line!r}")
# Parse the variable values.
for line in f:
try:
comps = line.split()
values[comps[0]] = float(comps[1])
except:
raise PulpSolverError(f"Can't read SCIP solver output: {line!r}")
return status, values
SCIP = SCIP_CMD
class FSCIP_CMD(LpSolver_CMD):
"""The multi-threaded FiberSCIP version of the SCIP optimization solver"""
name = "FSCIP_CMD"
def __init__(
self,
path=None,
mip=True,
keepFiles=False,
msg=True,
options=None,
timeLimit=None,
gapRel=None,
gapAbs=None,
maxNodes=None,
threads=None,
logPath=None,
):
"""
:param bool msg: if False, no log is shown
:param bool mip: if False, assume LP even if integer variables
:param list options: list of additional options to pass to solver
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
:param str path: path to the solver binary
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
:param int threads: sets the maximum number of threads
:param str logPath: path to the log file
"""
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
options=options,
path=path,
keepFiles=keepFiles,
timeLimit=timeLimit,
gapRel=gapRel,
gapAbs=gapAbs,
maxNodes=maxNodes,
threads=threads,
logPath=logPath,
)
FSCIP_STATUSES = {
"No Solution": constants.LpStatusNotSolved,
"Final Solution": constants.LpStatusOptimal,
}
NO_SOLUTION_STATUSES = {
constants.LpStatusInfeasible,
constants.LpStatusUnbounded,
constants.LpStatusNotSolved,
}
def defaultPath(self):
return self.executableExtension(fscip_path)
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol, tmpOptions, tmpParams = self.create_tmp_files(
lp.name, "lp", "sol", "set", "prm"
)
lp.writeLP(tmpLp)
file_options: List[str] = []
if self.timeLimit is not None:
file_options.append(f"limits/time={self.timeLimit}")
if "gapRel" in self.optionsDict:
file_options.append(f"limits/gap={self.optionsDict['gapRel']}")
if "gapAbs" in self.optionsDict:
file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}")
if "maxNodes" in self.optionsDict:
file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}")
if not self.mip:
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
file_parameters: List[str] = []
# disable presolving in the LoadCoordinator to make sure a solution file is always written
file_parameters.append("NoPreprocessingInLC = TRUE")
command: List[str] = []
command.append(self.path)
command.append(tmpParams)
command.append(tmpLp)
command.extend(["-s", tmpOptions])
command.extend(["-fsol", tmpSol])
if not self.msg:
command.append("-q")
if "logPath" in self.optionsDict:
command.extend(["-l", self.optionsDict["logPath"]])
if "threads" in self.optionsDict:
command.extend(["-sth", f"{self.optionsDict['threads']}"])
options = iter(self.options)
for option in options:
# identify cli options by a leading dash (-) and treat other options as file options
if option.startswith("-"):
# assumption: all cli options require an argument which is provided as a separate parameter
argument = next(options)
command.extend([option, argument])
else:
# assumption: all file options contain a slash (/)
is_file_options = "/" in option
# assumption: all file options and parameters require an argument which is provided after the equal sign (=)
if "=" not in option:
argument = next(options)
option += f"={argument}"
if is_file_options:
file_options.append(option)
else:
file_parameters.append(option)
# wipe the solution file since FSCIP does not overwrite it if no solution was found which causes parsing errors
self.silent_remove(tmpSol)
with open(tmpOptions, "w") as options_file:
options_file.write("\n".join(file_options))
with open(tmpParams, "w") as parameters_file:
parameters_file.write("\n".join(file_parameters))
subprocess.check_call(
command,
stdout=sys.stdout if self.msg else subprocess.DEVNULL,
stderr=sys.stderr if self.msg else subprocess.DEVNULL,
)
if not os.path.exists(tmpSol):
raise PulpSolverError("PuLP: Error while executing " + self.path)
status, values = self.readsol(tmpSol)
# Make sure to add back in any 0-valued variables SCIP leaves out.
finalVals = {}
for v in lp.variables():
finalVals[v.name] = values.get(v.name, 0.0)
lp.assignVarsVals(finalVals)
lp.assignStatus(status)
self.delete_tmp_files(tmpLp, tmpSol, tmpOptions, tmpParams)
return status
@staticmethod
def parse_status(string: str) -> Optional[int]:
for fscip_status, pulp_status in FSCIP_CMD.FSCIP_STATUSES.items():
if fscip_status in string:
return pulp_status
return None
@staticmethod
def parse_objective(string: str) -> Optional[float]:
fields = string.split(":")
if len(fields) != 2:
return None
label, objective = fields
if label != "objective value":
return None
objective = objective.strip()
try:
objective = float(objective)
except ValueError:
return None
return objective
@staticmethod
def parse_variable(string: str) -> Optional[Tuple[str, float]]:
fields = string.split()
if len(fields) < 2:
return None
name, value = fields[:2]
try:
value = float(value)
except ValueError:
return None
return name, value
@staticmethod
def readsol(filename):
"""Read a FSCIP solution file"""
with open(filename) as file:
# First line must contain a solution status
status_line = file.readline()
status = FSCIP_CMD.parse_status(status_line)
if status is None:
raise PulpSolverError(f"Can't get FSCIP solver status: {status_line!r}")
if status in FSCIP_CMD.NO_SOLUTION_STATUSES:
return status, {}
# Look for an objective value. If we can't find one, stop.
objective_line = file.readline()
objective = FSCIP_CMD.parse_objective(objective_line)
if objective is None:
raise PulpSolverError(
f"Can't get FSCIP solver objective: {objective_line!r}"
)
# Parse the variable values.
variables: Dict[str, float] = {}
for variable_line in file:
variable = FSCIP_CMD.parse_variable(variable_line)
if variable is None:
raise PulpSolverError(
f"Can't read FSCIP solver output: {variable_line!r}"
)
name, value = variable
variables[name] = value
return status, variables
FSCIP = FSCIP_CMD
class SCIP_PY(LpSolver):
"""
The SCIP Optimization Suite (via its python interface)
The SCIP internals are available after calling solve as:
- each variable in variable.solverVar
- each constraint in constraint.solverConstraint
- the model in problem.solverModel
"""
name = "SCIP_PY"
try:
global scip
import pyscipopt as scip
except ImportError:
def available(self):
"""True if the solver is available"""
return False
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
raise PulpSolverError(f"The {self.name} solver is not available")
else:
def __init__(
self,
mip=True,
msg=True,
options=None,
timeLimit=None,
gapRel=None,
gapAbs=None,
maxNodes=None,
logPath=None,
threads=None,
):
"""
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param list options: list of additional options to pass to solver
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param float gapAbs: absolute gap tolerance for the solver to stop
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
:param str logPath: path to the log file
:param int threads: sets the maximum number of threads
"""
super().__init__(
mip=mip,
msg=msg,
options=options,
timeLimit=timeLimit,
gapRel=gapRel,
gapAbs=gapAbs,
maxNodes=maxNodes,
logPath=logPath,
threads=threads,
)
def findSolutionValues(self, lp):
lp.resolveOK = True
solutionStatus = lp.solverModel.getStatus()
scip_to_pulp_status = {
"optimal": constants.LpStatusOptimal,
"unbounded": constants.LpStatusUnbounded,
"infeasible": constants.LpStatusInfeasible,
"inforunbd": constants.LpStatusNotSolved,
"timelimit": constants.LpStatusNotSolved,
"userinterrupt": constants.LpStatusNotSolved,
"nodelimit": constants.LpStatusNotSolved,
"totalnodelimit": constants.LpStatusNotSolved,
"stallnodelimit": constants.LpStatusNotSolved,
"gaplimit": constants.LpStatusNotSolved,
"memlimit": constants.LpStatusNotSolved,
"sollimit": constants.LpStatusNotSolved,
"bestsollimit": constants.LpStatusNotSolved,
"restartlimit": constants.LpStatusNotSolved,
"unknown": constants.LpStatusUndefined,
}
status = scip_to_pulp_status[solutionStatus]
lp.assignStatus(status)
if status == constants.LpStatusOptimal:
solution = lp.solverModel.getBestSol()
for variable in lp._variables:
variable.varValue = solution[variable.solverVar]
for constraint in lp.constraints.values():
constraint.slack = lp.solverModel.getSlack(
constraint.solverConstraint, solution
)
# TODO: check if problem is an LP i.e. does not have integer variables
# if :
# for variable in lp._variables:
# variable.dj = lp.solverModel.getVarRedcost(variable.solverVar)
# for constraint in lp.constraints.values():
# constraint.pi = lp.solverModel.getDualSolVal(constraint.solverConstraint)
return status
def available(self):
"""True if the solver is available"""
# if pyscipopt can be installed (and therefore imported) it has access to scip
return True
def callSolver(self, lp):
"""Solves the problem with scip"""
lp.solverModel.optimize()
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into a scip model
"""
##################################################
# create model
##################################################
lp.solverModel = scip.Model(lp.name)
if lp.sense == constants.LpMaximize:
lp.solverModel.setMaximize()
else:
lp.solverModel.setMinimize()
##################################################
# add options
##################################################
if not self.msg:
lp.solverModel.hideOutput()
if self.timeLimit is not None:
lp.solverModel.setParam("limits/time", self.timeLimit)
if "gapRel" in self.optionsDict:
lp.solverModel.setParam("limits/gap", self.optionsDict["gapRel"])
if "gapAbs" in self.optionsDict:
lp.solverModel.setParam("limits/absgap", self.optionsDict["gapAbs"])
if "maxNodes" in self.optionsDict:
lp.solverModel.setParam("limits/nodes", self.optionsDict["maxNodes"])
if "logPath" in self.optionsDict:
lp.solverModel.setLogfile(self.optionsDict["logPath"])
if "threads" in self.optionsDict and int(self.optionsDict["threads"]) > 1:
warnings.warn(
f"The solver {self.name} can only run with a single thread"
)
if not self.mip:
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
options = iter(self.options)
for option in options:
# assumption: all file options require an argument which is provided after the equal sign (=)
if "=" in option:
name, value = option.split("=", maxsplit=2)
else:
name, value = option, next(options)
lp.solverModel.setParam(name, value)
##################################################
# add variables
##################################################
category_to_vtype = {
constants.LpBinary: "B",
constants.LpContinuous: "C",
constants.LpInteger: "I",
}
for var in lp.variables():
var.solverVar = lp.solverModel.addVar(
name=var.name,
vtype=category_to_vtype[var.cat],
lb=var.lowBound, # a lower bound of None represents -infinity
ub=var.upBound, # an upper bound of None represents +infinity
obj=lp.objective.get(var, 0.0),
)
##################################################
# add constraints
##################################################
sense_to_operator = {
constants.LpConstraintLE: operator.le,
constants.LpConstraintGE: operator.ge,
constants.LpConstraintEQ: operator.eq,
}
for name, constraint in lp.constraints.items():
constraint.solverConstraint = lp.solverModel.addCons(
cons=sense_to_operator[constraint.sense](
scip.quicksum(
coefficient * variable.solverVar
for variable, coefficient in constraint.items()
),
-constraint.constant,
),
name=name,
)
def actualSolve(self, lp):
"""
Solve a well formulated lp problem
creates a scip model, variables and constraints and attaches
them to the lp model which it then solves
"""
self.buildSolverModel(lp)
self.callSolver(lp)
solutionStatus = self.findSolutionValues(lp)
for variable in lp._variables:
variable.modified = False
for constraint in lp.constraints.values():
constraint.modified = False
return solutionStatus
def actualResolve(self, lp):
"""
Solve a well formulated lp problem
uses the old solver and modifies the rhs of the modified constraints
"""
# TODO: add ability to resolve pysciptopt models
# - http://listserv.zib.de/pipermail/scip/2020-May/003977.html
# - https://scipopt.org/doc-8.0.0/html/REOPT.php
raise PulpSolverError(
f"The {self.name} solver does not implement resolving"
)

View File

@@ -0,0 +1,760 @@
# PuLP : Python LP Modeler
# Version 1.4.2
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
# $Id:solvers.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."""
from .core import LpSolver, LpSolver_CMD, subprocess, PulpSolverError
from .. import constants
import warnings
import sys
import re
def _ismip(lp):
"""Check whether lp is a MIP.
From an XPRESS point of view, a problem is also a MIP if it contains
SOS constraints."""
return lp.isMIP() or len(lp.sos1) or len(lp.sos2)
class XPRESS(LpSolver_CMD):
"""The XPRESS LP solver that uses the XPRESS command line tool
in a subprocess"""
name = "XPRESS"
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
gapRel=None,
options=None,
keepFiles=False,
path=None,
maxSeconds=None,
targetGap=None,
heurFreq=None,
heurStra=None,
coverCuts=None,
preSolve=None,
warmStart=False,
):
"""
Initializes the Xpress solver.
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param maxSeconds: deprecated for timeLimit
:param targetGap: deprecated for gapRel
:param heurFreq: the frequency at which heuristics are used in the tree search
:param heurStra: heuristic strategy
:param coverCuts: the number of rounds of lifted cover inequalities at the top node
:param preSolve: whether presolving should be performed before the main algorithm
:param options: Adding more options, e.g. options = ["NODESELECTION=1", "HEURDEPTH=5"]
More about Xpress options and control parameters please see
https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
:param bool warmStart: if True, then use current variable values as start
"""
if maxSeconds:
warnings.warn("Parameter maxSeconds is being depreciated for timeLimit")
if timeLimit is not None:
warnings.warn(
"Parameter timeLimit and maxSeconds passed, using timeLimit"
)
else:
timeLimit = maxSeconds
if targetGap is not None:
warnings.warn("Parameter targetGap is being depreciated for gapRel")
if gapRel is not None:
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
else:
gapRel = targetGap
LpSolver_CMD.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
path=path,
keepFiles=keepFiles,
heurFreq=heurFreq,
heurStra=heurStra,
coverCuts=coverCuts,
preSolve=preSolve,
warmStart=warmStart,
)
def defaultPath(self):
return self.executableExtension("optimizer")
def available(self):
"""True if the solver is available"""
return self.executable(self.path)
def actualSolve(self, lp):
"""Solve a well formulated lp problem"""
if not self.executable(self.path):
raise PulpSolverError("PuLP: cannot execute " + self.path)
tmpLp, tmpSol, tmpCmd, tmpAttr, tmpStart = self.create_tmp_files(
lp.name, "lp", "prt", "cmd", "attr", "slx"
)
variables = lp.writeLP(tmpLp, writeSOS=1, mip=self.mip)
if self.optionsDict.get("warmStart", False):
start = [(v.name, v.value()) for v in variables if v.value() is not None]
self.writeslxsol(tmpStart, start)
# Explicitly capture some attributes so that we can easily get
# information about the solution.
attrNames = []
if _ismip(lp) and self.mip:
attrNames.extend(["mipobjval", "bestbound", "mipstatus"])
statusmap = {
0: constants.LpStatusUndefined, # XPRS_MIP_NOT_LOADED
1: constants.LpStatusUndefined, # XPRS_MIP_LP_NOT_OPTIMAL
2: constants.LpStatusUndefined, # XPRS_MIP_LP_OPTIMAL
3: constants.LpStatusUndefined, # XPRS_MIP_NO_SOL_FOUND
4: constants.LpStatusUndefined, # XPRS_MIP_SOLUTION
5: constants.LpStatusInfeasible, # XPRS_MIP_INFEAS
6: constants.LpStatusOptimal, # XPRS_MIP_OPTIMAL
7: constants.LpStatusUndefined, # XPRS_MIP_UNBOUNDED
}
statuskey = "mipstatus"
else:
attrNames.extend(["lpobjval", "lpstatus"])
statusmap = {
0: constants.LpStatusNotSolved, # XPRS_LP_UNSTARTED
1: constants.LpStatusOptimal, # XPRS_LP_OPTIMAL
2: constants.LpStatusInfeasible, # XPRS_LP_INFEAS
3: constants.LpStatusUndefined, # XPRS_LP_CUTOFF
4: constants.LpStatusUndefined, # XPRS_LP_UNFINISHED
5: constants.LpStatusUnbounded, # XPRS_LP_UNBOUNDED
6: constants.LpStatusUndefined, # XPRS_LP_CUTOFF_IN_DUAL
7: constants.LpStatusNotSolved, # XPRS_LP_UNSOLVED
8: constants.LpStatusUndefined, # XPRS_LP_NONCONVEX
}
statuskey = "lpstatus"
with open(tmpCmd, "w") as cmd:
if not self.msg:
cmd.write("OUTPUTLOG=0\n")
# The readprob command must be in lower case for correct filename handling
cmd.write("readprob " + self.quote_path(tmpLp) + "\n")
if self.timeLimit is not None:
cmd.write("MAXTIME=%d\n" % self.timeLimit)
targetGap = self.optionsDict.get("gapRel")
if targetGap is not None:
cmd.write(f"MIPRELSTOP={targetGap:f}\n")
heurFreq = self.optionsDict.get("heurFreq")
if heurFreq is not None:
cmd.write("HEURFREQ=%d\n" % heurFreq)
heurStra = self.optionsDict.get("heurStra")
if heurStra is not None:
cmd.write("HEURSTRATEGY=%d\n" % heurStra)
coverCuts = self.optionsDict.get("coverCuts")
if coverCuts is not None:
cmd.write("COVERCUTS=%d\n" % coverCuts)
preSolve = self.optionsDict.get("preSolve")
if preSolve is not None:
cmd.write("PRESOLVE=%d\n" % preSolve)
if self.optionsDict.get("warmStart", False):
cmd.write("readslxsol " + self.quote_path(tmpStart) + "\n")
for option in self.options:
cmd.write(option + "\n")
if _ismip(lp) and self.mip:
cmd.write("mipoptimize\n")
else:
cmd.write("lpoptimize\n")
# The writeprtsol command must be in lower case for correct filename handling
cmd.write("writeprtsol " + self.quote_path(tmpSol) + "\n")
cmd.write(
f"set fh [open {self.quote_path(tmpAttr)} w]; list\n"
) # `list` to suppress output
for attr in attrNames:
cmd.write(f'puts $fh "{attr}=${attr}"\n')
cmd.write("close $fh\n")
cmd.write("QUIT\n")
with open(tmpCmd) as cmd:
consume = False
subout = None
suberr = None
if not self.msg:
# Xpress writes a banner before we can disable output. So
# we have to explicitly consume the banner.
if sys.hexversion >= 0x03030000:
subout = subprocess.DEVNULL
suberr = subprocess.DEVNULL
else:
# We could also use open(os.devnull, 'w') but then we
# would be responsible for closing the file.
subout = subprocess.PIPE
suberr = subprocess.STDOUT
consume = True
xpress = subprocess.Popen(
[self.path, lp.name],
shell=True,
stdin=cmd,
stdout=subout,
stderr=suberr,
universal_newlines=True,
)
if consume:
# Special case in which messages are disabled and we have
# to consume any output
for _ in xpress.stdout:
pass
if xpress.wait() != 0:
raise PulpSolverError("PuLP: Error while executing " + self.path)
values, redcost, slacks, duals, attrs = self.readsol(tmpSol, tmpAttr)
self.delete_tmp_files(tmpLp, tmpSol, tmpCmd, tmpAttr)
status = statusmap.get(attrs.get(statuskey, -1), constants.LpStatusUndefined)
lp.assignVarsVals(values)
lp.assignVarsDj(redcost)
lp.assignConsSlack(slacks)
lp.assignConsPi(duals)
lp.assignStatus(status)
return status
@staticmethod
def readsol(filename, attrfile):
"""Read an XPRESS solution file"""
values = {}
redcost = {}
slacks = {}
duals = {}
with open(filename) as f:
for lineno, _line in enumerate(f):
# The first 6 lines are status information
if lineno < 6:
continue
elif lineno == 6:
# Line with status information
_line = _line.split()
rows = int(_line[2])
cols = int(_line[5])
elif lineno < 10:
# Empty line, "Solution Statistics", objective direction
pass
elif lineno == 10:
# Solution status
pass
else:
# There is some more stuff and then follows the "Rows" and
# "Columns" section. That other stuff does not match the
# format of the rows/columns lines, so we can keep the
# parser simple
line = _line.split()
if len(line) > 1:
if line[0] == "C":
# A column
# (C, Number, Name, At, Value, Input Cost, Reduced Cost)
name = line[2]
values[name] = float(line[4])
redcost[name] = float(line[6])
elif len(line[0]) == 1 and line[0] in "LGRE":
# A row
# ([LGRE], Number, Name, At, Value, Slack, Dual, RHS)
name = line[2]
slacks[name] = float(line[5])
duals[name] = float(line[6])
# Read the attributes that we wrote explicitly
attrs = dict()
with open(attrfile) as f:
for line in f:
fields = line.strip().split("=")
if len(fields) == 2 and fields[0].lower() == fields[0]:
value = fields[1].strip()
try:
value = int(fields[1].strip())
except ValueError:
try:
value = float(fields[1].strip())
except ValueError:
pass
attrs[fields[0].strip()] = value
return values, redcost, slacks, duals, attrs
def writeslxsol(self, name, *values):
"""
Write a solution file in SLX format.
The function can write multiple solutions to the same file, each
solution must be passed as a list of (name,value) pairs. Solutions
are written in the order specified and are given names "solutionN"
where N is the index of the solution in the list.
:param string name: file name
:param list values: list of lists of (name,value) pairs
"""
with open(name, "w") as slx:
for i, sol in enumerate(values):
slx.write("NAME solution%d\n" % i)
for name, value in sol:
slx.write(f" C {name} {value:.16f}\n")
slx.write("ENDATA\n")
@staticmethod
def quote_path(path):
r"""
Quotes a path for the Xpress optimizer console, by wrapping it in
double quotes and escaping the following characters, which would
otherwise be interpreted by the Tcl shell: \ $ " [
"""
return '"' + re.sub(r'([\\$"[])', r"\\\1", path) + '"'
XPRESS_CMD = XPRESS
xpress = None
class XPRESS_PY(LpSolver):
"""The XPRESS LP solver that uses XPRESS Python API"""
name = "XPRESS_PY"
def __init__(
self,
mip=True,
msg=True,
timeLimit=None,
gapRel=None,
heurFreq=None,
heurStra=None,
coverCuts=None,
preSolve=None,
warmStart=None,
export=None,
options=None,
):
"""
Initializes the Xpress solver.
:param bool mip: if False, assume LP even if integer variables
:param bool msg: if False, no log is shown
:param float timeLimit: maximum time for solver (in seconds)
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
:param heurFreq: the frequency at which heuristics are used in the tree search
:param heurStra: heuristic strategy
:param coverCuts: the number of rounds of lifted cover inequalities at the top node
:param preSolve: whether presolving should be performed before the main algorithm
:param bool warmStart: if set then use current variable values as warm start
:param string export: if set then the model will be exported to this file before solving
:param options: Adding more options. This is a list the elements of which
are either (name,value) pairs or strings "name=value".
More about Xpress options and control parameters please see
https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
"""
if timeLimit is not None:
# The Xpress time limit has this interpretation:
# timelimit <0: Stop after -timelimit, no matter what
# timelimit >0: Stop after timelimit only if a feasible solution
# exists. We overwrite this meaning here since it is
# somewhat counterintuitive when compared to other
# solvers. You can always pass a positive timlimit
# via `options` to get that behavior.
timeLimit = -abs(timeLimit)
LpSolver.__init__(
self,
gapRel=gapRel,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
heurFreq=heurFreq,
heurStra=heurStra,
coverCuts=coverCuts,
preSolve=preSolve,
warmStart=warmStart,
)
self._available = None
self._export = export
def available(self):
"""True if the solver is available"""
if self._available is None:
try:
global xpress
import xpress
# Always disable the global output. We only want output if
# we install callbacks explicitly
xpress.setOutputEnabled(False)
self._available = True
except:
self._available = False
return self._available
def callSolver(self, lp, prepare=None):
"""Perform the actual solve from actualSolve() or actualResolve().
:param prepare: a function that is called with `lp` as argument
and allows final tweaks to `lp.solverModel` before
the low level solve is started.
"""
try:
model = lp.solverModel
# Mark all variables and constraints as unmodified so that
# actualResolve will do the correct thing.
for v in lp.variables():
v.modified = False
for c in lp.constraints.values():
c.modified = False
if self._export is not None:
if self._export.lower().endswith(".lp"):
model.write(self._export, "l")
else:
model.write(self._export)
if prepare is not None:
prepare(lp)
if _ismip(lp) and not self.mip:
# Solve only the LP relaxation
model.lpoptimize()
else:
# In all other cases, solve() does the correct thing
model.solve()
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
raise PulpSolverError(str(err))
def findSolutionValues(self, lp):
try:
model = lp.solverModel
# Collect results
if _ismip(lp) and self.mip:
# Solved as MIP
x, slacks, duals, djs = [], [], None, None
try:
model.getmipsol(x, slacks)
except:
x, slacks = None, None
statusmap = {
0: constants.LpStatusUndefined, # XPRS_MIP_NOT_LOADED
1: constants.LpStatusUndefined, # XPRS_MIP_LP_NOT_OPTIMAL
2: constants.LpStatusUndefined, # XPRS_MIP_LP_OPTIMAL
3: constants.LpStatusUndefined, # XPRS_MIP_NO_SOL_FOUND
4: constants.LpStatusUndefined, # XPRS_MIP_SOLUTION
5: constants.LpStatusInfeasible, # XPRS_MIP_INFEAS
6: constants.LpStatusOptimal, # XPRS_MIP_OPTIMAL
7: constants.LpStatusUndefined, # XPRS_MIP_UNBOUNDED
}
statuskey = "mipstatus"
else:
# Solved as continuous
x, slacks, duals, djs = [], [], [], []
try:
model.getlpsol(x, slacks, duals, djs)
except:
# No solution available
x, slacks, duals, djs = None, None, None, None
statusmap = {
0: constants.LpStatusNotSolved, # XPRS_LP_UNSTARTED
1: constants.LpStatusOptimal, # XPRS_LP_OPTIMAL
2: constants.LpStatusInfeasible, # XPRS_LP_INFEAS
3: constants.LpStatusUndefined, # XPRS_LP_CUTOFF
4: constants.LpStatusUndefined, # XPRS_LP_UNFINISHED
5: constants.LpStatusUnbounded, # XPRS_LP_UNBOUNDED
6: constants.LpStatusUndefined, # XPRS_LP_CUTOFF_IN_DUAL
7: constants.LpStatusNotSolved, # XPRS_LP_UNSOLVED
8: constants.LpStatusUndefined, # XPRS_LP_NONCONVEX
}
statuskey = "lpstatus"
if x is not None:
lp.assignVarsVals({v.name: x[v._xprs[0]] for v in lp.variables()})
if djs is not None:
lp.assignVarsDj({v.name: djs[v._xprs[0]] for v in lp.variables()})
if duals is not None:
lp.assignConsPi(
{c.name: duals[c._xprs[0]] for c in lp.constraints.values()}
)
if slacks is not None:
lp.assignConsSlack(
{c.name: slacks[c._xprs[0]] for c in lp.constraints.values()}
)
status = statusmap.get(
model.getAttrib(statuskey), constants.LpStatusUndefined
)
lp.assignStatus(status)
return status
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
raise PulpSolverError(str(err))
def actualSolve(self, lp, prepare=None):
"""Solve a well formulated lp problem"""
if not self.available():
# Import again to get a more verbose error message
message = "XPRESS Python API not available"
try:
import xpress
except ImportError as err:
message = str(err)
raise PulpSolverError(message)
self.buildSolverModel(lp)
self.callSolver(lp, prepare)
return self.findSolutionValues(lp)
def buildSolverModel(self, lp):
"""
Takes the pulp lp model and translates it into an xpress model
"""
self._extract(lp)
try:
# Apply controls, warmstart etc. We do this here rather than in
# callSolver() so that the caller has a chance to overwrite things
# either using the `prepare` argument to callSolver() or by
# explicitly calling
# self.buildSolverModel()
# self.callSolver()
# self.findSolutionValues()
# This also avoids setting warmstart information passed to the
# constructor from actualResolve(), which would almost certainly
# be unintended.
model = lp.solverModel
# Apply controls that were passed to the constructor
for key, name in [
("gapRel", "MIPRELSTOP"),
("timeLimit", "MAXTIME"),
("heurFreq", "HEURFREQ"),
("heurStra", "HEURSTRATEGY"),
("coverCuts", "COVERCUTS"),
("preSolve", "PRESOLVE"),
]:
value = self.optionsDict.get(key, None)
if value is not None:
model.setControl(name, value)
# Apply any other controls. These overwrite controls that were
# passed explicitly into the constructor.
for option in self.options:
if isinstance(option, tuple):
name = optione[0]
value = option[1]
else:
fields = option.split("=", 1)
if len(fields) != 2:
raise PulpSolverError("Invalid option " + str(option))
name = fields[0].strip()
value = fields[1].strip()
try:
model.setControl(name, int(value))
continue
except ValueError:
pass
try:
model.setControl(name, float(value))
continue
except ValueError:
pass
model.setControl(name, value)
# Setup warmstart information
if self.optionsDict.get("warmStart", False):
solval = list()
colind = list()
for v in sorted(lp.variables(), key=lambda x: x._xprs[0]):
if v.value() is not None:
solval.append(v.value())
colind.append(v._xprs[0])
if _ismip(lp) and self.mip:
# If we have a value for every variable then use
# loadmipsol(), which requires a dense solution. Otherwise
# use addmipsol() which allows sparse vectors.
if len(solval) == model.attributes.cols:
model.loadmipsol(solval)
else:
model.addmipsol(solval, colind, "warmstart")
else:
model.loadlpsol(solval, None, None, None)
# Setup message callback if output is requested
if self.msg:
def message(prob, data, msg, msgtype):
if msgtype > 0:
print(msg)
model.addcbmessage(message)
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
raise PulpSolverError(str(err))
def actualResolve(self, lp, prepare=None):
"""Resolve a problem that was previously solved by actualSolve()."""
try:
rhsind = list()
rhsval = list()
for name in sorted(lp.constraints):
con = lp.constraints[name]
if not con.modified:
continue
if not hasattr(con, "_xprs"):
# Adding constraints is not implemented at the moment
raise PulpSolverError("Cannot add new constraints")
# At the moment only RHS can change in pulp.py
rhsind.append(con._xprs[0])
rhsval.append(-con.constant)
if len(rhsind) > 0:
lp.solverModel.chgrhs(rhsind, rhsval)
bndind = list()
bndtype = list()
bndval = list()
for v in lp.variables():
if not v.modified:
continue
if not hasattr(v, "_xprs"):
# Adding variables is not implemented at the moment
raise PulpSolverError("Cannot add new variables")
# At the moment only bounds can change in pulp.py
bndind.append(v._xprs[0])
bndtype.append("L")
bndval.append(-xpress.infinity if v.lowBound is None else v.lowBound)
bndind.append(v._xprs[0])
bndtype.append("G")
bndval.append(xpress.infinity if v.upBound is None else v.upBound)
if len(bndtype) > 0:
lp.solverModel.chgbounds(bndind, bndtype, bndval)
self.callSolver(lp, prepare)
return self.findSolutionValues(lp)
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
raise PulpSolverError(str(err))
@staticmethod
def _reset(lp):
"""Reset any XPRESS specific information in lp."""
if hasattr(lp, "solverModel"):
delattr(lp, "solverModel")
for v in lp.variables():
if hasattr(v, "_xprs"):
delattr(v, "_xprs")
for c in lp.constraints.values():
if hasattr(c, "_xprs"):
delattr(c, "_xprs")
def _extract(self, lp):
"""Extract a given model to an XPRESS Python API instance.
The function stores XPRESS specific information in the `solverModel` property
of `lp` and each variable and constraint. These information can be
removed by calling `_reset`.
"""
self._reset(lp)
try:
model = xpress.problem()
if lp.sense == constants.LpMaximize:
model.chgobjsense(xpress.maximize)
# Create variables. We first collect the info for all variables
# and then create all of them in one shot. This is supposed to
# be faster in case we have to create a lot of variables.
obj = list()
lb = list()
ub = list()
ctype = list()
names = list()
for v in lp.variables():
lb.append(-xpress.infinity if v.lowBound is None else v.lowBound)
ub.append(xpress.infinity if v.upBound is None else v.upBound)
obj.append(lp.objective.get(v, 0.0))
if v.cat == constants.LpInteger:
ctype.append("I")
elif v.cat == constants.LpBinary:
ctype.append("B")
else:
ctype.append("C")
names.append(v.name)
model.addcols(obj, [0] * (len(obj) + 1), [], [], lb, ub, names, ctype)
for j, (v, x) in enumerate(zip(lp.variables(), model.getVariable())):
v._xprs = (j, x)
# Generate constraints. Sort by name to get deterministic
# ordering of constraints.
# Constraints are generated in blocks of 100 constraints to speed
# up things a bit but still keep memory usage small.
cons = list()
for i, name in enumerate(sorted(lp.constraints)):
con = lp.constraints[name]
# Sort the variables by index to get deterministic
# ordering of variables in the row.
lhs = xpress.Sum(
a * x._xprs[1]
for x, a in sorted(con.items(), key=lambda x: x[0]._xprs[0])
)
rhs = -con.constant
if con.sense == constants.LpConstraintLE:
c = xpress.constraint(body=lhs, sense=xpress.leq, rhs=rhs)
elif con.sense == constants.LpConstraintGE:
c = xpress.constraint(body=lhs, sense=xpress.geq, rhs=rhs)
elif con.sense == constants.LpConstraintEQ:
c = xpress.constraint(body=lhs, sense=xpress.eq, rhs=rhs)
else:
raise PulpSolverError(
"Unsupprted constraint type " + str(con.sense)
)
cons.append((i, c, con))
if len(cons) > 100:
model.addConstraint([c for _, c, _ in cons])
for i, c, con in cons:
con._xprs = (i, c)
cons = list()
if len(cons) > 0:
model.addConstraint([c for _, c, _ in cons])
for i, c, con in cons:
con._xprs = (i, c)
# SOS constraints
def addsos(m, sosdict, sostype):
"""Extract sos constraints from PuLP."""
soslist = []
# Sort by name to get deterministic ordering. Note that
# names may be plain integers, that is why we use str(name)
# to pass them to the SOS constructor.
for name in sorted(sosdict):
indices = []
weights = []
for v, val in sosdict[name].items():
indices.append(v._xprs[0])
weights.append(val)
soslist.append(xpress.sos(indices, weights, sostype, str(name)))
if len(soslist):
m.addSOS(soslist)
addsos(model, lp.sos1, 1)
addsos(model, lp.sos2, 2)
lp.solverModel = model
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
# Undo everything
self._reset(lp)
raise PulpSolverError(str(err))
def getAttribute(self, lp, which):
"""Get an arbitrary attribute for the model that was previously
solved using actualSolve()."""
return lp.solverModel.getAttrib(which)

104
utils/pulp/constants.py Normal file
View File

@@ -0,0 +1,104 @@
# 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:constants.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."""
"""
This file contains the constant definitions for PuLP
Note that hopefully these will be changed into something more pythonic
"""
VERSION = "2.7.0"
EPS = 1e-7
# variable categories
LpContinuous = "Continuous"
LpInteger = "Integer"
LpBinary = "Binary"
LpCategories = {LpContinuous: "Continuous", LpInteger: "Integer", LpBinary: "Binary"}
# objective sense
LpMinimize = 1
LpMaximize = -1
LpSenses = {LpMaximize: "Maximize", LpMinimize: "Minimize"}
LpSensesMPS = {LpMaximize: "MAX", LpMinimize: "MIN"}
# problem status
LpStatusNotSolved = 0
LpStatusOptimal = 1
LpStatusInfeasible = -1
LpStatusUnbounded = -2
LpStatusUndefined = -3
LpStatus = {
LpStatusNotSolved: "Not Solved",
LpStatusOptimal: "Optimal",
LpStatusInfeasible: "Infeasible",
LpStatusUnbounded: "Unbounded",
LpStatusUndefined: "Undefined",
}
# solution status
LpSolutionNoSolutionFound = 0
LpSolutionOptimal = 1
LpSolutionIntegerFeasible = 2
LpSolutionInfeasible = -1
LpSolutionUnbounded = -2
LpSolution = {
LpSolutionNoSolutionFound: "No Solution Found",
LpSolutionOptimal: "Optimal Solution Found",
LpSolutionIntegerFeasible: "Solution Found",
LpSolutionInfeasible: "No Solution Exists",
LpSolutionUnbounded: "Solution is Unbounded",
}
LpStatusToSolution = {
LpStatusNotSolved: LpSolutionInfeasible,
LpStatusOptimal: LpSolutionOptimal,
LpStatusInfeasible: LpSolutionInfeasible,
LpStatusUnbounded: LpSolutionUnbounded,
LpStatusUndefined: LpSolutionInfeasible,
}
# constraint sense
LpConstraintLE = -1
LpConstraintEQ = 0
LpConstraintGE = 1
LpConstraintTypeToMps = {LpConstraintLE: "L", LpConstraintEQ: "E", LpConstraintGE: "G"}
LpConstraintSenses = {LpConstraintEQ: "=", LpConstraintLE: "<=", LpConstraintGE: ">="}
# LP line size
LpCplexLPLineSize = 78
def isiterable(obj):
try:
obj = iter(obj)
except:
return False
else:
return True
class PulpError(Exception):
"""
Pulp Exception Class
"""
pass

401
utils/pulp/mps_lp.py Normal file
View File

@@ -0,0 +1,401 @@
"""
@author: Franco Peschiera
"""
import re
from . import constants as const
CORE_FILE_ROW_MODE = "ROWS"
CORE_FILE_COL_MODE = "COLUMNS"
CORE_FILE_RHS_MODE = "RHS"
CORE_FILE_BOUNDS_MODE = "BOUNDS"
CORE_FILE_BOUNDS_MODE_NAME_GIVEN = "BOUNDS_NAME"
CORE_FILE_BOUNDS_MODE_NO_NAME = "BOUNDS_NO_NAME"
CORE_FILE_RHS_MODE_NAME_GIVEN = "RHS_NAME"
CORE_FILE_RHS_MODE_NO_NAME = "RHS_NO_NAME"
ROW_MODE_OBJ = "N"
BOUNDS_EQUIV = dict(LO="lowBound", UP="upBound")
ROW_EQUIV = {v: k for k, v in const.LpConstraintTypeToMps.items()}
COL_EQUIV = {1: "Integer", 0: "Continuous"}
ROW_DEFAULT = dict(pi=None, constant=0)
COL_DEFAULT = dict(lowBound=0, upBound=None, varValue=None, dj=None)
def readMPS(path, sense, dropConsNames=False):
"""
adapted from Julian Märte (https://github.com/pchtsp/pysmps)
returns a dictionary with the contents of the model.
This dictionary can be used to generate an LpProblem
:param path: path of mps file
:param sense: 1 for minimize, -1 for maximize
:param dropConsNames: if True, do not store the names of constraints
:return: a dictionary with all the problem data
"""
mode = ""
parameters = dict(name="", sense=sense, status=0, sol_status=0)
variable_info = {}
constraints = {}
objective = dict(name="", coefficients=[])
sos1 = []
sos2 = []
# TODO: maybe take out rhs_names and bnd_names? not sure if they're useful
rhs_names = []
bnd_names = []
integral_marker = False
with open(path) as reader:
for line in reader:
line = re.split(" |\t", line)
line = [x.strip() for x in line]
line = list(filter(None, line))
if line[0] == "ENDATA":
break
if line[0] == "*":
continue
if line[0] == "NAME":
if len(line) > 1:
parameters["name"] = line[1]
else:
parameters["name"] = ""
continue
# here we get the mode
if line[0] in [CORE_FILE_ROW_MODE, CORE_FILE_COL_MODE]:
mode = line[0]
elif line[0] == CORE_FILE_RHS_MODE and len(line) <= 2:
if len(line) > 1:
rhs_names.append(line[1])
mode = CORE_FILE_RHS_MODE_NAME_GIVEN
else:
mode = CORE_FILE_RHS_MODE_NO_NAME
elif line[0] == CORE_FILE_BOUNDS_MODE and len(line) <= 2:
if len(line) > 1:
bnd_names.append(line[1])
mode = CORE_FILE_BOUNDS_MODE_NAME_GIVEN
else:
mode = CORE_FILE_BOUNDS_MODE_NO_NAME
# here we query the mode variable
elif mode == CORE_FILE_ROW_MODE:
row_type = line[0]
row_name = line[1]
if row_type == ROW_MODE_OBJ:
objective["name"] = row_name
else:
constraints[row_name] = dict(
sense=ROW_EQUIV[row_type],
name=row_name,
coefficients=[],
**ROW_DEFAULT,
)
elif mode == CORE_FILE_COL_MODE:
var_name = line[0]
if len(line) > 1 and line[1] == "'MARKER'":
if line[2] == "'INTORG'":
integral_marker = True
elif line[2] == "'INTEND'":
integral_marker = False
continue
if var_name not in variable_info:
variable_info[var_name] = dict(
cat=COL_EQUIV[integral_marker], name=var_name, **COL_DEFAULT
)
j = 1
while j < len(line) - 1:
if line[j] == objective["name"]:
# we store the variable objective coefficient
objective["coefficients"].append(
dict(name=var_name, value=float(line[j + 1]))
)
else:
# we store the variable coefficient
constraints[line[j]]["coefficients"].append(
dict(name=var_name, value=float(line[j + 1]))
)
j = j + 2
elif mode == CORE_FILE_RHS_MODE_NAME_GIVEN:
if line[0] != rhs_names[-1]:
raise Exception(
"Other RHS name was given even though name was set after RHS tag."
)
readMPSSetRhs(line, constraints)
elif mode == CORE_FILE_RHS_MODE_NO_NAME:
readMPSSetRhs(line, constraints)
if line[0] not in rhs_names:
rhs_names.append(line[0])
elif mode == CORE_FILE_BOUNDS_MODE_NAME_GIVEN:
if line[1] != bnd_names[-1]:
raise Exception(
"Other BOUNDS name was given even though name was set after BOUNDS tag."
)
readMPSSetBounds(line, variable_info)
elif mode == CORE_FILE_BOUNDS_MODE_NO_NAME:
readMPSSetBounds(line, variable_info)
if line[1] not in bnd_names:
bnd_names.append(line[1])
constraints = list(constraints.values())
if dropConsNames:
for c in constraints:
c["name"] = None
objective["name"] = None
variable_info = list(variable_info.values())
return dict(
parameters=parameters,
objective=objective,
variables=variable_info,
constraints=constraints,
sos1=sos1,
sos2=sos2,
)
def readMPSSetBounds(line, variable_dict):
bound = line[0]
var_name = line[2]
def set_one_bound(bound_type, value):
variable_dict[var_name][BOUNDS_EQUIV[bound_type]] = value
def set_both_bounds(value_low, value_up):
set_one_bound("LO", value_low)
set_one_bound("UP", value_up)
if bound == "FR":
set_both_bounds(None, None)
return
elif bound == "BV":
set_both_bounds(0, 1)
return
value = float(line[3])
if bound in ["LO", "UP"]:
set_one_bound(bound, value)
elif bound == "FX":
set_both_bounds(value, value)
return
def readMPSSetRhs(line, constraintsDict):
constraintsDict[line[1]]["constant"] = -float(line[2])
if len(line) == 5: # read fields 5, 6
constraintsDict[line[3]]["constant"] = -float(line[4])
return
def writeMPS(
LpProblem, filename, mpsSense=0, rename=0, mip=1, with_objsense: bool = False
):
wasNone, dummyVar = LpProblem.fixObjective()
if mpsSense == 0:
mpsSense = LpProblem.sense
cobj = LpProblem.objective
if mpsSense != LpProblem.sense:
n = cobj.name
cobj = -cobj
cobj.name = n
if rename:
constrNames, varNames, cobj.name = LpProblem.normalisedNames()
# No need to call self.variables() again, we have just filled self._variables:
vs = LpProblem._variables
else:
vs = LpProblem.variables()
varNames = {v.name: v.name for v in vs}
constrNames = {c: c for c in LpProblem.constraints}
model_name = LpProblem.name
if rename:
model_name = "MODEL"
objName = cobj.name
if not objName:
objName = "OBJ"
# constraints
row_lines = [
" " + const.LpConstraintTypeToMps[c.sense] + " " + constrNames[k] + "\n"
for k, c in LpProblem.constraints.items()
]
# Creation of a dict of dict:
# coefs[variable_name][constraint_name] = coefficient
coefs = {varNames[v.name]: {} for v in vs}
for k, c in LpProblem.constraints.items():
k = constrNames[k]
for v, value in c.items():
coefs[varNames[v.name]][k] = value
# matrix
columns_lines = []
for v in vs:
name = varNames[v.name]
columns_lines.extend(
writeMPSColumnLines(coefs[name], v, mip, name, cobj, objName)
)
# right hand side
rhs_lines = [
" RHS %-8s % .12e\n"
% (constrNames[k], -c.constant if c.constant != 0 else 0)
for k, c in LpProblem.constraints.items()
]
# bounds
bound_lines = []
for v in vs:
bound_lines.extend(writeMPSBoundLines(varNames[v.name], v, mip))
with open(filename, "w") as f:
if with_objsense:
f.write("OBJSENSE\n")
f.write(f" {const.LpSensesMPS[mpsSense]}\n")
else:
f.write(f"*SENSE:{const.LpSenses[mpsSense]}\n")
f.write(f"NAME {model_name}\n")
f.write("ROWS\n")
f.write(f" N {objName}\n")
f.write("".join(row_lines))
f.write("COLUMNS\n")
f.write("".join(columns_lines))
f.write("RHS\n")
f.write("".join(rhs_lines))
f.write("BOUNDS\n")
f.write("".join(bound_lines))
f.write("ENDATA\n")
LpProblem.restoreObjective(wasNone, dummyVar)
# returns the variables, in writing order
if rename == 0:
return vs
else:
return vs, varNames, constrNames, cobj.name
def writeMPSColumnLines(cv, variable, mip, name, cobj, objName):
columns_lines = []
if mip and variable.cat == const.LpInteger:
columns_lines.append(" MARK 'MARKER' 'INTORG'\n")
# Most of the work is done here
_tmp = [" %-8s %-8s % .12e\n" % (name, k, v) for k, v in cv.items()]
columns_lines.extend(_tmp)
# objective function
if variable in cobj:
columns_lines.append(
" %-8s %-8s % .12e\n" % (name, objName, cobj[variable])
)
if mip and variable.cat == const.LpInteger:
columns_lines.append(" MARK 'MARKER' 'INTEND'\n")
return columns_lines
def writeMPSBoundLines(name, variable, mip):
if variable.lowBound is not None and variable.lowBound == variable.upBound:
return [" FX BND %-8s % .12e\n" % (name, variable.lowBound)]
elif (
variable.lowBound == 0
and variable.upBound == 1
and mip
and variable.cat == const.LpInteger
):
return [" BV BND %-8s\n" % name]
bound_lines = []
if variable.lowBound is not 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 variable.lowBound != 0 or (
mip and variable.cat == const.LpInteger and variable.upBound is None
):
bound_lines.append(
" LO BND %-8s % .12e\n" % (name, variable.lowBound)
)
else:
if variable.upBound is not None:
bound_lines.append(" MI BND %-8s\n" % name)
else:
bound_lines.append(" FR BND %-8s\n" % name)
if variable.upBound is not None:
bound_lines.append(" UP BND %-8s % .12e\n" % (name, variable.upBound))
return bound_lines
def writeLP(LpProblem, filename, writeSOS=1, mip=1, max_length=100):
f = open(filename, "w")
f.write("\\* " + LpProblem.name + " *\\\n")
if LpProblem.sense == 1:
f.write("Minimize\n")
else:
f.write("Maximize\n")
wasNone, objectiveDummyVar = LpProblem.fixObjective()
objName = LpProblem.objective.name
if not objName:
objName = "OBJ"
f.write(LpProblem.objective.asCplexLpAffineExpression(objName, constant=0))
f.write("Subject To\n")
ks = list(LpProblem.constraints.keys())
ks.sort()
dummyWritten = False
for k in ks:
constraint = LpProblem.constraints[k]
if not list(constraint.keys()):
# empty constraint add the dummyVar
dummyVar = LpProblem.get_dummyVar()
constraint += dummyVar
# set this dummyvar to zero so infeasible problems are not made feasible
if not dummyWritten:
f.write((dummyVar == 0.0).asCplexLpConstraint("_dummy"))
dummyWritten = True
f.write(constraint.asCplexLpConstraint(k))
# check if any names are longer than 100 characters
LpProblem.checkLengthVars(max_length)
vs = LpProblem.variables()
# check for repeated names
LpProblem.checkDuplicateVars()
# 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 == const.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(f" {v.asCplexLpVariable()}\n")
# Integer non-binary variables
if mip:
vg = [v for v in vs if v.cat == const.LpInteger and not v.isBinary()]
if vg:
f.write("Generals\n")
for v in vg:
f.write(f"{v.name}\n")
# Binary variables
vg = [v for v in vs if v.isBinary()]
if vg:
f.write("Binaries\n")
for v in vg:
f.write(f"{v.name}\n")
# Special Ordered Sets
if writeSOS and (LpProblem.sos1 or LpProblem.sos2):
f.write("SOS\n")
if LpProblem.sos1:
for sos in LpProblem.sos1.values():
f.write("S1:: \n")
for v, val in sos.items():
f.write(f" {v.name}: {val:.12g}\n")
if LpProblem.sos2:
for sos in LpProblem.sos2.values():
f.write("S2:: \n")
for v, val in sos.items():
f.write(f" {v.name}: {val:.12g}\n")
f.write("End\n")
f.close()
LpProblem.restoreObjective(wasNone, objectiveDummyVar)
return vs

View File

@@ -0,0 +1,15 @@
# The configuration file that holds locations for 3rd party solvers
# This is an appropriate configuration file for linux uses and in this case is assuming that the
# needed libraries are in the same directory as the config file (note this is not ideal and
# may change in later versions)
# Libraries are ordered in the needed order to resolve dependancies and CoinMP is loaded last
# a windows specific configuation file is pulp.cfg.win
#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $
[locations]
CoinMPPath = %(here)s/../../parts/coinMP/lib/libCoinUtils.so, %(here)s/../../parts/coinMP/lib/libClp.so, %(here)s/../../parts/coinMP/lib/libOsi.so, %(here)s/../../parts/coinMP/lib/libOsiClp.so, %(here)s/../../parts/coinMP/lib/libCgl.so, %(here)s/../../parts/coinMP/lib/libCbc.so, %(here)s/../../parts/coinMP/lib/libOsiCbc.so, %(here)s/../../parts/coinMP/lib/libCbcSolver.so, %(here)s/../../parts/coinMP/lib/libCoinMP.so
CplexPath = /usr/ilog/cplex/bin/x86_rhel4.0_3.4/libcplex110.so
#note the recommended gurobi changes must be made to your PATH
#and LD_LIBRARY_PATH enviroment variables
GurobiPath = /opt/gurobi201/linux32/lib/python2.5
CbcPath = %(here)s/../../parts/cbc/bin/cbc
GlpkPath = %(here)s/../../parts/glpk/bin/glpsol

18
utils/pulp/pulp.cfg.win Normal file
View File

@@ -0,0 +1,18 @@
# The configuration file that holds locations for 3rd party solvers
# for CoinMp.dll it is assumed that no location will place the file in the
# same directory as the configuration file. (note this is not ideal and
# may change in later versions)
# a linux file is provided in pulp.cfg.linux
#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $
[locations]
CoinMPPath = %(here)s\solverdir\coinMP\%(os)s\%(arch)s\CoinMP.dll
CplexPath = cplex100.dll
#must have gurobi.dll somewhere on the search path
GurobiPath = C:\gurobi10\win32\python2.5\lib
CbcPath = cbc
GlpkPath = glpsol
PulpCbcPath = %(here)s\solverdir\cbc\%(os)s\%(arch)s\cbc.exe
PulpChocoPath = %(here)s/solverdir/choco/choco-parsers-with-dependencies.jar
[licenses]
ilm_cplex_license = "LICENSE your-enterprise\nRUNTIME NEVER ..."
ilm_cplex_license_signature = 0

2298
utils/pulp/pulp.py Normal file

File diff suppressed because it is too large Load Diff

88
utils/pulp/sparse.py Normal file
View File

@@ -0,0 +1,88 @@
# Sparse : Python basic dictionary sparse matrix
# Copyright (c) 2007, Stuart Mitchell (s.mitchell@auckland.ac.nz)
# $Id: sparse.py 1704 2007-12-20 21:56:14Z 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.
"""
sparse this module provides basic pure python sparse matrix implementation
notably this allows the sparse matrix to be output in various formats
"""
class Matrix(dict):
"""This is a dictionary based sparse matrix class"""
def __init__(self, rows, cols):
"""initialises the class by creating a matrix that will have the given
rows and columns
"""
self.rows = rows
self.cols = cols
self.rowdict = {row: {} for row in rows}
self.coldict = {col: {} for col in cols}
def add(self, row, col, item, colcheck=False, rowcheck=False):
if not (rowcheck and row not in self.rows):
if not (colcheck and col not in self.cols):
dict.__setitem__(self, (row, col), item)
self.rowdict[row][col] = item
self.coldict[col][row] = item
else:
print(self.cols)
raise RuntimeError(f"col {col} is not in the matrix columns")
else:
raise RuntimeError(f"row {row} is not in the matrix rows")
def addcol(self, col, rowitems):
"""adds a column"""
if col in self.cols:
for row, item in rowitems.items():
self.add(row, col, item, colcheck=False)
else:
raise RuntimeError("col is not in the matrix columns")
def get(self, k, d=0):
return dict.get(self, k, d)
def col_based_arrays(self):
numEls = len(self)
elemBase = []
startsBase = []
indBase = []
lenBase = []
for i, col in enumerate(self.cols):
startsBase.append(len(elemBase))
elemBase.extend(list(self.coldict[col].values()))
indBase.extend(list(self.coldict[col].keys()))
lenBase.append(len(elemBase) - startsBase[-1])
startsBase.append(len(elemBase))
return numEls, startsBase, lenBase, indBase, elemBase
if __name__ == "__main__":
"""unit test"""
rows = list(range(10))
cols = list(range(50, 60))
mat = Matrix(rows, cols)
mat.add(1, 52, "item")
mat.add(2, 54, "stuff")
print(mat.col_based_arrays())

View File

@@ -0,0 +1 @@
from .run_tests import pulpTestAll

View File

@@ -0,0 +1,51 @@
from pulp import *
import random
from itertools import product
def _bin_packing_instance(bins, seed=0):
packed_bins = [[] for _ in range(bins)]
bin_size = bins * 100
random.seed(seed)
for i in range(len(packed_bins)):
remaining_size = bin_size
while remaining_size >= 1:
item = random.randrange(1, remaining_size + 10)
packed_bins[i].append(item)
remaining_size -= item
packed_bins[i][-1] += remaining_size
all_items_with_bin = [(n, i) for i, l in enumerate(packed_bins) for n in l]
random.shuffle(all_items_with_bin)
items, packing = zip(*all_items_with_bin)
return items, packing, bin_size
def create_bin_packing_problem(bins, seed=0):
items, packing, bin_size = _bin_packing_instance(bins=bins, seed=seed)
prob = LpProblem("bin_packing", LpMinimize)
bin_indices = [i for i in range(len(items))]
item_indices = [i for i in range(len(items))]
using_bin = LpVariable.dicts("y", bin_indices, cat=LpBinary)
items_packed = LpVariable.dicts(
"x", indices=product(item_indices, bin_indices), cat=LpBinary
)
prob += lpSum(using_bin), "objective"
# pack every item
for i in item_indices:
prob += lpSum(items_packed[i, b] for b in bin_indices) == 1, f"pack_item_{i}"
# no bin overfilled
for b in bin_indices:
expr = (
lpSum([items[i] * items_packed[i, b] for i in item_indices])
<= bin_size * using_bin[b]
)
prob += expr, f"respect_bin_size_{b}"
return prob

View File

@@ -0,0 +1,33 @@
import unittest
import pulp
from pulp.tests import test_pulp, test_examples, test_gurobipy_env
def pulpTestAll(test_docs=False):
runner = unittest.TextTestRunner()
suite_all = get_test_suite(test_docs)
# we run all tests at the same time
ret = runner.run(suite_all)
if not ret.wasSuccessful():
raise pulp.PulpError("Tests Failed")
def get_test_suite(test_docs=False):
# Tests
loader = unittest.TestLoader()
suite_all = unittest.TestSuite()
# we get suite with all PuLP tests
pulp_solver_tests = loader.loadTestsFromModule(test_pulp)
suite_all.addTests(pulp_solver_tests)
# Add tests for gurobipy env
gurobipy_env = loader.loadTestsFromModule(test_gurobipy_env)
suite_all.addTests(gurobipy_env)
# We add examples and docs tests
if test_docs:
docs_examples = loader.loadTestsFromTestCase(test_examples.Examples_DocsTests)
suite_all.addTests(docs_examples)
return suite_all
if __name__ == "__main__":
pulpTestAll(test_docs=False)

View File

@@ -0,0 +1,36 @@
import os
import unittest
import pulp
import shutil
class Examples_DocsTests(unittest.TestCase):
def test_examples(self, examples_dir="../../examples"):
import importlib
this_file = os.path.realpath(__file__)
parent_dir = os.path.dirname(this_file)
files = os.listdir(os.path.join(parent_dir, examples_dir))
TMP_dir = "_tmp/"
if not os.path.exists(TMP_dir):
os.mkdir(TMP_dir)
for f_name in files:
if os.path.isdir(f_name):
continue
_f_name = "examples." + os.path.splitext(f_name)[0]
os.chdir(TMP_dir)
importlib.import_module(_f_name)
os.chdir("../")
shutil.rmtree(TMP_dir)
def test_doctest(self):
"""
runs all doctests
"""
import doctest
doctest.testmod(pulp)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,122 @@
import unittest
from pulp import GUROBI, LpProblem, LpVariable, const
try:
import gurobipy as gp
from gurobipy import GRB
except ImportError:
gp = None
def check_dummy_env():
with gp.Env(params={"OutputFlag": 0}):
pass
def generate_lp() -> LpProblem:
prob = LpProblem("test", const.LpMaximize)
x = LpVariable("x", 0, 1)
y = LpVariable("y", 0, 1)
z = LpVariable("z", 0, 1)
prob += x + y + z, "obj"
prob += x + y + z <= 1, "c1"
return prob
class GurobiEnvTests(unittest.TestCase):
def setUp(self):
if gp is None:
self.skipTest("Skipping all tests in test_gurobipy_env.py")
self.options = {"Method": 0}
self.env_options = {"MemLimit": 1, "OutputFlag": 0}
def test_gp_env(self):
# Using gp.Env within a context manager
with gp.Env(params=self.env_options) as env:
prob = generate_lp()
solver = GUROBI(msg=False, env=env, **self.options)
prob.solve(solver)
solver.close()
check_dummy_env()
@unittest.SkipTest
def test_gp_env_no_close(self):
# Not closing results in an error for a single use license.
with gp.Env(params=self.env_options) as env:
prob = generate_lp()
solver = GUROBI(msg=False, env=env, **self.options)
prob.solve(solver)
self.assertRaises(gp.GurobiError, check_dummy_env)
def test_multiple_gp_env(self):
# Using the same env multiple times
with gp.Env(params=self.env_options) as env:
solver = GUROBI(msg=False, env=env)
prob = generate_lp()
prob.solve(solver)
solver.close()
solver2 = GUROBI(msg=False, env=env)
prob2 = generate_lp()
prob2.solve(solver2)
solver2.close()
check_dummy_env()
@unittest.SkipTest
def test_backward_compatibility(self):
"""
Backward compatibility check as previously the environment was not being
freed. On a single-use license this passes (fails to initialise a dummy
env).
"""
solver = GUROBI(msg=False, **self.options)
prob = generate_lp()
prob.solve(solver)
self.assertRaises(gp.GurobiError, check_dummy_env)
gp.disposeDefaultEnv()
solver.close()
def test_manage_env(self):
solver = GUROBI(msg=False, manageEnv=True, **self.options)
prob = generate_lp()
prob.solve(solver)
solver.close()
check_dummy_env()
def test_multiple_solves(self):
solver = GUROBI(msg=False, manageEnv=True, **self.options)
prob = generate_lp()
prob.solve(solver)
solver.close()
check_dummy_env()
solver2 = GUROBI(msg=False, manageEnv=True, **self.options)
prob.solve(solver2)
solver2.close()
check_dummy_env()
@unittest.SkipTest
def test_leak(self):
"""
Check that we cannot initialise environments after a memory leak. On a
single-use license this passes (fails to initialise a dummy env with a
memory leak).
"""
solver = GUROBI(msg=False, **self.options)
prob = generate_lp()
prob.solve(solver)
tmp = solver.model
solver.close()
solver2 = GUROBI(msg=False, **self.options)
prob2 = generate_lp()
prob2.solve(solver2)
self.assertRaises(gp.GurobiError, check_dummy_env)

File diff suppressed because it is too large Load Diff

232
utils/pulp/utilities.py Normal file
View File

@@ -0,0 +1,232 @@
# Utility functions
import itertools
import collections
def resource_clock():
import resource
return resource.getrusage(resource.RUSAGE_CHILDREN).ru_utime
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):
"""
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
try: # python >= 3.4
from itertools import combinations as combination
except ImportError:
try: # python 2.7
from itertools import combination
except ImportError: # pulp's
combination = __combination
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
try: # python >= 3.4
from itertools import permutations as permutation
except ImportError:
try: # python 2.7
from itertools import permutation
except ImportError: # pulp's
permutation = __permutation
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 combinations 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 is not 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 _ 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