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:
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