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

Last change on this file since 1768 was 1768, checked in by wehart, 11 years ago

Rework of Coopr to use the new PyUtilib? package decomposition.

NOTE: to use Coopr with this update, we need to work with a new version of coopr_install.

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