1from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log 2from .core import ( 3 cplex_dll_path, 4 ctypesArrayFill, 5 ilm_cplex_license, 6 ilm_cplex_license_signature, 7 to_string, 8) 9from .. import constants, sparse 10import os 11import warnings 12import re 13 14 15class CPLEX_CMD(LpSolver_CMD): 16 """The CPLEX LP solver""" 17 18 name = "CPLEX_CMD" 19 20 def __init__( 21 self, 22 timelimit=None, 23 mip=True, 24 msg=True, 25 timeLimit=None, 26 gapRel=None, 27 gapAbs=None, 28 options=None, 29 warmStart=False, 30 keepFiles=False, 31 path=None, 32 threads=None, 33 logPath=None, 34 maxMemory=None, 35 maxNodes=None, 36 mip_start=False, 37 ): 38 """ 39 :param bool mip: if False, assume LP even if integer variables 40 :param bool msg: if False, no log is shown 41 :param float timeLimit: maximum time for solver (in seconds) 42 :param float gapRel: relative gap tolerance for the solver to stop (in fraction) 43 :param float gapAbs: absolute gap tolerance for the solver to stop 44 :param int threads: sets the maximum number of threads 45 :param list options: list of additional options to pass to solver 46 :param bool warmStart: if True, the solver will use the current value of variables as a start 47 :param bool keepFiles: if True, files are saved in the current directory and not deleted after solving 48 :param str path: path to the solver binary 49 :param str logPath: path to the log file 50 :param float maxMemory: max memory to use during the solving. Stops the solving when reached. 51 :param int maxNodes: max number of nodes during branching. Stops the solving when reached. 52 :param bool mip_start: deprecated for warmStart 53 :param float timelimit: deprecated for timeLimit 54 """ 55 if timelimit is not None: 56 warnings.warn("Parameter timelimit is being depreciated for timeLimit") 57 if timeLimit is not None: 58 warnings.warn( 59 "Parameter timeLimit and timelimit passed, using timeLimit " 60 ) 61 else: 62 timeLimit = timelimit 63 if mip_start: 64 warnings.warn("Parameter mip_start is being depreciated for warmStart") 65 if warmStart: 66 warnings.warn( 67 "Parameter mipStart and mip_start passed, using warmStart" 68 ) 69 else: 70 warmStart = mip_start 71 LpSolver_CMD.__init__( 72 self, 73 gapRel=gapRel, 74 mip=mip, 75 msg=msg, 76 timeLimit=timeLimit, 77 options=options, 78 maxMemory=maxMemory, 79 maxNodes=maxNodes, 80 warmStart=warmStart, 81 path=path, 82 keepFiles=keepFiles, 83 threads=threads, 84 gapAbs=gapAbs, 85 logPath=logPath, 86 ) 87 88 def defaultPath(self): 89 return self.executableExtension("cplex") 90 91 def available(self): 92 """True if the solver is available""" 93 return self.executable(self.path) 94 95 def actualSolve(self, lp): 96 """Solve a well formulated lp problem""" 97 if not self.executable(self.path): 98 raise PulpSolverError("PuLP: cannot execute " + self.path) 99 tmpLp, tmpSol, tmpMst = self.create_tmp_files(lp.name, "lp", "sol", "mst") 100 vs = lp.writeLP(tmpLp, writeSOS=1) 101 try: 102 os.remove(tmpSol) 103 except: 104 pass 105 if not self.msg: 106 cplex = subprocess.Popen( 107 self.path, 108 stdin=subprocess.PIPE, 109 stdout=subprocess.PIPE, 110 stderr=subprocess.PIPE, 111 ) 112 else: 113 cplex = subprocess.Popen(self.path, stdin=subprocess.PIPE) 114 cplex_cmds = "read " + tmpLp + "\n" 115 if self.optionsDict.get("warmStart", False): 116 self.writesol(filename=tmpMst, vs=vs) 117 cplex_cmds += "read " + tmpMst + "\n" 118 cplex_cmds += "set advance 1\n" 119 120 if self.timeLimit is not None: 121 cplex_cmds += "set timelimit " + str(self.timeLimit) + "\n" 122 options = self.options + self.getOptions() 123 for option in options: 124 cplex_cmds += option + "\n" 125 if lp.isMIP(): 126 if self.mip: 127 cplex_cmds += "mipopt\n" 128 cplex_cmds += "change problem fixed\n" 129 else: 130 cplex_cmds += "change problem lp\n" 131 cplex_cmds += "optimize\n" 132 cplex_cmds += "write " + tmpSol + "\n" 133 cplex_cmds += "quit\n" 134 cplex_cmds = cplex_cmds.encode("UTF-8") 135 cplex.communicate(cplex_cmds) 136 if cplex.returncode != 0: 137 raise PulpSolverError("PuLP: Error while trying to execute " + self.path) 138 if not os.path.exists(tmpSol): 139 status = constants.LpStatusInfeasible 140 values = reducedCosts = shadowPrices = slacks = solStatus = None 141 else: 142 ( 143 status, 144 values, 145 reducedCosts, 146 shadowPrices, 147 slacks, 148 solStatus, 149 ) = self.readsol(tmpSol) 150 self.delete_tmp_files(tmpLp, tmpMst, tmpSol) 151 if self.optionsDict.get("logPath") != "cplex.log": 152 self.delete_tmp_files("cplex.log") 153 if status != constants.LpStatusInfeasible: 154 lp.assignVarsVals(values) 155 lp.assignVarsDj(reducedCosts) 156 lp.assignConsPi(shadowPrices) 157 lp.assignConsSlack(slacks) 158 lp.assignStatus(status, solStatus) 159 return status 160 161 def getOptions(self): 162 # CPLEX parameters: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.6.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/settingParams.html 163 # CPLEX status: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/macros/Solution_status_codes.html 164 params_eq = dict( 165 logPath="set logFile {}", 166 gapRel="set mip tolerances mipgap {}", 167 gapAbs="set mip tolerances absmipgap {}", 168 maxMemory="set mip limits treememory {}", 169 threads="set threads {}", 170 maxNodes="set mip limits nodes {}", 171 ) 172 return [ 173 v.format(self.optionsDict[k]) 174 for k, v in params_eq.items() 175 if k in self.optionsDict and self.optionsDict[k] is not None 176 ] 177 178 def readsol(self, filename): 179 """Read a CPLEX solution file""" 180 # CPLEX solution codes: http://www-eio.upc.es/lceio/manuals/cplex-11/html/overviewcplex/statuscodes.html 181 try: 182 import xml.etree.ElementTree as et 183 except ImportError: 184 import elementtree.ElementTree as et 185 solutionXML = et.parse(filename).getroot() 186 solutionheader = solutionXML.find("header") 187 statusString = solutionheader.get("solutionStatusString") 188 statusValue = solutionheader.get("solutionStatusValue") 189 cplexStatus = { 190 "1": constants.LpStatusOptimal, # optimal 191 "101": constants.LpStatusOptimal, # mip optimal 192 "102": constants.LpStatusOptimal, # mip optimal tolerance 193 "104": constants.LpStatusOptimal, # max solution limit 194 "105": constants.LpStatusOptimal, # node limit feasible 195 "107": constants.LpStatusOptimal, # time lim feasible 196 "109": constants.LpStatusOptimal, # fail but feasible 197 "113": constants.LpStatusOptimal, # abort feasible 198 } 199 if statusValue not in cplexStatus: 200 raise PulpSolverError( 201 "Unknown status returned by CPLEX: \ncode: '{}', string: '{}'".format( 202 statusValue, statusString 203 ) 204 ) 205 status = cplexStatus[statusValue] 206 # we check for integer feasible status to differentiate from optimal in solution status 207 cplexSolStatus = { 208 "104": constants.LpSolutionIntegerFeasible, # max solution limit 209 "105": constants.LpSolutionIntegerFeasible, # node limit feasible 210 "107": constants.LpSolutionIntegerFeasible, # time lim feasible 211 "109": constants.LpSolutionIntegerFeasible, # fail but feasible 212 "111": constants.LpSolutionIntegerFeasible, # memory limit feasible 213 "113": constants.LpSolutionIntegerFeasible, # abort feasible 214 } 215 solStatus = cplexSolStatus.get(statusValue) 216 shadowPrices = {} 217 slacks = {} 218 constraints = solutionXML.find("linearConstraints") 219 for constraint in constraints: 220 name = constraint.get("name") 221 shadowPrice = constraint.get("dual") 222 slack = constraint.get("slack") 223 shadowPrices[name] = float(shadowPrice) 224 slacks[name] = float(slack) 225 226 values = {} 227 reducedCosts = {} 228 for variable in solutionXML.find("variables"): 229 name = variable.get("name") 230 value = variable.get("value") 231 reducedCost = variable.get("reducedCost") 232 values[name] = float(value) 233 reducedCosts[name] = float(reducedCost) 234 235 return status, values, reducedCosts, shadowPrices, slacks, solStatus 236 237 def writesol(self, filename, vs): 238 """Writes a CPLEX solution file""" 239 try: 240 import xml.etree.ElementTree as et 241 except ImportError: 242 import elementtree.ElementTree as et 243 root = et.Element("CPLEXSolution", version="1.2") 244 attrib_head = dict() 245 attrib_quality = dict() 246 et.SubElement(root, "header", attrib=attrib_head) 247 et.SubElement(root, "header", attrib=attrib_quality) 248 variables = et.SubElement(root, "variables") 249 250 values = [(v.name, v.value()) for v in vs if v.value() is not None] 251 for index, (name, value) in enumerate(values): 252 attrib_vars = dict(name=name, value=str(value), index=str(index)) 253 et.SubElement(variables, "variable", attrib=attrib_vars) 254 mst = et.ElementTree(root) 255 mst.write(filename, encoding="utf-8", xml_declaration=True) 256 257 return True 258 259 260class CPLEX_PY(LpSolver): 261 """ 262 The CPLEX LP/MIP solver (via a Python Binding) 263 264 This solver wraps the python api of cplex. 265 It has been tested against cplex 12.3. 266 For api functions that have not been wrapped in this solver please use 267 the base cplex classes 268 """ 269 270 name = "CPLEX_PY" 271 try: 272 global cplex 273 import cplex 274 except (Exception) as e: 275 err = e 276 """The CPLEX LP/MIP solver from python PHANTOM Something went wrong!!!!""" 277 278 def available(self): 279 """True if the solver is available""" 280 return False 281 282 def actualSolve(self, lp): 283 """Solve a well formulated lp problem""" 284 raise PulpSolverError("CPLEX_PY: Not Available:\n{}".format(self.err)) 285 286 else: 287 288 def __init__( 289 self, 290 mip=True, 291 msg=True, 292 timeLimit=None, 293 gapRel=None, 294 warmStart=False, 295 logPath=None, 296 epgap=None, 297 logfilename=None, 298 ): 299 """ 300 :param bool mip: if False, assume LP even if integer variables 301 :param bool msg: if False, no log is shown 302 :param float timeLimit: maximum time for solver (in seconds) 303 :param float gapRel: relative gap tolerance for the solver to stop (in fraction) 304 :param bool warmStart: if True, the solver will use the current value of variables as a start 305 :param str logPath: path to the log file 306 :param float epgap: deprecated for gapRel 307 :param str logfilename: deprecated for logPath 308 """ 309 if epgap is not None: 310 warnings.warn("Parameter epgap is being depreciated for gapRel") 311 if gapRel is not None: 312 warnings.warn("Parameter gapRel and epgap passed, using gapRel") 313 else: 314 gapRel = epgap 315 if logfilename is not None: 316 warnings.warn("Parameter logfilename is being depreciated for logPath") 317 if logPath is not None: 318 warnings.warn( 319 "Parameter logPath and logfilename passed, using logPath" 320 ) 321 else: 322 logPath = logfilename 323 324 LpSolver.__init__( 325 self, 326 gapRel=gapRel, 327 mip=mip, 328 msg=msg, 329 timeLimit=timeLimit, 330 warmStart=warmStart, 331 logPath=logPath, 332 ) 333 334 def available(self): 335 """True if the solver is available""" 336 return True 337 338 def actualSolve(self, lp, callback=None): 339 """ 340 Solve a well formulated lp problem 341 342 creates a cplex model, variables and constraints and attaches 343 them to the lp model which it then solves 344 """ 345 self.buildSolverModel(lp) 346 # set the initial solution 347 log.debug("Solve the Model using cplex") 348 self.callSolver(lp) 349 # get the solution information 350 solutionStatus = self.findSolutionValues(lp) 351 for var in lp._variables: 352 var.modified = False 353 for constraint in lp.constraints.values(): 354 constraint.modified = False 355 return solutionStatus 356 357 def buildSolverModel(self, lp): 358 """ 359 Takes the pulp lp model and translates it into a cplex model 360 """ 361 model_variables = lp.variables() 362 self.n2v = dict((var.name, var) for var in model_variables) 363 if len(self.n2v) != len(model_variables): 364 raise PulpSolverError( 365 "Variables must have unique names for cplex solver" 366 ) 367 log.debug("create the cplex model") 368 self.solverModel = lp.solverModel = cplex.Cplex() 369 log.debug("set the name of the problem") 370 if not self.mip: 371 self.solverModel.set_problem_name(lp.name) 372 log.debug("set the sense of the problem") 373 if lp.sense == constants.LpMaximize: 374 lp.solverModel.objective.set_sense( 375 lp.solverModel.objective.sense.maximize 376 ) 377 obj = [float(lp.objective.get(var, 0.0)) for var in model_variables] 378 379 def cplex_var_lb(var): 380 if var.lowBound is not None: 381 return float(var.lowBound) 382 else: 383 return -cplex.infinity 384 385 lb = [cplex_var_lb(var) for var in model_variables] 386 387 def cplex_var_ub(var): 388 if var.upBound is not None: 389 return float(var.upBound) 390 else: 391 return cplex.infinity 392 393 ub = [cplex_var_ub(var) for var in model_variables] 394 colnames = [var.name for var in model_variables] 395 396 def cplex_var_types(var): 397 if var.cat == constants.LpInteger: 398 return "I" 399 else: 400 return "C" 401 402 ctype = [cplex_var_types(var) for var in model_variables] 403 ctype = "".join(ctype) 404 lp.solverModel.variables.add( 405 obj=obj, lb=lb, ub=ub, types=ctype, names=colnames 406 ) 407 rows = [] 408 senses = [] 409 rhs = [] 410 rownames = [] 411 for name, constraint in lp.constraints.items(): 412 # build the expression 413 expr = [(var.name, float(coeff)) for var, coeff in constraint.items()] 414 if not expr: 415 # if the constraint is empty 416 rows.append(([], [])) 417 else: 418 rows.append(list(zip(*expr))) 419 if constraint.sense == constants.LpConstraintLE: 420 senses.append("L") 421 elif constraint.sense == constants.LpConstraintGE: 422 senses.append("G") 423 elif constraint.sense == constants.LpConstraintEQ: 424 senses.append("E") 425 else: 426 raise PulpSolverError("Detected an invalid constraint type") 427 rownames.append(name) 428 rhs.append(float(-constraint.constant)) 429 lp.solverModel.linear_constraints.add( 430 lin_expr=rows, senses=senses, rhs=rhs, names=rownames 431 ) 432 log.debug("set the type of the problem") 433 if not self.mip: 434 self.solverModel.set_problem_type(cplex.Cplex.problem_type.LP) 435 log.debug("set the logging") 436 if not self.msg: 437 self.setlogfile(None) 438 logPath = self.optionsDict.get("logPath") 439 if logPath is not None: 440 if self.msg: 441 warnings.warn( 442 "`logPath` argument replaces `msg=1`. The output will be redirected to the log file." 443 ) 444 self.setlogfile(open(logPath, "w")) 445 gapRel = self.optionsDict.get("gapRel") 446 if gapRel is not None: 447 self.changeEpgap(gapRel) 448 if self.timeLimit is not None: 449 self.setTimeLimit(self.timeLimit) 450 if self.optionsDict.get("warmStart", False): 451 # We assume "auto" for the effort_level 452 effort = self.solverModel.MIP_starts.effort_level.auto 453 start = [ 454 (k, v.value()) for k, v in self.n2v.items() if v.value() is not None 455 ] 456 if not start: 457 warnings.warn("No variable with value found: mipStart aborted") 458 return 459 ind, val = zip(*start) 460 self.solverModel.MIP_starts.add( 461 cplex.SparsePair(ind=ind, val=val), effort, "1" 462 ) 463 464 def setlogfile(self, fileobj): 465 """ 466 sets the logfile for cplex output 467 """ 468 self.solverModel.set_error_stream(fileobj) 469 self.solverModel.set_log_stream(fileobj) 470 self.solverModel.set_warning_stream(fileobj) 471 self.solverModel.set_results_stream(fileobj) 472 473 def changeEpgap(self, epgap=10 ** -4): 474 """ 475 Change cplex solver integer bound gap tolerence 476 """ 477 self.solverModel.parameters.mip.tolerances.mipgap.set(epgap) 478 479 def setTimeLimit(self, timeLimit=0.0): 480 """ 481 Make cplex limit the time it takes --added CBM 8/28/09 482 """ 483 self.solverModel.parameters.timelimit.set(timeLimit) 484 485 def callSolver(self, isMIP): 486 """Solves the problem with cplex""" 487 # solve the problem 488 self.solveTime = -clock() 489 self.solverModel.solve() 490 self.solveTime += clock() 491 492 def findSolutionValues(self, lp): 493 CplexLpStatus = { 494 lp.solverModel.solution.status.MIP_optimal: constants.LpStatusOptimal, 495 lp.solverModel.solution.status.optimal: constants.LpStatusOptimal, 496 lp.solverModel.solution.status.optimal_tolerance: constants.LpStatusOptimal, 497 lp.solverModel.solution.status.infeasible: constants.LpStatusInfeasible, 498 lp.solverModel.solution.status.infeasible_or_unbounded: constants.LpStatusInfeasible, 499 lp.solverModel.solution.status.MIP_infeasible: constants.LpStatusInfeasible, 500 lp.solverModel.solution.status.MIP_infeasible_or_unbounded: constants.LpStatusInfeasible, 501 lp.solverModel.solution.status.unbounded: constants.LpStatusUnbounded, 502 lp.solverModel.solution.status.MIP_unbounded: constants.LpStatusUnbounded, 503 lp.solverModel.solution.status.abort_dual_obj_limit: constants.LpStatusNotSolved, 504 lp.solverModel.solution.status.abort_iteration_limit: constants.LpStatusNotSolved, 505 lp.solverModel.solution.status.abort_obj_limit: constants.LpStatusNotSolved, 506 lp.solverModel.solution.status.abort_relaxed: constants.LpStatusNotSolved, 507 lp.solverModel.solution.status.abort_time_limit: constants.LpStatusNotSolved, 508 lp.solverModel.solution.status.abort_user: constants.LpStatusNotSolved, 509 lp.solverModel.solution.status.MIP_abort_feasible: constants.LpStatusOptimal, 510 lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpStatusOptimal, 511 lp.solverModel.solution.status.MIP_time_limit_infeasible: constants.LpStatusInfeasible, 512 } 513 lp.cplex_status = lp.solverModel.solution.get_status() 514 status = CplexLpStatus.get(lp.cplex_status, constants.LpStatusUndefined) 515 CplexSolStatus = { 516 lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpSolutionIntegerFeasible, 517 lp.solverModel.solution.status.MIP_abort_feasible: constants.LpSolutionIntegerFeasible, 518 lp.solverModel.solution.status.MIP_feasible: constants.LpSolutionIntegerFeasible, 519 } 520 # TODO: I did not find the following status: CPXMIP_NODE_LIM_FEAS, CPXMIP_MEM_LIM_FEAS 521 sol_status = CplexSolStatus.get(lp.cplex_status) 522 lp.assignStatus(status, sol_status) 523 var_names = [var.name for var in lp._variables] 524 con_names = [con for con in lp.constraints] 525 try: 526 objectiveValue = lp.solverModel.solution.get_objective_value() 527 variablevalues = dict( 528 zip(var_names, lp.solverModel.solution.get_values(var_names)) 529 ) 530 lp.assignVarsVals(variablevalues) 531 constraintslackvalues = dict( 532 zip(con_names, lp.solverModel.solution.get_linear_slacks(con_names)) 533 ) 534 lp.assignConsSlack(constraintslackvalues) 535 if lp.solverModel.get_problem_type() == cplex.Cplex.problem_type.LP: 536 variabledjvalues = dict( 537 zip( 538 var_names, 539 lp.solverModel.solution.get_reduced_costs(var_names), 540 ) 541 ) 542 lp.assignVarsDj(variabledjvalues) 543 constraintpivalues = dict( 544 zip( 545 con_names, 546 lp.solverModel.solution.get_dual_values(con_names), 547 ) 548 ) 549 lp.assignConsPi(constraintpivalues) 550 except cplex.exceptions.CplexSolverError: 551 # raises this error when there is no solution 552 pass 553 # put pi and slack variables against the constraints 554 # TODO: clear up the name of self.n2c 555 if self.msg: 556 print("Cplex status=", lp.cplex_status) 557 lp.resolveOK = True 558 for var in lp._variables: 559 var.isModified = False 560 return status 561 562 def actualResolve(self, lp, **kwargs): 563 """ 564 looks at which variables have been modified and changes them 565 """ 566 raise NotImplementedError("Resolves in CPLEX_PY not yet implemented") 567 568 569CPLEX = CPLEX_CMD 570