Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions pyomo/repn/plugins/gams_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@
_plusMinusOne = {-1, 1}


def gams_ftoa(val, parenthesize_negative_values=False):
a = ftoa(val, parenthesize_negative_values)
# Python renders non-finite floats as nan/inf, which are not valid GAMS
# numeric literals: map them to GAMS's special values NA and INF
if a == 'nan':
return 'NA'
if a == '(-inf)':
return '(-INF)'
if a.endswith('inf'):
return a[:-3] + 'INF'
return a


def _handle_PowExpression(visitor, node, values):
# If the exponent is a positive integer, use the power() function.
# Otherwise, use the ** operator.
Expand Down Expand Up @@ -119,14 +132,14 @@ def visiting_potential_leaf(self, node):
"""
if node.__class__ in native_types:
try:
return True, ftoa(node, True)
return True, gams_ftoa(node, True)
except TypeError:
return True, repr(node)

if node.is_expression_type():
# Special handling if NPV and semi-NPV types:
if not node.is_potentially_variable():
return True, ftoa(node(), True)
return True, gams_ftoa(node(), True)
if node.__class__ is EXPR.MonomialTermExpression:
return True, self._monomial_to_string(node)
if node.__class__ is EXPR.LinearExpression:
Expand Down Expand Up @@ -154,7 +167,7 @@ def visiting_potential_leaf(self, node):
if node.is_fixed() and not (
self.output_fixed_variables and node.is_potentially_variable()
):
return True, ftoa(node(), True)
return True, gams_ftoa(node(), True)
else:
assert node.is_variable_type()
return True, self.smap.getSymbol(node)
Expand All @@ -164,7 +177,7 @@ def _monomial_to_string(self, node):
if const.__class__ not in native_types:
const = value(const)
if var.is_fixed() and not self.output_fixed_variables:
return ftoa(const * var.value, True)
return gams_ftoa(const * var.value, True)
# Special handling: ftoa is slow, so bypass _to_string when this
# is a trivial term
if not const:
Expand All @@ -174,21 +187,21 @@ def _monomial_to_string(self, node):
return '-' + self.smap.getSymbol(var)
else:
return self.smap.getSymbol(var)
return ftoa(const, True) + '*' + self.smap.getSymbol(var)
return gams_ftoa(const, True) + '*' + self.smap.getSymbol(var)

def _linear_to_string(self, node):
values = [
(
self._monomial_to_string(arg)
if arg.__class__ is EXPR.MonomialTermExpression
else (
ftoa(arg, True)
gams_ftoa(arg, True)
if arg.__class__ in native_numeric_types
else (
self.smap.getSymbol(arg)
if arg.is_variable_type()
and (not arg.fixed or self.output_fixed_variables)
else ftoa(value(arg), True)
else gams_ftoa(value(arg), True)
)
)
)
Expand Down Expand Up @@ -641,20 +654,20 @@ def _write_model(
constraint_names.append('%s' % cName)
ConstraintIO.write(
'%s.. %s =e= %s ;\n'
% (constraint_names[-1], con_body_str, ftoa(ub, False))
% (constraint_names[-1], con_body_str, gams_ftoa(ub, False))
)
else:
if lb is not None:
constraint_names.append('%s_lo' % cName)
ConstraintIO.write(
'%s.. %s =l= %s ;\n'
% (constraint_names[-1], ftoa(lb, False), con_body_str)
% (constraint_names[-1], gams_ftoa(lb, False), con_body_str)
)
if ub is not None:
constraint_names.append('%s_hi' % cName)
ConstraintIO.write(
'%s.. %s =l= %s ;\n'
% (constraint_names[-1], con_body_str, ftoa(ub, False))
% (constraint_names[-1], con_body_str, gams_ftoa(ub, False))
)

obj = list(model.component_data_objects(Objective, active=True, sort=sort))
Expand Down Expand Up @@ -700,7 +713,8 @@ def _write_model(

for var in categorized_vars.fixed:
output_file.write(
"%s.fx = %s;\n" % (var, ftoa(value(symbolMap.getObject(var)), False))
"%s.fx = %s;\n"
% (var, gams_ftoa(value(symbolMap.getObject(var)), False))
)
output_file.write("\n")

Expand All @@ -718,7 +732,9 @@ def _write_model(
lb, ub = var.bounds
if category == 'positive':
if ub is not None:
output_file.write("%s.up = %s;\n" % (var_name, ftoa(ub, False)))
output_file.write(
"%s.up = %s;\n" % (var_name, gams_ftoa(ub, False))
)
elif category == 'ints':
if lb is None:
warn_int_bounds = True
Expand All @@ -729,7 +745,9 @@ def _write_model(
)
output_file.write("%s.lo = -1.0E+100;\n" % (var_name))
elif lb != 0:
output_file.write("%s.lo = %s;\n" % (var_name, ftoa(lb, False)))
output_file.write(
"%s.lo = %s;\n" % (var_name, gams_ftoa(lb, False))
)
if ub is None:
warn_int_bounds = True
# GAMS has an option value called IntVarUp that is the
Expand All @@ -742,21 +760,33 @@ def _write_model(
)
output_file.write("%s.up = +1.0E+100;\n" % (var_name))
else:
output_file.write("%s.up = %s;\n" % (var_name, ftoa(ub, False)))
output_file.write(
"%s.up = %s;\n" % (var_name, gams_ftoa(ub, False))
)
elif category == 'binary':
if lb != 0:
output_file.write("%s.lo = %s;\n" % (var_name, ftoa(lb, False)))
output_file.write(
"%s.lo = %s;\n" % (var_name, gams_ftoa(lb, False))
)
if ub != 1:
output_file.write("%s.up = %s;\n" % (var_name, ftoa(ub, False)))
output_file.write(
"%s.up = %s;\n" % (var_name, gams_ftoa(ub, False))
)
elif category == 'reals':
if lb is not None:
output_file.write("%s.lo = %s;\n" % (var_name, ftoa(lb, False)))
output_file.write(
"%s.lo = %s;\n" % (var_name, gams_ftoa(lb, False))
)
if ub is not None:
output_file.write("%s.up = %s;\n" % (var_name, ftoa(ub, False)))
output_file.write(
"%s.up = %s;\n" % (var_name, gams_ftoa(ub, False))
)
else:
raise KeyError('Category %s not supported' % category)
if warmstart and var.value is not None:
output_file.write("%s.l = %s;\n" % (var_name, ftoa(var.value, False)))
output_file.write(
"%s.l = %s;\n" % (var_name, gams_ftoa(var.value, False))
)

if warn_int_bounds:
logger.warning(
Expand Down
39 changes: 39 additions & 0 deletions pyomo/repn/tests/gams/test_gams.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,45 @@ def test_fixed_var_to_string(self):
("x1*(-8.8) - x2", False),
)

def test_nonfinite_var_to_string(self):
# Python renders non-finite floats as nan/inf, which are not valid
# GAMS numeric literals: they must map to GAMS's NA / INF
m = ConcreteModel()
m.x = Var()
m.z = Var()
lbl = NumericLabeler('x')
smap = SymbolMap(lbl)
tc = StorageTreeChecker(m)
m.z.fix(float('nan'))
self.assertEqual(
expression_to_string(m.x - m.z, tc, smap=smap), ("x1 + NA", False)
)
m.z.fix(float('inf'))
self.assertEqual(
expression_to_string(m.x - m.z, tc, smap=smap), ("x1 + (-INF)", False)
)
m.z.fix(float('-inf'))
self.assertEqual(
expression_to_string(m.x + m.z, tc, smap=smap), ("x1 + (-INF)", False)
)

def test_nonfinite_var_values_write_gams(self):
m = ConcreteModel()
m.x = Var(initialize=float('nan'))
m.y = Var(initialize=float('inf'))
m.z = Var(initialize=2.5)
m.z.fix(float('-inf'))
m.obj = Objective(expr=m.x + m.y + m.z)
m.c = Constraint(expr=m.x + m.y >= 1)
outs = StringIO()
m.write(outs, format='gams', io_options=dict(warmstart=True))
out = outs.getvalue()
self.assertIn("x1.l = NA;", out)
self.assertIn("x2.l = INF;", out)
self.assertIn("+ (-INF)", out)
self.assertNotIn("nan", out)
self.assertNotIn("inf", out)

def test_dnlp_to_string(self):
m = ConcreteModel()
m.x = Var()
Expand Down
Loading