如何向 PuLP 添加新的求解器
Solver API 位于目录 pulp/apis/
中,并按每个求解器一个文件的方式组织。每个文件可以包含多个API(通常在1到2个之间)。
关于配置求解器以及pulp如何在系统中找到已安装求解器的通用信息,请参见 如何在 PuLP 中配置求解器 部分。
示例
最小的求解器API可以在文件 pulp.apis.mipcl_api.MIPCL_CMD
中找到,该文件位于 pulp/apis/mipcl_api.py
。我们将使用它作为示例,列出所有需要进行的更改。
继承基类
求解器需要继承两个类之一:pulp.apis.LpSolver
或 pulp.apis.LpSolver_CMD
,位于 pulp.apis.core.py
中。第一个是通用类。第二个用于基于命令行的求解器API。这是示例中使用的那个:
from .core import LpSolver_CMD, subprocess, PulpSolverError
import os
from .. import constants
import warnings
class MIPCL_CMD(LpSolver_CMD):
name = "MIPCL_CMD"
def __init__(
self,
path=None,
keepFiles=False,
mip=True,
msg=True,
options=None,
timeLimit=None,
):
LpSolver_CMD.__init__(
self,
mip=mip,
msg=msg,
timeLimit=timeLimit,
options=options,
path=path,
keepFiles=keepFiles,
)
如示例所示,声明求解器时的主要事项是:
使用其名称声明一个类级别的属性。
为求解器定义一些参数(理想情况下,遵循其他求解器的惯例)。
使用可用数据初始化父类。
(可选) 执行一些其他特定逻辑。
除了这些,求解器还需要实现一些最基本的方法。我们将逐一介绍。
available
方法
由 pulp.apis.LpSolver
和 pulp.apis.LpSolver_CMD
要求。
如果求解器可用且正常运行,则返回 True
。否则返回 False
:
def available(self):
return self.executable(self.path)
actualSolve
方法
由 pulp.apis.LpSolver
和 pulp.apis.LpSolver_CMD
要求。
接受一个 pulp.pulp.LpProblem
作为参数,解决它,存储解决方案,并返回一个状态码:
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 += " %s" % tmpMps
cmd += " -solfile %s" % tmpSol
if self.timeLimit is not None:
cmd += " -time %s" % 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
defaultPath
方法
仅由 pulp.apis.LpSolver_CMD
要求。它返回命令行求解器的默认路径:
def defaultPath(self):
return self.executableExtension("mps_mipcl")
使求解器对 PuLP 可用
修改 pulp/apis/__init__.py
文件以导入你的求解器并将其添加到 _all_solvers
列表中:
from .mipcl_api import MIPCL_CMD
_all_solvers = [
# (...)
MIPCL_CMD,
]
在测试套件中包含求解器
通过在 pulp/tests/test_pulp.py
文件中添加几行对应于你的求解器的代码,将其包含在 PuLP 的测试套件中:
# (...)
class MIPCL_CMDTest(BaseSolverTest.PuLPTest):
solveInst = MIPCL_CMD
额外:添加一个官方求解器API
在使用这些求解器时,还需要考虑一些额外的最佳实践。actualSolve
方法具有以下结构:
def actualSolve(self, lp):
self.buildSolverModel(lp)
# set the initial solution
self.callSolver(lp)
# get the solution information
solutionStatus = self.findSolutionValues(lp)
return solutionStatus
除此之外,buildSolverModel
方法会在 LP 问题中填充一个名为 lp.solverModel 的属性。该属性将包含指向官方求解器 API 中模型对象的指针(例如,GUROBI 的 gurobipy.Model
)。
这些考虑将允许一种一致的、更标准的方式来调用官方求解器。特别是,它们允许进行详细的配置,例如在 这里 解释的配置。