Repository: Pyomo/pyomo

Issue:
GAMS writer fails for power nested in a log
The functionality to selectively replace `**` with `power()` fails in an edge case when `**` is nested within a `log` function, e.g. `log(x ** 2)`. In `replace_power()`, the `assert` that `arg` is bound by `(` and `)` fails because the arg is bound by `log(` and `)`. This is possibly also true with other similar functions, e.g. `exp()`.

@gseastream 


Discussion:

Resolving patch:
diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py
--- a/pyomo/repn/plugins/gams_writer.py
+++ b/pyomo/repn/plugins/gams_writer.py
@@ -344,8 +344,8 @@ def _write_model(self,
             if '**' in line:
                 # Investigate power functions for an integer exponent, in which
                 # case replace with power(x, int) function to improve domain
-                # issues. Skip first term since it's always "con_name.."
-                line = replace_power(line) + ';'
+                # issues.
+                line = replace_power(line)
             if len(line) > 80000:
                 line = split_long_line(line)
             output_file.write(line + "\n")
@@ -538,8 +538,7 @@ def __iter__(self):
 def split_terms(line):
     """
     Take line from GAMS model file and return list of terms split by space
-    but grouping together parentheses-bound expressions. Assumes lines end
-    with space followed by semicolon.
+    but grouping together parentheses-bound expressions.
     """
     terms = []
     begin = 0
@@ -564,10 +563,9 @@ def split_terms(line):
                     terms.append(line[begin:i])
                 terms.append(line[i])
                 begin = i + 1
-    if begin < len(line) - 1:
+    assert inparens == 0, "Missing close parenthesis in line '%s'" % line
+    if begin < len(line):
         terms.append(line[begin:len(line)])
-    if terms[-1][-1] == ';':
-        terms[-1] = terms[-1][:-1]
     return terms
 
 
@@ -589,6 +587,7 @@ def split_args(term):
             assert i > begin, "Invalid syntax around '**' operator"
             args.append(term[begin:i])
             begin = i + 2
+    assert inparens == 0, "Missing close parenthesis in term '%s'" % term
     args.append(term[begin:len(term)])
     return args
 
@@ -600,10 +599,13 @@ def replace_power(line):
             args = split_args(term)
             for i in xrange(len(args)):
                 if '**' in args[i]:
-                    assert args[i][0] == '(' and args[i][-1] == ')',\
-                        "Assume arg is a parenthesis-bound expression"
-                    arg = args[i][1:-1]
-                    args[i] = '( %s)' % replace_power(arg)
+                    first_paren = args[i].find('(')
+                    assert ((first_paren != -1) and (args[i][-1] == ')')), (
+                        "Assumed arg '%s' was a parenthesis-bound expression "
+                        "or function" % args[i])
+                    arg = args[i][first_paren + 1:-1]
+                    args[i] = '%s( %s )' % (args[i][:first_paren],
+                                            replace_power(arg))
             try:
                 if float(args[-1]) == int(float(args[-1])):
                     term = ''
@@ -616,7 +618,8 @@ def replace_power(line):
                     term += arg + '**'
                 term += args[-1]
         new_line += term + ' '
-    return new_line
+    # Remove trailing space
+    return new_line[:-1]
 
 
 def split_long_line(line):


Test patch:
diff --git a/pyomo/repn/tests/test_gams.py b/pyomo/repn/tests/test_gams.py
new file mode 100644
--- /dev/null
+++ b/pyomo/repn/tests/test_gams.py
@@ -0,0 +1,48 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and 
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+
+#
+# Test replace_power function in gams writer
+#
+
+
+import pyutilib.th as unittest
+from pyomo.repn.plugins.gams_writer import replace_power
+
+
+class GAMSTests(unittest.TestCase):
+
+    def test_gams_replace_power(self):
+        line1 = "x**2.01"
+        self.assertTrue(replace_power(line1) == line1)
+
+        line2 = "abc**2.0"
+        self.assertTrue(replace_power(line2) == "power(abc, 2.0)")
+
+        line3 = "log( abc**2.0 )"
+        self.assertTrue(replace_power(line3) == "log( power(abc, 2.0) )")
+
+        line4 = "log( abc**2.0 ) + 5"
+        self.assertTrue(replace_power(line4) == "log( power(abc, 2.0) ) + 5")
+
+        line5 = "exp( abc**2.0 ) + 5"
+        self.assertTrue(replace_power(line5) == "exp( power(abc, 2.0) ) + 5")
+
+        line6 = "log( abc**2.0 )**4"
+        self.assertTrue(replace_power(line6) ==
+                        "power(log( power(abc, 2.0) ), 4)")
+
+        line6 = "log( abc**2.0 )**4.5"
+        self.assertTrue(replace_power(line6) == line6)
+
+
+
+if __name__ == "__main__":
+    unittest.main()