Repository: Pyomo/pyomo

Issue:
GAMS writer fails to identify fixed variables in LinearExpression
The following two tests both fail because the GAMS writer is not able to properly handle the fixed variables in `LinearExpression`.

```
    def test_quicksum(self):
        m = ConcreteModel()
        m.y = Var(domain=Binary)
        m.c = Constraint(expr=quicksum([m.y, m.y], linear=True) == 1)
        m.y.fix(1)
        lbl = NumericLabeler('x')
        smap = SymbolMap(lbl)
        tc = StorageTreeChecker(m)
        self.assertEqual(expression_to_string(m.c.body, tc, smap=smap), "2")
```
expected: `1 + 1`; output: `x1 + x1`

```
    def test_quicksum_integer_var_fixed(self):
        m = ConcreteModel()
        m.x = Var()
        m.y = Var(domain=Binary)
        m.c = Constraint(expr=quicksum([m.y, m.y], linear=True) == 1)
        m.o = Objective(expr=m.x ** 2)
        m.y.fix(1)
        os = StringIO()
        m.write(os, format='gams')
        self.assertIn("USING nlp", os.getvalue())
```

I suspect that this is related in some way to #919, but I am still working to understand the GAMS visitor enough again.

@gseastream Any quick thoughts?


Discussion:
I don't particularly understand what's going on in the expression system, but I took a look at the PR's and nothing sticks out to me.

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
@@ -180,11 +180,14 @@ def __init__(self, var_list, symbol_map):
         self.ints = []
         self.positive = []
         self.reals = []
+        self.fixed = []
 
         # categorize variables
         for var in var_list:
             v = symbol_map.getObject(var)
-            if v.is_binary():
+            if v.is_fixed():
+                self.fixed.append(var)
+            elif v.is_binary():
                 self.binary.append(var)
             elif v.is_integer():
                 if (v.has_lb() and (value(v.lb) >= 0)) and \
@@ -621,9 +624,17 @@ def _write_model(self,
             output_file.write(";\n\nPOSITIVE VARIABLES\n\t")
             output_file.write("\n\t".join(categorized_vars.positive))
         output_file.write(";\n\nVARIABLES\n\tGAMS_OBJECTIVE\n\t")
-        output_file.write("\n\t".join(categorized_vars.reals))
+        output_file.write("\n\t".join(
+            categorized_vars.reals + categorized_vars.fixed
+        ))
         output_file.write(";\n\n")
 
+        for var in categorized_vars.fixed:
+            output_file.write("%s.fx = %s;\n" % (
+                var, ftoa(value(symbolMap.getObject(var)))
+            ))
+        output_file.write("\n")
+
         for line in ConstraintIO.getvalue().splitlines():
             if len(line) > 80000:
                 line = split_long_line(line)


Test patch:
diff --git a/pyomo/repn/tests/gams/fixed_linear_expr.gams.baseline b/pyomo/repn/tests/gams/fixed_linear_expr.gams.baseline
new file mode 100644
--- /dev/null
+++ b/pyomo/repn/tests/gams/fixed_linear_expr.gams.baseline
@@ -0,0 +1,52 @@
+$offlisting
+$offdigit
+
+EQUATIONS
+	c1_lo
+	c2
+	obj;
+
+POSITIVE VARIABLES
+	x;
+
+VARIABLES
+	GAMS_OBJECTIVE
+	y;
+
+y.fx = 0;
+
+c1_lo.. 0 =l= y + y ;
+c2.. x + y =e= 1 ;
+obj.. GAMS_OBJECTIVE =e= x ;
+
+
+MODEL GAMS_MODEL /all/ ;
+option solprint=off;
+option limrow=0;
+option limcol=0;
+option solvelink=5;
+SOLVE GAMS_MODEL USING lp minimizing GAMS_OBJECTIVE;
+
+Scalars MODELSTAT 'model status', SOLVESTAT 'solve status';
+MODELSTAT = GAMS_MODEL.modelstat;
+SOLVESTAT = GAMS_MODEL.solvestat;
+
+Scalar OBJEST 'best objective', OBJVAL 'objective value';
+OBJEST = GAMS_MODEL.objest;
+OBJVAL = GAMS_MODEL.objval;
+
+Scalar NUMVAR 'number of variables';
+NUMVAR = GAMS_MODEL.numvar
+
+Scalar NUMEQU 'number of equations';
+NUMEQU = GAMS_MODEL.numequ
+
+Scalar NUMDVAR 'number of discrete variables';
+NUMDVAR = GAMS_MODEL.numdvar
+
+Scalar NUMNZ 'number of nonzeros';
+NUMNZ = GAMS_MODEL.numnz
+
+Scalar ETSOLVE 'time to execute solve statement';
+ETSOLVE = GAMS_MODEL.etsolve
+
diff --git a/pyomo/repn/tests/gams/test_gams.py b/pyomo/repn/tests/gams/test_gams.py
--- a/pyomo/repn/tests/gams/test_gams.py
+++ b/pyomo/repn/tests/gams/test_gams.py
@@ -19,7 +19,9 @@
 from pyomo.core.base import NumericLabeler, SymbolMap
 from pyomo.environ import (Block, ConcreteModel, Connector, Constraint,
                            Objective, TransformationFactory, Var, exp, log,
-                           ceil, floor, asin, acos, atan, asinh, acosh, atanh)
+                           ceil, floor, asin, acos, atan, asinh, acosh, atanh,
+                           Binary, quicksum)
+from pyomo.gdp import Disjunction
 from pyomo.repn.plugins.gams_writer import (StorageTreeChecker,
                                             expression_to_string,
                                             split_long_line)
