如何向 PuLP 添加新的求解器

Solver API 位于目录 pulp/apis/ 中,并按每个求解器一个文件的方式组织。每个文件可以包含多个API(通常在1到2个之间)。

关于配置求解器以及pulp如何在系统中找到已安装求解器的通用信息,请参见 如何在 PuLP 中配置求解器 部分。

示例

最小的求解器API可以在文件 pulp.apis.mipcl_api.MIPCL_CMD 中找到,该文件位于 pulp/apis/mipcl_api.py。我们将使用它作为示例,列出所有需要进行的更改。

继承基类

求解器需要继承两个类之一:pulp.apis.LpSolverpulp.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,
        )

如示例所示,声明求解器时的主要事项是:

  1. 使用其名称声明一个类级别的属性。

  2. 为求解器定义一些参数(理想情况下,遵循其他求解器的惯例)。

  3. 使用可用数据初始化父类。

  4. (可选) 执行一些其他特定逻辑。

除了这些,求解器还需要实现一些最基本的方法。我们将逐一介绍。

available 方法

pulp.apis.LpSolverpulp.apis.LpSolver_CMD 要求。

如果求解器可用且正常运行,则返回 True。否则返回 False:

def available(self):
    return self.executable(self.path)

actualSolve 方法

pulp.apis.LpSolverpulp.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)。

这些考虑将允许一种一致的、更标准的方式来调用官方求解器。特别是,它们允许进行详细的配置,例如在 这里 解释的配置。