Repository: Pyomo/pyomo

Issue:
identify_mutable_parameters sometimes identifies numeric constants
There seem to be some circumstances in which `identify_mutable_parameters` identifies numeric constants. As far as I can tell, this happens when the numeric constant is alone as one argument of a logical expression. The following code reproduces the issue:
```python
import pyomo.environ as pyo
from pyomo.core.expr.visitor import identify_mutable_parameters

m = pyo.ConcreteModel()
m.p = pyo.Param(mutable=True)
expr = m.p - 5 == 0

for p in identify_mutable_parameters(expr):
    print(p.to_string())
```
This prints:
```
0.0
p
```



Discussion:

Resolving patch:
diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py
--- a/pyomo/core/expr/visitor.py
+++ b/pyomo/core/expr/visitor.py
@@ -1181,7 +1181,8 @@ def visit(self, node):
             return
 
         # TODO: Confirm that this has the right semantics
-        if not node.is_variable_type() and node.is_fixed():
+        if (not node.is_variable_type() and node.is_fixed()
+                and not node.is_constant()):
             if id(node) in self.seen:
                 return
             self.seen.add(id(node))


Test patch:
diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py
--- a/pyomo/core/tests/unit/test_visitor.py
+++ b/pyomo/core/tests/unit/test_visitor.py
@@ -19,7 +19,7 @@
 from pyomo.common.unittest import nottest
 
 from pyomo.environ import ConcreteModel, RangeSet, Param, Var, Expression, ExternalFunction, VarList, sum_product, inequality, quicksum, sin, tanh
-from pyomo.core.expr.numvalue import nonpyomo_leaf_types
+from pyomo.core.expr.numvalue import nonpyomo_leaf_types, NumericConstant
 from pyomo.core.expr.numeric_expr import (
     SumExpression, ProductExpression, 
     MonomialTermExpression, LinearExpression)
@@ -33,6 +33,7 @@
 from pyomo.core.base.param import _ParamData, SimpleParam
 from pyomo.core.expr.template_expr import IndexTemplate
 from pyomo.core.expr.expr_errors import TemplateExpressionError
+from pyomo.common.collections import ComponentSet
 from pyomo.common.log import LoggingIntercept
 from io import StringIO
 
@@ -76,7 +77,7 @@ def test_identify_duplicate_vars(self):
 
     def test_identify_vars_expr(self):
         #
-        # Identify variables when there are duplicates
+        # Identify variables in named expressions
         #
         m = ConcreteModel()
         m.a = Var(initialize=1)
@@ -156,7 +157,7 @@ def test_identify_mutable_parameters(self):
         m.a = Var(initialize=1)
         m.b = Var(m.I, initialize=1)
         #
-        # There are no variables in expressions with only vars
+        # There are no parameters in expressions with only vars
         #
         self.assertEqual( list(identify_mutable_parameters(m.a)), [] )
         self.assertEqual( list(identify_mutable_parameters(m.b[1])), [] )
@@ -167,6 +168,23 @@ def test_identify_mutable_parameters(self):
         self.assertEqual( list(identify_mutable_parameters(
             m.a**m.b[1] + m.b[2]*m.b[3]*m.b[2])), [] )
 
+    def test_identify_mutable_parameters_constants(self):
+        #
+        # SimpleParams and NumericConstants are not recognized
+        #
+        m = ConcreteModel()
+        m.x = Var(initialize=1)
+        m.x.fix()
+        m.p = Param(initialize=2, mutable=False)
+        m.p_m = Param(initialize=3, mutable=True)
+        e1 = (m.x + m.p + NumericConstant(5))
+        self.assertEqual(list(identify_mutable_parameters(e1)), [])
+
+        e2 = (5*m.x + NumericConstant(3)*m.p_m + m.p == 0)
+        mut_params = list(identify_mutable_parameters(e2))
+        self.assertEqual(len(mut_params), 1)
+        self.assertIs(mut_params[0], m.p_m)
+
     def test_identify_duplicate_params(self):
         #
         # Identify mutable params when there are duplicates
@@ -179,7 +197,7 @@ def test_identify_duplicate_params(self):
 
     def test_identify_mutable_parameters_expr(self):
         #
-        # Identify variables when there are duplicates
+        # Identify mutable params in named expressions
         #
         m = ConcreteModel()
         m.a = Param(initialize=1, mutable=True)
@@ -191,6 +209,17 @@ def test_identify_mutable_parameters_expr(self):
         self.assertEqual( list(identify_mutable_parameters(m.E[0])), [ m.a ] )
         self.assertEqual( list(identify_mutable_parameters(m.E[1])), [ m.b ] )
 
+    def test_identify_mutable_parameters_logical_expr(self):
+        #
+        # Identify mutable params in logical expressions
+        #
+        m = ConcreteModel()
+        m.a = Param(initialize=0, mutable=True)
+        expr = m.a+1 == 0
+        param_set = ComponentSet(identify_mutable_parameters(expr))
+        self.assertEqual(len(param_set), 1)
+        self.assertIn(m.a, param_set)
+
     def test_identify_mutable_parameters_params(self):
         m = ConcreteModel()
         m.I = RangeSet(3)