@@ -117,6 +119,72 @@ def test_var_on_deactivated_block(self):
         model.obj = Objective(expr=model.x)
         self._check_baseline(model)
 
+    def test_fixed_linear_expr(self):
+        # Note that this checks both that a fixed variable is fixed, and
+        # that the resulting model type is correctly classified (in this
+        # case, fixing a binary makes this an LP)
+        m = ConcreteModel()
+        m.y = Var(within=Binary)
+        m.y.fix(0)
+        m.x = Var(bounds=(0,None))
+        m.c1 = Constraint(expr=quicksum([m.y, m.y], linear=True) >= 0)
+        m.c2 = Constraint(expr=quicksum([m.x, m.y], linear=True) == 1)
+        m.obj = Objective(expr=m.x)
+        self._check_baseline(m)
+
+    def test_nested_GDP_with_deactivate(self):
+        m = ConcreteModel()
+        m.x = Var(bounds=(0, 1))
+
+        @m.Disjunct([0, 1])
+        def disj(disj, _):
+            @disj.Disjunct(['A', 'B'])
+            def nested(n_disj, _):
+                pass  # Blank nested disjunct
+
+            return disj
+
+        m.choice = Disjunction(expr=[m.disj[0], m.disj[1]])
+
+        m.c = Constraint(expr=m.x ** 2 + m.disj[1].nested['A'].indicator_var >= 1)
+
+        m.disj[0].indicator_var.fix(1)
+        m.disj[1].deactivate()
+        m.disj[0].nested['A'].indicator_var.fix(1)
+        m.disj[0].nested['B'].deactivate()
+        m.disj[1].nested['A'].indicator_var.set_value(1)
+        m.disj[1].nested['B'].deactivate()
+        m.o = Objective(expr=m.x)
+        TransformationFactory('gdp.fix_disjuncts').apply_to(m)
+
+        os = StringIO()
+        m.write(os, format='gams', io_options=dict(solver='dicopt'))
+        self.assertIn("USING minlp", os.getvalue())
+
+    def test_quicksum(self):
+        m = ConcreteModel()
+        m.y = Var(domain=Binary)
+        m.c = Constraint(expr=quicksum([m.y, m.y], linear=True) == 1)
+        m.y.fix(1)
+        lbl = NumericLabeler('x')
+        smap = SymbolMap(lbl)
+        tc = StorageTreeChecker(m)
+        self.assertEqual(("x1 + x1", False), expression_to_string(m.c.body, tc, smap=smap))
+        m.x = Var()
+        m.c2 = Constraint(expr=quicksum([m.x, m.y], linear=True) == 1)
+        self.assertEqual(("x2 + x1", False), expression_to_string(m.c2.body, tc, smap=smap))
+
+    def test_quicksum_integer_var_fixed(self):
+        m = ConcreteModel()
+        m.x = Var()
+        m.y = Var(domain=Binary)
+        m.c = Constraint(expr=quicksum([m.y, m.y], linear=True) == 1)
+        m.o = Objective(expr=m.x ** 2)
+        m.y.fix(1)
+        os = StringIO()
+        m.write(os, format='gams')
+        self.assertIn("USING nlp", os.getvalue())
+
     def test_expr_xfrm(self):
         from pyomo.repn.plugins.gams_writer import (
             expression_to_string, StorageTreeChecker)