- 添加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配置文件
874 lines
31 KiB
Python
874 lines
31 KiB
Python
# 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
|