Repository: Pyomo/pyomo

Issue:
`quicksum()` causes trivial constraints to remain in the model
I have a constraint with a `body` of the following form:

```
var_1[x] - (var_2[x] + (1/6*var_3[x,y] + 1/6*var_3[x,y]))
```

When all of the variables are fixed, I would like the constraint to be deactivated.

This functionality already appears to be implemented with `TrivialConstraintDeactivator` and/or `skip_trivial_constraints=True` which both check `body.polynomial_degree() == 0` as the test for whether the constraint should be skipped.

As it happens, I'm finding that when all the variables are fixed I get the following:

```python
from pyomo.core.expr.visitor import polynomial_degree, _expression_is_fixed
assert polynomial_degree(body) == 1
assert _expression_is_fixed(body) is True
```

That is, the degree is non-zero yet the constraint is also wholly fixed. I've narrowed this down to the final two terms forming a `LinearExpression` for which the above assertions still hold.

Most importantly, if I rewrite the building of the constraint so that the final two terms fall under a `SumExpression` instead, `polynomial_degree(body) == 0` as expected. I've done this by swapping out `quicksum()` for `builtins.sum()`.

Is this a bug or is this expected behaviour? If it's the latter, performance is an issue for this constraint so what would be the suggested approach?


Discussion:
Reproducible example:

```python
from pyomo.environ import ConcreteModel, Constraint, Var, RangeSet, sum_product, polynomial_degree

m = ConcreteModel()
m.S = RangeSet(2)
m.var_1 = Var(initialize=0)
m.var_2 = Var(initialize=0)
m.var_3 = Var(m.S, initialize=0)

def con_rule(model):
    return model.var_1 - (model.var_2 + sum_product(defaultdict(lambda: 1/6), model.var_3)) <= 0

m.c1 = Constraint(rule=con_rule)

def con_rule(model):
    return model.var_1 - (model.var_2 + sum(1 / 6 * model.var_3[i] for i in model.S)) <= 0

m.c2 = Constraint(rule=con_rule)

m.var_1.fix(1)
m.var_2.fix(1)
m.var_3.fix(1)

_expression_is_fixed(m.c1.body)
# True
polynomial_degree(m.c1.body)
# 1

_expression_is_fixed(m.c2.body)
# True
polynomial_degree(m.c2.body)
# 0

m.c1.body.to_string()
# 'var_1 - (var_2 + (0.16666666666666666*var_3[1] + 0.16666666666666666*var_3[2]))'
m.c2.body.to_string()
# 'var_1 - (0.16666666666666666*var_3[1] + 0.16666666666666666*var_3[2] + var_2)'
```
Can confirm this observed behavior.

This is also the root of the issue here: https://github.com/Pyomo/pyomo/issues/924
@qtothec, would this be fixed with:

```python
class LinearExpression(ExpressionBase):
    ...
    def _compute_polynomial_degree(self, result):
        return 1 if len(self.linear_vars) > 0 else 0  # current
    ...
    def _compute_polynomial_degree(self, result):
        return 1 if not self.is_fixed() else 0  # suggested
```

If so, I'd be happy to raise a PR with the above test case. This change fixes my example, I'm just wondering if there are other implications.
@ruaridhw Yes, your suggestion should fix the problem. A PR would be welcome.

Resolving patch:
diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py
--- a/pyomo/core/expr/numeric_expr.py
+++ b/pyomo/core/expr/numeric_expr.py
@@ -1366,18 +1366,16 @@ def getname(self, *args, **kwds):
         return 'sum'
 
     def _compute_polynomial_degree(self, result):
-        return 1 if len(self.linear_vars) > 0 else 0
+        return 1 if not self.is_fixed() else 0
 
     def is_constant(self):
         return len(self.linear_vars) == 0
 
+    def _is_fixed(self, values=None):
+        return all(v.fixed for v in self.linear_vars)
+
     def is_fixed(self):
-        if len(self.linear_vars) == 0:
-            return True
-        for v in self.linear_vars:
-            if not v.fixed:
-                return False
-        return True
+        return self._is_fixed()
 
     def _to_string(self, values, verbose, smap, compute_values):
         tmp = []


Test patch:
diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py
--- a/pyomo/core/tests/unit/test_numeric_expr.py
+++ b/pyomo/core/tests/unit/test_numeric_expr.py
@@ -16,6 +16,8 @@
 import math
 import os
 import re
+from collections import defaultdict
+
 import six
 import sys
 from os.path import abspath, dirname
@@ -5212,5 +5214,43 @@ def test_LinearExpression_expression(self):
         self.assertTrue(len(repn.linear_coefs) == N)
         self.assertTrue(len(repn.linear_vars) == N)
 
+    def test_LinearExpression_polynomial_degree(self):
+        m = ConcreteModel()
+        m.S = RangeSet(2)
+        m.var_1 = Var(initialize=0)
+        m.var_2 = Var(initialize=0)
+        m.var_3 = Var(m.S, initialize=0)
+
+        def con_rule(model):
+            return model.var_1 - (model.var_2 + sum_product(defaultdict(lambda: 6), model.var_3)) <= 0
+
+        m.c1 = Constraint(rule=con_rule)
+
+        m.var_1.fix(1)
+        m.var_2.fix(1)
+        m.var_3.fix(1)
+
+        self.assertTrue(is_fixed(m.c1.body))
+        self.assertEqual(polynomial_degree(m.c1.body), 0)
+
+    def test_LinearExpression_is_fixed(self):
+        m = ConcreteModel()
+        m.S = RangeSet(2)
+        m.var_1 = Var(initialize=0)
+        m.var_2 = Var(initialize=0)
+        m.var_3 = Var(m.S, initialize=0)
+
+        def con_rule(model):
+            return model.var_1 - (model.var_2 + sum_product(defaultdict(lambda: 6), model.var_3)) <= 0
+
+        m.c1 = Constraint(rule=con_rule)
+
+        m.var_1.fix(1)
+        m.var_2.fix(1)
+
+        self.assertFalse(is_fixed(m.c1.body))
+        self.assertEqual(polynomial_degree(m.c1.body), 1)
+
+
 if __name__ == "__main__":
     unittest.main()