source: coopr.pyomo/trunk/coopr/pyomo/io/cpxlp.py @ 2170

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

A rework of the NL/LP file writers. This now uses the
canonical expression representation. I'm pretty sure that the LP writer
is OK, but much less sure about the NL writer. Also, it's clear that we don't
have adequate tests of the writers to ensure that all different types of
models are written correctly.

File size: 15.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#
12# Problem Writer for CPLEX LP Format Files
13#
14
15from coopr.opt import ProblemFormat
16from coopr.pyomo.base import expr, Var, Constraint, Objective
17from coopr.pyomo.base.var import _VarValue, _VarBase
18from coopr.pyomo.base.param import _ParamValue
19from coopr.pyomo.base.numtypes import minimize, maximize
20from coopr.pyomo.base.expr import *
21from coopr.pyomo.base import *
22from coopr.opt.base import problem, AbstractProblemWriter
23from coopr.pyomo.expr import *
24import math
25
26def convert_name(namestr):
27    namestr = namestr.replace('[','(')
28    namestr = namestr.replace(']',')')
29    return namestr
30
31class ProblemWriter_cpxlp(AbstractProblemWriter):
32
33    def __init__(self):
34        AbstractProblemWriter.__init__(self,ProblemFormat.cpxlp)
35
36        # temporarily placing attributes used in extensive-form writing
37        # here, for eventual migration to the base class.
38        self._output_objectives = True
39        self._output_constraints = True       
40        self._output_variables = True # means we're outputting *some* variables - types determined by following flags.
41
42        # building on the above, partition out the variables that should
43        # be written if the _output_variables attribute is set to True.
44        # this is useful when writing components of multiple models to a
45        # single LP file. unfortunately, the CPLEX LP format requires all
46        # integer and binary (and continuous) variables to appear in a
47        # single continuous block.
48        self._output_continuous_variables = True
49        self._output_integer_variables = True
50        self._output_binary_variables = True
51
52        # do I pre-pend the model name to all identifiers I output?
53        # useful in cases where you are writing components of multiple
54        # models to a single output LP file.
55        self._output_prefixes = False 
56
57    def __call__(self, model, filename):
58        if filename is None:
59           filename = model.name + ".lp"
60        OUTPUT=open(filename,"w")
61        self._print_model_LP(model,OUTPUT)
62        OUTPUT.close()
63        return filename, None
64
65    def _get_bound(self, exp):
66        if isinstance(exp,expr._IdentityExpression):
67           return self._get_bound(exp._args[0])
68        elif isinstance(exp,NumericConstant):
69           return exp.value
70        else:
71           raise ValueError, "ERROR: nonconstant bound: " + str(exp)
72           return None
73
74    def _print_bound(self, exp, OUTPUT, offset=0.0):
75        if isinstance(exp,expr._IdentityExpression):
76           self._print_bound(exp._args[0], OUTPUT, offset)
77        elif isinstance(exp,NumericConstant) or isinstance(exp,_ParamValue):
78           print >>OUTPUT, exp.value+offset
79        else:
80           # Idea: Bundle the following into a single string and pass with thrown exception
81           print "ERROR: nonconstant constraint bound"
82           print "Expression type=" + exp.__class__.__name__
83           if not exp is None:
84                print "Offending expression:"
85                exp.pprint()
86           raise ValueError
87
88    def _collect_key(self, a):
89        return a[0]
90
91    def _print_expr(self, model, x, OUTPUT, print_offset=False):
92        max_terms_on_line=5 # this is the line-limit hack
93        terms_output = 0
94        #
95        # Linear
96        #
97        if 1 in x:
98            keys = x[1].keys()
99            keys.sort()
100            for id in keys:
101                coef = x[1][id]
102                name = convert_name(model._var[id.keys()[0]].name)
103                print >>OUTPUT, ('- %f' % math.fabs(coef) if coef < 0.0 else '+ %f' % coef), name,
104                terms_output += 1
105                if terms_output >= max_terms_on_line:
106                    print >>OUTPUT, ""
107                    print >>OUTPUT, "     ",
108                    terms_output = 0
109        #
110        # Quadratic
111        #
112        if 2 in x:
113            keys = x[2].keys()
114            keys.sort()
115            for id in keys:
116                coef = x[2][id]
117                print >>OUTPUT, ('- %f' % math.fabs(coef) if coef < 0.0 else '+ %f' % coef),
118                for var in id:
119                    name = convert_name(model._var[var].name)
120                    if id[var] > 1:
121                        print "%s^%d" % (name,id[var]),
122                    else:
123                        print name,
124                terms_output += 1
125                if terms_output >= max_terms_on_line:
126                    print >>OUTPUT, ""
127                    print >>OUTPUT, "     ",
128                    terms_output = 0
129        #
130        # Constant offset
131        #
132        offset=0.0
133        if 0 in x:
134            offset = x[0][None]
135        if print_offset and offset != 0.0:
136            print >>OUTPUT, ('- %f' % math.fabs(offset) if offset < 0.0 else '+ %f' % offset), "ONE_VAR_CONSTANT"
137            terms_output += 1
138        #
139        # Return constant offset
140        #
141        return offset
142
143    def _print_model_LP(self, model, OUTPUT):
144
145        _obj = model.active_components(Objective)
146       
147        #
148        # Objective
149        #
150        if self._output_objectives is True:
151           #printed_quadterm = False
152           if len(_obj) == 0:
153              raise ValueError, "ERROR: No objectives defined for input model=" + str(model.name) + "; cannot write legal LP file"
154           if _obj[ _obj.keys()[0] ].sense == maximize:
155              print >>OUTPUT, "max "
156           else:
157              print >>OUTPUT, "min "
158           print >>OUTPUT, "obj: ",
159           obj = _obj[ _obj.keys()[0] ]
160           for key in obj:
161                if is_constant(obj[key].repn):
162                    print "WARNING: ignoring objective %s[%s] which is constant" % (str(obj),str(key))
163                    continue
164                if is_nonlinear(obj[key].repn) and not is_quadratic(obj[key].repn):
165                    raise ValueError, "Cannot write legal LP file.  Objective %s[%s] has nonlinear terms that are not quadratic." % (str(obj),str(key))
166                self._print_expr(model, obj[key].repn, OUTPUT, print_offset=True)
167           print >>OUTPUT, ""
168       
169        #
170        # Constraints
171        #
172        # if there are no non-trivial constraints, you'll end up with an empty constraint block. CPLEX is OK with
173        # this, but GLPK isn't. And eliminating the constraint block (i.e., the "s.t." line) causes GLPK to whine
174        # elsewhere. output a warning if the constraint block is empty, so users can quickly determine the cause
175        # of the solve failure.
176
177        if self._output_constraints is True:
178
179           # for now, if this routine isn't writing everything, then assume a meta-level handler is
180           # dealing with the writing of the transitional elements - these should probably be done in
181           # the form of "end_objective()" and "end_constraint()" helper methods.                   
182           if (self._output_objectives is True) and (self._output_variables is True):
183              print >>OUTPUT, "s.t."
184           
185           CON = model.active_components(Constraint)
186           have_nontrivial=False
187           for key in CON:
188             if CON[key].trivial:
189                continue
190             have_nontrivial=True
191             i=0
192             C = CON[key]
193             for cndx in C:
194               if not C[cndx].active:
195                    continue
196               try:
197                    # there are conditions, e.g., when fixing variables, under which a constraint block might be empty.
198                    # ignore these, for both practical reasons and the fact that the CPLEX LP format requires a variable
199                    # in the constraint body. it is also possible that the body of the constraint consists of only a
200                    # constant, in which case the "variable" of
201                    if is_constant(C[cndx].repn):
202                        print "WARNING: ignoring constraint %s[%s] which is constant" % (str(C),str(cndx))
203                        continue
204                    if is_nonlinear(C[cndx].repn):
205                        raise ValueError, "Cannot write legal LP file.  Constraint %s[%s] has a body with nonlinear terms." % (str(C),str(cndx))
206                    prefix = ""
207                    if self._output_prefixes is True:
208                        if C.model is None:
209                           raise RuntimeError, "Constraint="+C._data[cndx].label+" has no model attribute - no label prefix can be assigned"
210                        prefix = C.model.name+"_"
211
212                    if C._data[cndx]._equality:
213                         #
214                         # Equality constraint
215                         #
216                         print >>OUTPUT, prefix + "c_e_" + convert_name(C._data[cndx].label) + "_: ",
217                         offset = self._print_expr(model, C[cndx].repn, OUTPUT)
218                         print >>OUTPUT, "=",
219                         self._print_bound(C._data[cndx].lower, OUTPUT, -offset)
220                         print >>OUTPUT, ""
221                         i=i+1
222                    else:
223                         #
224                         # Inequality constraint
225                         #
226                         if C._data[cndx].lower is not None:
227                            print >>OUTPUT, prefix + "c_l_" + convert_name(C._data[cndx].label) + "_: ",
228                            offset = self._print_expr(model, C[cndx].repn, OUTPUT)
229                            print >>OUTPUT, ">=",
230                            self._print_bound(C._data[cndx].lower, OUTPUT, -offset)
231                            print >>OUTPUT, ""
232                            i=i+1
233                         if C._data[cndx].upper is not None:
234                            print >>OUTPUT, prefix + "c_u_" + convert_name(C._data[cndx].label) + "_: ",
235                            offset = self._print_expr(model, C[cndx].repn, OUTPUT)
236                            print >>OUTPUT, "<=",
237                            self._print_bound(C._data[cndx].upper, OUTPUT, -offset)
238                            print >>OUTPUT, ""
239                            i=i+1
240               except ValueError, msg:
241                  print msg
242                  raise ValueError, "ERROR: Failed to output constraint="+C.name+", index="+`cndx`+" in generation of LP format file"
243           if not have_nontrivial:
244             print "WARNING: Empty constraint block written in LP format - solver may error"
245         
246           # the CPLEX LP format doesn't allow constants in the objective (or constraint body), which is a bit silly.
247           # to avoid painful book-keeping, we introduce the following "variable", constrained to the value 1. this is
248           # used when quadratic terms are present. worst-case, if not used, is that CPLEX easily pre-processes it out.
249           print >>OUTPUT, "c_e_ONE_VAR_CONSTANT: ONE_VAR_CONSTANT = 1.0"
250           print >>OUTPUT, ""
251           
252        #
253        # Bounds
254        #
255
256        if self._output_variables is True:
257
258           # for now, if this routine isn't writing everything, then assume a meta-level handler is
259           # dealing with the writing of the transitional elements - these should probably be done in
260           # the form of "end_objective()" and "end_constraint()" helper methods.                   
261           if (self._output_objectives is True) and (self._output_constraints is True):
262              print >>OUTPUT, "bounds "
263
264           # scan all variables even if we're only writing a subset of them. required
265           # because we don't store maps by variable type currently.
266           
267           # track the number of integer and binary variables, so you can output their status later.
268           niv = nbv = 0
269           VAR = model.active_components(Var)
270           for var in VAR.values():
271              if isinstance(var.domain, IntegerSet):
272                 niv += 1
273              elif isinstance(var.domain, BooleanSet):
274                 nbv += 1
275
276              if self._output_continuous_variables is True:
277                 for ndx in var._varval.keys():
278                    if not var._varval[ndx].active:
279                        continue
280                    prefix = ""
281                    if self._output_prefixes is True:
282                       prefix = convert_name(var.model.name)+"_"
283
284                    if var[ndx].id != -1: # if the variable isn't referenced in the model, don't output bounds...
285                       # in the CPLEX LP file format, the default variable bounds are 0 and +inf.
286                       # these bounds are in conflict with Pyomo, which assumes -inf and inf (which
287                       # we would argue is more rational).
288                       print >>OUTPUT,"   ",
289                       if var[ndx].lb is not None:
290                          print >>OUTPUT, str(value(var[ndx].lb())) + " <= ",
291                       else:
292                          print >>OUTPUT, " -inf <= ",
293                       name_to_output = prefix+convert_name(var[ndx].label)
294                       if name_to_output == "e":
295                          raise ValueError, "Attempting to write variable with name=e in a CPLEX LP formatted file - will cause a parse failure due to confusion with numeric values expressed in scientific notation"
296                       print >>OUTPUT, name_to_output,
297                       if var[ndx].ub is not None:
298                          print >>OUTPUT, " <= " + str(value(var[ndx].ub())),
299                          print >>OUTPUT, ""
300                       else:
301                          print >>OUTPUT, " <= +inf"
302                       
303           if (niv > 0) and (self._output_integer_variables is True): 
304
305              # if we're outputting the whole model, then assume we can output the "integer"
306              # header. if not, then assume a meta-level process is taking care of it.
307              if (self._output_objectives is True) and (self._output_constraints is True):             
308                 print >>OUTPUT, "integer"
309
310              prefix = ""
311              if self._output_prefixes is True:
312                 prefix = convert_name(var.model.name)+"_"
313
314              for var in VAR.values():
315                 if isinstance(var.domain, IntegerSet):
316                    for ndx in var.keys():
317                       if not var[ndx].active:
318                            continue
319                       if var[ndx].id != -1: # if the variable isn't referenced, skip.
320                          print >>OUTPUT, "   ", prefix+convert_name(var[ndx].label)
321
322           if (nbv > 0) and (self._output_binary_variables is True):
323
324              # if we're outputting the whole model, then assume we can output the "binary"
325              # header. if not, then assume a meta-level process is taking care of it.
326              if (self._output_objectives is True) and (self._output_constraints is True):             
327                 print >>OUTPUT, "binary"
328
329              prefix = ""
330              if self._output_prefixes is True:
331                 prefix = convert_name(var.model.name)+"_"
332
333              for var in VAR.values():
334                 if isinstance(var.domain, BooleanSet):
335                    for ndx in var.keys():
336                       if not var[ndx].active:
337                            continue
338                       if var[ndx].id != -1: # if the variable isn't referenced, skip.
339                          print >>OUTPUT, "   ", prefix+convert_name(var[ndx].label)
340
341        #
342        # wrap-up
343        #
344
345        if (self._output_objectives is True) and (self._output_constraints is True) and (self._output_variables is True):
346           #
347           # End
348           #
349           print >>OUTPUT, "end "
350       
351problem.WriterRegistration(str(ProblemFormat.cpxlp), ProblemWriter_cpxlp)
Note: See TracBrowser for help on using the repository browser.