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