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:
165
utils/pulp/apis/__init__.py
Normal file
165
utils/pulp/apis/__init__.py
Normal 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
|
||||
158
utils/pulp/apis/choco_api.py
Normal file
158
utils/pulp/apis/choco_api.py
Normal 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
873
utils/pulp/apis/coin_api.py
Normal 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
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
493
utils/pulp/apis/core.py
Normal 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
|
||||
579
utils/pulp/apis/cplex_api.py
Normal file
579
utils/pulp/apis/cplex_api.py
Normal 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
409
utils/pulp/apis/glpk_api.py
Normal 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
|
||||
566
utils/pulp/apis/gurobi_api.py
Normal file
566
utils/pulp/apis/gurobi_api.py
Normal 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
|
||||
]
|
||||
409
utils/pulp/apis/highs_api.py
Normal file
409
utils/pulp/apis/highs_api.py
Normal 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")
|
||||
154
utils/pulp/apis/mipcl_api.py
Normal file
154
utils/pulp/apis/mipcl_api.py
Normal 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
|
||||
345
utils/pulp/apis/mosek_api.py
Normal file
345
utils/pulp/apis/mosek_api.py
Normal 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
679
utils/pulp/apis/scip_api.py
Normal 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"
|
||||
)
|
||||
760
utils/pulp/apis/xpress_api.py
Normal file
760
utils/pulp/apis/xpress_api.py
Normal 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)
|
||||
Reference in New Issue
Block a user