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

Last change on this file since 1719 was 1719, checked in by jwatson, 11 years ago

Fixed minor bug relating to read of variable and non-zero counts under CPLEX >= version 11.2.

Now, cplex, glpk, cbc, and pico all report identical solutions to the farmer extensive form!

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