source: coopr.plugins/trunk/coopr/plugins/mip/CPLEX.py @ 2048

Last change on this file since 2048 was 2048, checked in by wehart, 10 years ago

Reworking MIP plugins to more selectively print
branch-and-bound information. Don't print
this info unless solving a MIP.

File size: 20.8 KB
Line 
1#  _________________________________________________________________________
2#
3#  Coopr: A COmmon Optimization Python Repository
4#  Copyright (c) 2008 Sandia Corporation.
5#  This software is distributed under the BSD License.
6#  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
7#  the U.S. Government retains certain rights in this software.
8#  For more information, see the Coopr README.txt file.
9#  _________________________________________________________________________
10
11
12import os
13import re
14from coopr.opt.base import *
15from coopr.opt.results import *
16from coopr.opt.solver import *
17from coopr.pyomo.base.var import *
18import mockmip
19import pyutilib.services
20import pyutilib.common
21import pyutilib.misc
22import pyutilib.plugin.core
23import string
24import re
25
26import xml.dom.minidom
27
28
29class CPLEX(ILMLicensedSystemCallSolver):
30    """The CPLEX LP/MIP solver
31    """
32
33    def __init__(self, **kwds):
34        #
35        # Call base class constructor
36        #
37        kwds['type'] = 'cplex'
38        ILMLicensedSystemCallSolver.__init__(self, **kwds)
39
40        # We are currently invoking CPLEX via the command line, with input re-direction. As opposed
41        # to writing our own ilcplex-based C++ driver. Consequently, we need to define an attribute
42        # to retain the execution script name.
43        self.cplex_script_file_name = None
44
45        # NOTE: eventually both of the following attributes should be migrated to a common base class.
46        # is the current solve warm-started? a transient data member to communicate state information
47        # across the _presolve, _apply_solver, and _postsolve methods.
48        self.warm_start_solve = False
49        # related to the above, the temporary name of the MST warm-start file (if any).
50        self.warm_start_file_name = None
51
52        #
53        # Define valid problem formats and associated results formats
54        #
55        self._valid_problem_formats=[ProblemFormat.cpxlp, ProblemFormat.mps]
56        self._valid_result_formats={}
57        self._valid_result_formats[ProblemFormat.cpxlp] = [ResultsFormat.soln]
58        self._valid_result_formats[ProblemFormat.mps] = [ResultsFormat.soln]
59
60    #
61    # ultimately, this utility should go elsewhere - perhaps on the PyomoModel itself.
62    # in the mean time, it is staying here.
63    #
64    def _hasIntegerVariables(self, instance):
65
66       import coopr.pyomo.base.var
67       from coopr.pyomo.base.set_types import IntegerSet, BooleanSet
68
69       for variable in instance.active_components(Var).values():
70
71           if (isinstance(variable.domain, IntegerSet) is True) or (isinstance(variable.domain, BooleanSet) is True):
72
73               return True
74
75       return False
76
77    #
78    # CPLEX has a simple, easy-to-use warm-start capability.
79    #
80    def warm_start_capable(self):
81       return True
82
83    #
84    # write a warm-start file in the CPLEX MST format.
85    #
86    def write_warmstart_file(self, instance):
87
88       import coopr.pyomo.base.var
89
90       self.warm_start_file_name = pyutilib.services.TempfileManager.create_tempfile(suffix = '.cplex.mst')
91
92       doc = xml.dom.minidom.Document()
93       root_element = doc.createElement("CPLEXSolution")
94       root_element.setAttribute("version","1.0")
95       doc.appendChild(root_element)
96
97       # currently not populated.
98       header_element = doc.createElement("header")
99       # currently not populated.       
100       quality_element = doc.createElement("quality")
101       # definitely populated!
102       variables_element = doc.createElement("variables")
103
104       root_element.appendChild(header_element)
105       root_element.appendChild(quality_element)
106       root_element.appendChild(variables_element)
107
108       # for each variable, add a child to the variables element.
109       # both continuous and discrete are accepted (and required,
110       # depending on other options), according to the CPLEX manual.
111       output_index = 0
112       for variable in instance.active_components(Var).values():
113
114           for index in variable._varval.keys():
115
116               if (variable[index].status != coopr.pyomo.base.var.VarStatus.unused) and (variable[index].value != None) and (variable[index].fixed == False):
117
118                   variable_element = doc.createElement("variable")
119                   name = variable[index].label
120                   name = name.replace('[','(')
121                   name = name.replace(']',')')
122                   variable_element.setAttribute("name", name)
123                   variable_element.setAttribute("index", str(output_index))
124                   variable_element.setAttribute("value", str(variable[index].value))
125
126                   variables_element.appendChild(variable_element)
127
128                   output_index = output_index + 1
129
130       mst_file = open(self.warm_start_file_name,'w')
131       doc.writexml(mst_file, indent="    ", newl="\n")
132       mst_file.close()
133
134    # over-ride presolve to extract the warm-start keyword, if specified.
135    def _presolve(self, *args, **kwds):
136
137       # if the first argument is a string (representing a filename),
138       # then we don't have an instance => the solver is being applied
139       # to a file.
140
141       self.warm_start_solve = False               
142       if "warmstart" in kwds:
143          self.warm_start_solve = kwds["warmstart"]
144          del kwds["warmstart"]
145
146       if (len(args) > 0) and (isinstance(args[0],basestring) is False):
147
148          # write the warm-start file - currently only supports MIPs.
149          # we only know how to deal with a single problem instance.       
150          if self.warm_start_solve is True:
151
152             if len(args) != 1:
153                raise ValueError, "CPLEX _presolve method can only handle a single problem instance - "+str(len(args))+" were supplied"                 
154
155             if self._hasIntegerVariables(args[0]) is True:         
156                self.write_warmstart_file(args[0])
157         
158       # let the base class handle any remaining keywords/actions.
159       ILMLicensedSystemCallSolver._presolve(self, *args, **kwds)
160
161    def executable(self):
162        executable = pyutilib.services.registered_executable("cplex")
163        if executable is None:
164            pyutilib.plugin.core.PluginGlobals.env().log.error("Could not locate the 'cplex' executable, which is required for solver %s" % self.name)
165            self.enable = False
166            return None
167        return executable.get_path()
168
169    def create_command_line(self,executable,problem_files):
170        #
171        # Define log file
172        # The log file in CPLEX contains the solution trace, but the solver status can be found in the solution file.
173        #
174        self.log_file = pyutilib.services.TempfileManager.create_tempfile(suffix = '.cplex.log')
175
176        #
177        # Define solution file
178        # As indicated above, contains (in XML) both the solution and solver status.
179        #
180        self.soln_file = pyutilib.services.TempfileManager.create_tempfile(suffix = '.cplex.sol')
181
182        #
183        # Define results file
184        #
185        if self._results_format is None or self._results_format == ResultsFormat.soln:
186           self.results_file = self.soln_file
187        elif self._results_format == ResultsFormat.sol:
188           self.results_file = self.sol_file
189
190        #
191        # Write the CPLEX execution script
192        #
193        self.cplex_script_file_name = pyutilib.services.TempfileManager.create_tempfile(suffix = '.cplex.script')
194        cplex_script_file = open(self.cplex_script_file_name,'w')
195        cplex_script_file.write("set logfile "+self.log_file+"\n")
196        if self._timelimit is not None and self._timelimit > 0.0:
197            cplex_script_file.write("set timelimit "+`self._timelimit`+"\n")
198        if (self.mipgap is not None) and (self.mipgap > 0.0):
199            cplex_script_file.write("set mip tolerances mipgap "+`self.mipgap`+"\n")           
200        for key in self.options:
201                if key in ['relax_integrality']:
202                    pass
203                elif isinstance(self.options[key],basestring) and ' ' in self.options[key]:
204                    opt = " ".join(key.split('_'))+" "+str(self.options[key])
205                else:
206                    opt = " ".join(key.split('_'))+" "+str(self.options[key])
207                cplex_script_file.write("set "+opt+"\n")
208        cplex_script_file.write("read "+problem_files[0]+"\n")
209
210        # if we're dealing with an LP, the MST file will be empty.
211        if (self.warm_start_solve is True) and (self.warm_start_file_name is not None):
212            cplex_script_file.write("read "+self.warm_start_file_name+"\n")
213
214        if 'relax_integrality' in self.options:
215            cplex_script_file.write("change problem lp\n")
216           
217        cplex_script_file.write("display problem stats\n")
218        cplex_script_file.write("optimize\n")
219        cplex_script_file.write("write " + self.soln_file+"\n")
220        cplex_script_file.write("quit\n")
221        cplex_script_file.close()
222
223        # dump the script and warm-start file names for the
224        # user if we're keeping files around.
225        if self.keepFiles:
226           print "Solver script file=" + self.cplex_script_file_name
227           if (self.warm_start_solve is True) and (self.warm_start_file_name is not None):
228              print "Solver warm-start file=" + self.warm_start_file_name
229
230        #
231        # Define command line
232        #
233        if self._problem_format in [ProblemFormat.cpxlp, ProblemFormat.mps]:
234           proc = self._timer + " " + self.executable() + " < " + self.cplex_script_file_name
235        return pyutilib.misc.Bunch(cmd=proc, log_file=self.log_file, env=None)
236
237    def process_logfile(self):
238        """
239        Process logfile
240        """
241        results = SolverResults()
242        results.problem.number_of_variables = None
243        results.problem.number_of_nonzeros = None
244        #
245        # Process logfile
246        #
247        OUTPUT = open(self.log_file)
248        output = "".join(OUTPUT.readlines())
249        OUTPUT.close()
250        #
251        # It is generally useful to know the CPLEX version number for logfile parsing.
252        #
253        cplex_version = None
254       
255        #
256        # Parse logfile lines
257        #
258        for line in output.split("\n"):
259            tokens = re.split('[ \t]+',line.strip())
260            if len(tokens) > 3 and tokens[0] == "CPLEX" and tokens[1] == "Error":
261                results.solver.status=SolverStatus.error
262                results.solver.error = " ".join(tokens)
263            elif len(tokens) >= 3 and tokens[0] == "ILOG" and tokens[1] == "CPLEX":
264                cplex_version = tokens[2].rstrip(',')
265            elif len(tokens) >= 3 and tokens[0] == "Variables":
266                if results.problem.number_of_variables is None: # CPLEX 11.2 and subsequent has two Variables sections.
267                    results.problem.number_of_variables = tokens[2]
268            # In CPLEX 11 (and presumably before), there was only a single line output to
269            # indicate the constriant count, e.g., "Linear constraints : 16 [Less: 7, Greater: 6, Equal: 3]".
270            # In CPLEX 11.2 (or somewhere in between 11 and 11.2 - I haven't bothered to track it down
271            # in that detail), there is another instance of this line prefix in the min/max problem statistics
272            # block - which we don't care about. In this case, the line looks like: "Linear constraints :" and
273            # that's all.
274            elif len(tokens) >= 4 and tokens[0] == "Linear" and tokens[1] == "constraints":
275                results.problem.number_of_constraints = tokens[3]
276            elif len(tokens) >= 3 and tokens[0] == "Nonzeros":
277                if results.problem.number_of_nonzeros is None: # CPLEX 11.2 and subsequent has two Nonzeros sections.               
278                    results.problem.number_of_nonzeros = tokens[2]
279            elif len(tokens) >= 5 and tokens[4] == "MINIMIZE":
280                results.problem.sense = ProblemSense.minimize
281            elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE":
282                results.problem.sense = ProblemSense.maximize
283            elif len(tokens) >= 4 and tokens[0] == "Solution" and tokens[1] == "time" and tokens[2] == "=":
284               # technically, I'm not sure if this is CPLEX user time or user+system - CPLEX doesn't appear
285               # to differentiate, and I'm not sure we can always provide a break-down.
286               results.solver.user_time = eval(tokens[3])
287        return results
288
289    def process_soln_file(self,results):
290        lp_solution=False
291        if not os.path.exists(self.soln_file):
292           return
293
294        soln = Solution()
295        soln.objective['f'].value=None
296        INPUT = open(self.soln_file,"r")
297        results.problem.number_of_objectives=1
298        mip_problem=False
299        for line in INPUT:
300            line = line.strip()
301            #print "LLINE",line
302            line = line.lstrip('<?/')
303            line = line.rstrip('/>?')
304            tokens=line.split(' ')
305
306            if tokens[0] == "variable":
307                variable_name = None
308                variable_value = None
309                variable_reduced_cost = None
310                variable_status = None
311                for i in range(1,len(tokens)):
312                   field_name =  string.strip(tokens[i].split('=')[0])
313                   field_value = (string.strip(tokens[i].split('=')[1])).lstrip("\"").rstrip("\"")
314                   if field_name == "name":
315                      variable_name = field_value
316                   elif field_name == "value":
317                      variable_value = field_value
318                   elif field_name == "reducedCost":
319                      variable_reduced_cost = field_value
320                   elif field_name == "status":
321                      variable_status = field_value
322
323                # skip the "constant-one" variable, used to capture/retain objective offsets in the CPLEX LP format.
324                if variable_name != "ONE_VAR_CONSTANT":
325                   try:
326                       soln.variable[variable_name].value = eval(variable_value)
327                       #print soln.variable[variable_name].value, type(soln.variable[variable_name].value)
328                   except:
329                       soln.variable[variable_name].value = variable_value
330                   if not variable_reduced_cost is None:
331                        try:
332                            soln.variable[variable_name].rc = eval(variable_reduced_cost)
333                            if variable_status is not None:
334                               if variable_status == "LL":
335                                  soln.variable[variable_name].lrc = eval(variable_reduced_cost)
336                               else:
337                                  soln.variable[variable_name].lrc = 0.0
338                               if variable_status == "UL":
339                                  soln.variable[variable_name].urc = eval(variable_reduced_cost)
340                               else:
341                                  soln.variable[variable_name].urc = 0.0
342                        except:
343                            raise ValueError, "Unexpected reduced-cost value="+str(variable_reduced_cost)+" encountered for variable="+variable_name
344            elif tokens[0] == "constraint":
345                constraint_name = None
346                constraint_dual = None
347                for i in range(1,len(tokens)):
348                   field_name =  string.strip(tokens[i].split('=')[0])
349                   field_value = (string.strip(tokens[i].split('=')[1])).lstrip("\"").rstrip("\"")
350                   if field_name == "name":
351                      constraint_name = field_value
352                   elif field_name == "dual": # for LPs
353                      # assumes the name field is first.
354                      if eval(field_value) != 0.0:
355                        soln.constraint[constraint_name].dual = eval(field_value)
356                   elif field_name == "slack": # for MIPs
357                      # assumes the name field is first.
358                      if eval(field_value) != 0.0:
359                        soln.constraint[constraint_name].slack = eval(field_value)
360            elif tokens[0].startswith("problemName"):
361                filename = (string.strip(tokens[0].split('=')[1])).lstrip("\"").rstrip("\"")
362                #print "HERE",filename
363                results.problem.name = os.path.basename(filename)
364                if '.' in results.problem.name:
365                    results.problem.name = results.problem.name.split('.')[0]
366                tINPUT=open(filename,"r")
367                for tline in tINPUT:
368                    tline = tline.strip()
369                    if tline == "":
370                        continue
371                    tokens = re.split('[\t ]+',tline)
372                    if tokens[0][0] in ['\\', '*']:
373                        continue
374                    elif tokens[0] == "NAME":
375                        results.problem.name = tokens[1]
376                    else:
377                        sense = tokens[0].lower()
378                        if sense in ['max','maximize']:
379                            results.problem.sense = ProblemSense.maximize
380                        if sense in ['min','minimize']:
381                            results.problem.sense = ProblemSense.minimize
382                    break
383                tINPUT.close()
384               
385            elif tokens[0].startswith("objectiveValue"):
386                objective_value = (string.strip(tokens[0].split('=')[1])).lstrip("\"").rstrip("\"")
387                soln.objective['f'].value = objective_value
388            elif tokens[0].startswith("solutionStatusString"):
389                solution_status = (string.strip(" ".join(tokens).split('=')[1])).lstrip("\"").rstrip("\"")
390                if solution_status in ["optimal", "integer optimal solution"]:
391                    soln.status = SolutionStatus.optimal
392                    soln.gap = 0.0
393                    if results.problem.sense == ProblemSense.minimize:
394                        results.problem.lower_bound = soln.objective['f'].value
395                        if "upper_bound" in dir(results.problem):
396                            del results.problem.upper_bound
397                    else:
398                        results.problem.upper_bound = soln.objective['f'].value
399                        if "lower_bound" in dir(results.problem):
400                            del results.problem.lower_bound
401                    mip_problem=True
402            elif tokens[0].startswith("MIPNodes"):
403                if mip_problem:
404                    n = eval(string.strip(" ".join(tokens).split('=')[1])).lstrip("\"").rstrip("\"")
405                    results.solver.statistics.branch_and_bound.number_of_created_subproblems=n
406                    results.solver.statistics.branch_and_bound.number_of_bounded_subproblems=n
407
408               
409        if not results.solver.status is SolverStatus.error:
410            results.solution.insert(soln)
411        INPUT.close()
412
413    def _postsolve(self):
414
415        # take care of the annoying (and empty) CPLEX temporary files in the current directory.
416        # this approach doesn't seem overly efficient, but python os module functions don't
417        # accept regular expression directly.
418        filename_list = os.listdir(".")
419        for filename in filename_list:
420           # CPLEX temporary files come in two flavors - cplex.log and clone*.log.
421           # the latter is the case for multi-processor environments.
422           # IMPT: trap the possible exception raised by the file not existing.
423           #       this can occur in pyro environments where > 1 workers are
424           #       running CPLEX, and were started from the same directory.
425           #       these logs don't matter anyway (we redirect everything),
426           #       and are largely an annoyance.
427           try:
428              if  re.match('cplex\.log', filename) != None:
429                  os.remove(filename)
430              elif re.match('clone\d+\.log', filename) != None:
431                  os.remove(filename)
432           except OSError:
433              pass
434
435        # let the base class deal with returning results.
436        return ILMLicensedSystemCallSolver._postsolve(self)           
437
438
439class MockCPLEX(CPLEX,mockmip.MockMIP):
440    """A Mock CPLEX solver used for testing
441    """
442
443    def __init__(self, **kwds):
444        try:
445           CPLEX.__init__(self, **kwds)
446        except pyutilib.common.ApplicationError: #pragma:nocover
447           pass                        #pragma:nocover
448        mockmip.MockMIP.__init__(self,"cplex")
449
450    def available(self, exception_flag=True):
451        return CPLEX.available(self,exception_flag)
452
453    def create_command_line(self,executable,problem_files):
454        command = CPLEX.create_command_line(self,executable,problem_files)
455        mockmip.MockMIP.create_command_line(self,executable,problem_files)
456        return command
457
458    def executable(self):
459        return mockmip.MockMIP.executable(self)
460
461    def _execute_command(self,cmd):
462        return mockmip.MockMIP._execute_command(self,cmd)
463
464
465pyutilib.services.register_executable(name="cplex")
466SolverRegistration("cplex", CPLEX)
467SolverRegistration("_mock_cplex", MockCPLEX)
Note: See TracBrowser for help on using the repository browser.