# 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