Repository: Pyomo/pyomo

Issue:
GAMS writer: invalid syntax caused by -0.0
@gseastream I have a model in which constraints like `x - -0.0 == 0.0` started to appear. This is invalid GAMS syntax, and so the model failed to solve. I don't have the Pyomo model handy right now, but can send it to you later.


Discussion:
I don't think we can resolve this until we have a concrete example.  Also, I don't think we should try to resolve this until we merge the new expression system, since that has a different writer for GAMS.
@qtothec: to reiterate @whart222 (and your own comment in #340), can you point us to an example?
This turns out to be quite easy:
```
from pyomo.environ import *
m = ConcreteModel()
m.x = Var()
m.y = Var()
m.z = Var()

m.c = Constraint(expr=m.x + m.y == m.z)
m.z.fix(-0.0)
m.o = Objective(expr=m.x)

m.write('/tmp/test.gms')
with open('/tmp/test.gms', 'r') as fin:
    print fin.read()
```
Output includes:
```
c1.. x1 + x2 - -0.0 =e= 0.0 ;
```
I have confirmed that this error persists even after resolving PR #272 
From my brief understanding of the new expression tree walker used by the writer, I have come up with a simple solution that may or may not cover every situation but seems to work. Essentially, if a variable is fixed, wrap it in parentheses, within the `visit` method of the `ToGamsVisitor`:

```python
elif arg.is_variable_type():
    if arg.is_fixed():
        tmp.append("(%s)" % val)
    else:
        tmp.append(val)
```

The problem with the double sign comes up whenever a variable in a sum expression with a negative coefficient is fixed to a negative number. Going off of the example @qtothec gave above, you could even have:

```python
m.c = Constraint(expr=m.x - m.z == 0)
m.z.fix(-1)
```

And you'd get a double negative. This seems to happen just because `z` is replaced with its fixed value independent of what it's coefficient is. Also, we could do more and only add parentheses when necessary if that would be preferable, but doing it always doesn't hurt anything (I believe), and it might be nice to see every variable that was replaced with a fixed value in the model file if someone was inspecting it.

If you all like this solution, let me know and I can create a PR for it.
I would be perfectly happy with this fix if it works. Could you add in the example above as a new test as well?

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
@@ -59,7 +59,10 @@ def visit(self, node, values):
             elif arg.__class__ in native_types:
                 tmp.append("'{0}'".format(val))
             elif arg.is_variable_type():
-                tmp.append(val)
+                if arg.is_fixed():
+                    tmp.append("(%s)" % val)
+                else:
+                    tmp.append(val)
             elif arg.is_expression_type() and node._precedence() < arg._precedence():
                 tmp.append("({0})".format(val))
             else:


Test patch:
diff --git a/pyomo/repn/tests/gams/small10.pyomo.gms b/pyomo/repn/tests/gams/small10.pyomo.gms
--- a/pyomo/repn/tests/gams/small10.pyomo.gms
+++ b/pyomo/repn/tests/gams/small10.pyomo.gms
@@ -21,21 +21,21 @@ VARIABLES
 	GAMS_OBJECTIVE
 	x1;
 
-c1.. x1 =e= 0.0 ;
-c2.. 0.0*x1 + x1 =e= 0.0 ;
+c1.. (1.0)*x1 =e= 0.0 ;
+c2.. (0.0)*x1 + x1 =e= 0.0 ;
 c3.. x1 =e= 0.0 ;
 c4.. x1 =e= 0.0 ;
 c5.. 0.0*power(x1, 2) + x1 =e= 0.0 ;
-c6.. 0.0*x1*1.0 + x1 =e= 0.0 ;
-c7.. 0.0*power(x1, 2) + x1 =e= 0.0 ;
-c8.. 0.0*x1*1.0 + x1 =e= 0.0 ;
-c9.. 0.0*x1 =e= 0.0 ;
+c6.. 0.0*x1*(1.0) + x1 =e= 0.0 ;
+c7.. (0.0)*power(x1, 2) + x1 =e= 0.0 ;
+c8.. (0.0)*x1*(1.0) + x1 =e= 0.0 ;
+c9.. (0.0)*x1 =e= 0.0 ;
 c10.. 0.0*power(x1, 2) =e= 0.0 ;
-c11.. 0.0*x1*1.0 =e= 0.0 ;
-c12.. 0.0*power(x1, 2) =e= 0.0 ;
-c13.. 0.0*x1*1.0 =e= 0.0 ;
+c11.. 0.0*x1*(1.0) =e= 0.0 ;
+c12.. (0.0)*power(x1, 2) =e= 0.0 ;
+c13.. (0.0)*x1*(1.0) =e= 0.0 ;
 c14.. 0.0*x1 =e= 0.0 ;
-c15.. GAMS_OBJECTIVE =e= x1 + 0.0*x1 + 0.0*x1 + x1*x1*0.0 + x1*x1*0.0 + 0.0*power(x1, 2) ;
+c15.. GAMS_OBJECTIVE =e= (1.0)*x1 + (0.0)*x1 + 0.0*x1 + x1*x1*0.0 + x1*x1*(0.0) + (0.0)*power(x1, 2) ;
 
 
 MODEL GAMS_MODEL /all/ ;
diff --git a/pyomo/repn/tests/gams/small6.pyomo.gms b/pyomo/repn/tests/gams/small6.pyomo.gms
--- a/pyomo/repn/tests/gams/small6.pyomo.gms
+++ b/pyomo/repn/tests/gams/small6.pyomo.gms
@@ -15,12 +15,12 @@ VARIABLES
 	x2
 	x3;
 
-c1.. (1/2.0)*x1*(x2 - x3) =e= 2.0 ;
-c2.. x1*(1/2.0)*(x2 - x3) =e= 2.0 ;
-c3.. x1*(x2 - x3)*(1/2.0) =e= 2.0 ;
-c4.. x1*(x2*(1/2.0) - x3*(1/2.0)) =e= 2.0 ;
-c5.. x1*(x2 - x3)*(1/2.0) =e= 2.0 ;
-c6.. x1*(x2 - x3) - 2.0*2.0 =e= 0.0 ;
+c1.. (1/(2.0))*x1*(x2 - x3) =e= 2.0 ;
+c2.. x1*(1/(2.0))*(x2 - x3) =e= 2.0 ;
+c3.. x1*(x2 - x3)*(1/(2.0)) =e= 2.0 ;
+c4.. x1*(x2*(1/(2.0)) - x3*(1/(2.0))) =e= 2.0 ;
+c5.. x1*(x2 - x3)*(1/(2.0)) =e= 2.0 ;
+c6.. x1*(x2 - x3) - 2.0*(2.0) =e= 0.0 ;
 c7.. GAMS_OBJECTIVE =e= x2 ;
 
 x1.lo = -1.0;
diff --git a/pyomo/repn/tests/gams/small7.pyomo.gms b/pyomo/repn/tests/gams/small7.pyomo.gms
--- a/pyomo/repn/tests/gams/small7.pyomo.gms
+++ b/pyomo/repn/tests/gams/small7.pyomo.gms
@@ -33,30 +33,30 @@ VARIABLES
 	x2
 	x3;
 
-c1.. (1/2.0)*0.5*x1*(x2 - x3) =e= 2.0 ;
-c2.. x1*(1/2.0)*(1/2.0)*(x2 - x3) =e= 2.0 ;
-c3.. x1*(x2 - x3)*(1/2.0)*0.5 =e= 2.0 ;
-c4.. x1*(x2*(1/2.0)*0.5 - x3*(1/2.0)*0.5) =e= 2.0 ;
-c5.. x1*(x2 - x3)*(1/2.0)*0.5 =e= 2.0 ;
-c6.. x1*(x2 - x3) - 4.0*2.0 =e= 0.0 ;
-c7.. (1/(2.0*2.0))*x1*(x2 - x3) =e= 2.0 ;
-c8.. x1*(1/(2.0*2.0))*(x2 - x3) =e= 2.0 ;
-c9.. x1*(x2 - x3)*(1/(2.0*2.0)) =e= 2.0 ;
-c10.. x1*(x2*(1/(2.0*2.0)) - x3*(1/(2.0*2.0))) =e= 2.0 ;
-c11.. x1*(x2 - x3)*(1/(2.0*2.0)) =e= 2.0 ;
-c12.. x1*(x2 - x3) - 4.0*2.0 =e= 0.0 ;
-c13.. (1/(2.0 + 2.0))*x1*(x2 - x3) =e= 2.0 ;
-c14.. x1*(1/(2.0 + 2.0))*(x2 - x3) =e= 2.0 ;
-c15.. x1*(x2 - x3)*(1/(2.0 + 2.0)) =e= 2.0 ;
-c16.. x1*(x2*(1/(2.0 + 2.0)) - x3*(1/(2.0 + 2.0))) =e= 2.0 ;
-c17.. x1*(x2 - x3)*(1/(2.0 + 2.0)) =e= 2.0 ;
-c18.. x1*(x2 - x3) - 2.0*(2.0 + 2.0) =e= 0.0 ;
-c19.. (1/power((2.0 + 2.0), 2))*x1*(x2 - x3) =e= 2.0 ;
-c20.. x1*(1/power((2.0 + 2.0), 2))*(x2 - x3) =e= 2.0 ;
-c21.. x1*(x2 - x3)*(1/power((2.0 + 2.0), 2)) =e= 2.0 ;
-c22.. x1*(x2*(1/power((2.0 + 2.0), 2)) - x3*(1/power((2.0 + 2.0), 2))) =e= 2.0 ;
-c23.. x1*(x2 - x3)*(1/power((2.0 + 2.0), 2)) =e= 2.0 ;
-c24.. x1*(x2 - x3) - 2.0*power((2.0 + 2.0), 2) =e= 0.0 ;
+c1.. (1/(2.0))*0.5*x1*(x2 - x3) =e= 2.0 ;
+c2.. x1*(1/(2.0))*(1/(2.0))*(x2 - x3) =e= 2.0 ;
+c3.. x1*(x2 - x3)*(1/(2.0))*0.5 =e= 2.0 ;
+c4.. x1*(x2*(1/(2.0))*0.5 - x3*(1/(2.0))*0.5) =e= 2.0 ;
+c5.. x1*(x2 - x3)*(1/(2.0))*0.5 =e= 2.0 ;
+c6.. x1*(x2 - x3) - 4.0*(2.0) =e= 0.0 ;
+c7.. (1/(2.0*(2.0)))*x1*(x2 - x3) =e= 2.0 ;
+c8.. x1*(1/((2.0)*(2.0)))*(x2 - x3) =e= 2.0 ;
+c9.. x1*(x2 - x3)*(1/(2.0*(2.0))) =e= 2.0 ;
+c10.. x1*(x2*(1/(2.0*(2.0))) - x3*(1/(2.0*(2.0)))) =e= 2.0 ;
+c11.. x1*(x2 - x3)*(1/(2.0*(2.0))) =e= 2.0 ;
+c12.. x1*(x2 - x3) - 4.0*(2.0) =e= 0.0 ;
+c13.. (1/((2.0) + 2.0))*x1*(x2 - x3) =e= 2.0 ;
+c14.. x1*(1/((2.0) + (2.0)))*(x2 - x3) =e= 2.0 ;
+c15.. x1*(x2 - x3)*(1/((2.0) + 2.0)) =e= 2.0 ;
+c16.. x1*(x2*(1/((2.0) + 2.0)) - x3*(1/((2.0) + 2.0))) =e= 2.0 ;
+c17.. x1*(x2 - x3)*(1/((2.0) + 2.0)) =e= 2.0 ;
+c18.. x1*(x2 - x3) - 2.0*((2.0) + 2.0) =e= 0.0 ;
+c19.. (1/power(((2.0) + 2.0), 2))*x1*(x2 - x3) =e= 2.0 ;
+c20.. x1*(1/power(((2.0) + (2.0)), 2))*(x2 - x3) =e= 2.0 ;
+c21.. x1*(x2 - x3)*(1/power(((2.0) + 2.0), 2)) =e= 2.0 ;
+c22.. x1*(x2*(1/power(((2.0) + 2.0), 2)) - x3*(1/power(((2.0) + 2.0), 2))) =e= 2.0 ;
+c23.. x1*(x2 - x3)*(1/power(((2.0) + 2.0), 2)) =e= 2.0 ;
+c24.. x1*(x2 - x3) - 2.0*power(((2.0) + 2.0), 2) =e= 0.0 ;
 c25.. GAMS_OBJECTIVE =e= x2 ;
 
 x1.lo = -1.0;
diff --git a/pyomo/repn/tests/gams/small9.pyomo.gms b/pyomo/repn/tests/gams/small9.pyomo.gms
--- a/pyomo/repn/tests/gams/small9.pyomo.gms
+++ b/pyomo/repn/tests/gams/small9.pyomo.gms
@@ -13,10 +13,10 @@ VARIABLES
 	x1
 	x2;
 
-c1.. x1*0.0*x2 + x1 =e= 1.0 ;
+c1.. x1*(0.0)*x2 + x1 =e= 1.0 ;
 c2.. 0.0*x1*x2 + x1 =e= 1.0 ;
 c3.. x1 =e= 1.0 ;
-c4.. x1*0.0*x2 =e= 1.0 ;
+c4.. x1*(0.0)*x2 =e= 1.0 ;
 c5.. 0.0*x1*x2 =e= 1.0 ;
 c6.. GAMS_OBJECTIVE =e= x1 ;
 
diff --git a/pyomo/repn/tests/test_gams.py b/pyomo/repn/tests/test_gams.py
--- a/pyomo/repn/tests/test_gams.py
+++ b/pyomo/repn/tests/test_gams.py
@@ -57,6 +57,26 @@ def test_power_function_to_string(self):
         self.assertEquals(expression_to_string(
             pow(m.x, 2), smap=smap), "power(x1, 2)")
 
+    def test_fixed_var_to_string(self):
+        m = ConcreteModel()
+        m.x = Var()
+        m.y = Var()
+        m.z = Var()
+        m.z.fix(-3)
+        lbl = NumericLabeler('x')
+        smap = SymbolMap(lbl)
+        self.assertEquals(expression_to_string(
+            m.x + m.y - m.z, lbl, smap=smap), "x1 + x2 - (-3)")
+        m.z.fix(-400)
+        self.assertEquals(expression_to_string(
+            m.z + m.y - m.z, smap=smap), "(-400) + x2 - (-400)")
+        m.z.fix(8.8)
+        self.assertEquals(expression_to_string(
+            m.x + m.z - m.y, smap=smap), "x1 + (8.8) - x2")
+        m.z.fix(-8.8)
+        self.assertEquals(expression_to_string(
+            m.x * m.z - m.y, smap=smap), "x1*(-8.8) - x2")
+
     def test_gams_connector_in_active_constraint(self):
         """Test connector in active constraint for GAMS writer."""
         m = ConcreteModel()
diff --git a/pyomo/solvers/tests/checks/test_GAMS.py b/pyomo/solvers/tests/checks/test_GAMS.py
--- a/pyomo/solvers/tests/checks/test_GAMS.py
+++ b/pyomo/solvers/tests/checks/test_GAMS.py
@@ -165,6 +165,50 @@ def test_keepfiles_gms(self):
 
             shutil.rmtree(tmpdir)
 
+    @unittest.skipIf(not gamspy_available,
+                     "The 'gams' python bindings are not available")
+    def test_fixed_var_sign_py(self):
+        with SolverFactory("gams", solver_io="python") as opt:
+
+            m = ConcreteModel()
+            m.x = Var()
+            m.y = Var()
+            m.z = Var()
+            m.z.fix(-3)
+            m.c1 = Constraint(expr= m.x + m.y - m.z == 0)
+            m.c2 = Constraint(expr= m.z + m.y - m.z >= -10000)
+            m.c3 = Constraint(expr= -3 * m.z + m.y - m.z >= -10000)
+            m.c4 = Constraint(expr= -m.z + m.y - m.z >= -10000)
+            m.c5 = Constraint(expr= m.x <= 100)
+            m.o = Objective(expr= m.x, sense=maximize)
+
+            results = opt.solve(m)
+
+            self.assertEqual(results.solver.termination_condition,
+                             TerminationCondition.optimal)
+
+    @unittest.skipIf(not gamsgms_available,
+                     "The 'gams' executable is not available")
+    def test_fixed_var_sign_gms(self):
+        with SolverFactory("gams", solver_io="gms") as opt:
+
+            m = ConcreteModel()
+            m.x = Var()
+            m.y = Var()
+            m.z = Var()
+            m.z.fix(-3)
+            m.c1 = Constraint(expr= m.x + m.y - m.z == 0)
+            m.c2 = Constraint(expr= m.z + m.y - m.z >= -10000)
+            m.c3 = Constraint(expr= -3 * m.z + m.y - m.z >= -10000)
+            m.c4 = Constraint(expr= -m.z + m.y - m.z >= -10000)
+            m.c5 = Constraint(expr= m.x <= 100)
+            m.o = Objective(expr= m.x, sense=maximize)
+
+            results = opt.solve(m)
+
+            self.assertEqual(results.solver.termination_condition,
+                             TerminationCondition.optimal)
+
 class GAMSLogfileTestBase(unittest.TestCase):
     def setUp(self):
         """Set up model and temporary directory."""