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:
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")
|
||||
Reference in New Issue
Block a user