Repository: Pyomo/pyomo

Issue:
NPV_SumExpression does not inherit from NPV_Mixin
## Summary

`NPV_SumExpression` does not inherit form `NPV_Mixin`. As a result replacing mutable parameters in an `NPV_SumExpression` with variables does not work correctly.

### Steps to reproduce the issue

```
import pyomo.environ as pe
from pyomo.core.expr.visitor import replace_expressions

m = pe.ConcreteModel()
m.x = pe.Var()
m.p = pe.Param(mutable=True)
e1 = 2 + m.p
print(type(e1))
print(e1)
e2 = replace_expressions(e1, substitution_map={id(m.p): m.x})
print(type(e2))
print(e2)
```

outputs

```
<class 'pyomo.core.expr.numeric_expr.NPV_SumExpression'>
2 + p
<class 'pyomo.core.expr.numeric_expr.NPV_SumExpression'>
2 + x
```



Discussion:

Resolving patch:
diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py
--- a/pyomo/core/expr/compare.py
+++ b/pyomo/core/expr/compare.py
@@ -25,7 +25,7 @@
 
 
 def handle_linear_expression(node: LinearExpression, pn: List):
-    pn.append((LinearExpression, 2*len(node.linear_vars) + 1))
+    pn.append((type(node), 2*len(node.linear_vars) + 1))
     pn.append(node.constant)
     pn.extend(node.linear_coefs)
     pn.extend(node.linear_vars)
@@ -37,13 +37,19 @@ def handle_expression(node: ExpressionBase, pn: List):
     return node.args
 
 
+def handle_named_expression(node, pn: List, include_named_exprs=True):
+    if include_named_exprs:
+        pn.append((type(node), 1))
+    return (node.expr, )
+
+
 def handle_unary_expression(node: UnaryFunctionExpression, pn: List):
-    pn.append((UnaryFunctionExpression, 1, node.getname()))
+    pn.append((type(node), 1, node.getname()))
     return node.args
 
 
 def handle_external_function_expression(node: ExternalFunctionExpression, pn: List):
-    pn.append((ExternalFunctionExpression, node.nargs(), node._fcn))
+    pn.append((type(node), node.nargs(), node._fcn))
     return node.args
 
 
@@ -73,9 +79,10 @@ def handle_external_function_expression(node: ExternalFunctionExpression, pn: Li
 
 
 class PrefixVisitor(StreamBasedExpressionVisitor):
-    def __init__(self):
+    def __init__(self, include_named_exprs=True):
         super().__init__()
         self._result = None
+        self._include_named_exprs = include_named_exprs
 
     def initializeWalker(self, expr):
         self._result = []
@@ -88,7 +95,10 @@ def enterNode(self, node):
             return tuple(), None
 
         if node.is_expression_type():
-            return handler[ntype](node, self._result), None
+            if node.is_named_expression_type():
+                return handle_named_expression(node, self._result, self._include_named_exprs), None
+            else:
+                return handler[ntype](node, self._result), None
         else:
             self._result.append(node)
             return tuple(), None
@@ -99,7 +109,7 @@ def finalizeResult(self, result):
         return ans
 
 
-def convert_expression_to_prefix_notation(expr):
+def convert_expression_to_prefix_notation(expr, include_named_exprs=True):
     """
     This function converts pyomo expressions to a list that looks very
     much like prefix notation.  The result can be used in equality
@@ -142,11 +152,11 @@ def convert_expression_to_prefix_notation(expr):
         The expression in prefix notation
 
     """
-    visitor = PrefixVisitor()
+    visitor = PrefixVisitor(include_named_exprs=include_named_exprs)
     return visitor.walk_expression(expr)
 
 
-def compare_expressions(expr1, expr2):
+def compare_expressions(expr1, expr2, include_named_exprs=True):
     """
     Returns True if 2 expression trees are identical. Returns False
     otherwise.
@@ -156,7 +166,11 @@ def compare_expressions(expr1, expr2):
     expr1: NumericValue
         A Pyomo Var, Param, or expression
     expr2: NumericValue
-        A PYomo Var, Param, or expression
+        A Pyomo Var, Param, or expression
+    include_named_exprs: bool
+        If False, then named expressions will be ignored. In other words, this function
+        will return True if one expression has a named expression and the other does not
+        as long as the rest of the expression trees are identical.
 
     Returns
     -------
@@ -164,8 +178,8 @@ def compare_expressions(expr1, expr2):
         A bool indicating whether or not the expressions are identical.
 
     """
-    pn1 = convert_expression_to_prefix_notation(expr1)
-    pn2 = convert_expression_to_prefix_notation(expr2)
+    pn1 = convert_expression_to_prefix_notation(expr1, include_named_exprs=include_named_exprs)
+    pn2 = convert_expression_to_prefix_notation(expr2, include_named_exprs=include_named_exprs)
     try:
         res = pn1 == pn2
     except PyomoException:
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
@@ -958,7 +958,7 @@ def getname(self, *args, **kwds):
         return 'sum'
 
 
-class NPV_SumExpression(SumExpressionBase):
+class NPV_SumExpression(NPV_Mixin, SumExpressionBase):
     __slots__ = ()
 
     def create_potentially_variable_object(self):
@@ -975,8 +975,22 @@ def _to_string(self, values, verbose, smap, compute_values):
             return "{0} {1}".format(values[0],values[1])
         return "{0} + {1}".format(values[0],values[1])
 
-    def is_potentially_variable(self):
-        return False
+    def create_node_with_local_data(self, args, classtype=None):
+        assert classtype is None
+        try:
+            npv_args = all(
+                type(arg) in native_types or not arg.is_potentially_variable()
+                for arg in args
+            )
+        except AttributeError:
+            # We can hit this during expression replacement when the new
+            # type is not a PyomoObject type, but is not in the
+            # native_types set.  We will play it safe and clear the NPV flag
+            npv_args = False
+        if npv_args:
+            return NPV_SumExpression(args)
+        else:
+            return SumExpression(args)
 
 
 class SumExpression(SumExpressionBase):
@@ -1232,12 +1246,9 @@ def _apply_operation(self, result):
         return self._fcn(result[0])
 
 
-class NPV_UnaryFunctionExpression(UnaryFunctionExpression):
+class NPV_UnaryFunctionExpression(NPV_Mixin, UnaryFunctionExpression):
     __slots__ = ()
 
-    def is_potentially_variable(self):
-        return False
-
 
 # NOTE: This should be a special class, since the expression generation relies
 # on the Python __abs__ method.
@@ -1253,6 +1264,11 @@ class AbsExpression(UnaryFunctionExpression):
     def __init__(self, arg):
         super(AbsExpression, self).__init__(arg, 'abs', abs)
 
+    def create_node_with_local_data(self, args, classtype=None):
+        if classtype is None:
+            classtype = self.__class__
+        return classtype(args)
+
 
 class NPV_AbsExpression(NPV_Mixin, AbsExpression):
     __slots__ = ()


Test patch:
diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py
--- a/pyomo/core/tests/unit/test_compare.py
+++ b/pyomo/core/tests/unit/test_compare.py
@@ -13,7 +13,7 @@
     LinearExpression, MonomialTermExpression, SumExpression,
     ProductExpression, DivisionExpression, PowExpression,
     NegationExpression, UnaryFunctionExpression, ExternalFunctionExpression,
-    Expr_ifExpression
+    Expr_ifExpression, AbsExpression
 )
 from pyomo.core.expr.logical_expr import (
     InequalityExpression, EqualityExpression, RangedExpression
@@ -120,7 +120,7 @@ def test_abs(self):
         m.x = pe.Var()
         e = abs(m.x)
         pn = convert_expression_to_prefix_notation(e)
-        expected = [(UnaryFunctionExpression, 1, 'abs'),
+        expected = [(AbsExpression, 1, 'abs'),
                     m.x]
         self.assertEqual(pn, expected)
 
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
@@ -12,17 +12,23 @@
 #
 
 import os
+import pyomo.core.tests.unit.test_visitor
 from os.path import abspath, dirname
 currdir = dirname(abspath(__file__))+os.sep
 
 import pyomo.common.unittest as unittest
 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.environ import ConcreteModel, RangeSet, Param, Var, Expression, ExternalFunction, VarList, sum_product, inequality, quicksum, sin, tanh, value
 from pyomo.core.expr.numvalue import nonpyomo_leaf_types, NumericConstant
 from pyomo.core.expr.numeric_expr import (
     SumExpression, ProductExpression, 
-    MonomialTermExpression, LinearExpression)
+    MonomialTermExpression, LinearExpression,
+    NPV_SumExpression, NPV_ProductExpression, NegationExpression,
+    NPV_NegationExpression, PowExpression, NPV_PowExpression,
+    MaxExpression, NPV_MaxExpression, MinExpression, NPV_MinExpression,
+    DivisionExpression, NPV_DivisionExpression, UnaryFunctionExpression,
+    NPV_UnaryFunctionExpression, AbsExpression, NPV_AbsExpression)
 from pyomo.core.expr.visitor import (
     FixedExpressionError, NonConstantExpressionError,
     StreamBasedExpressionVisitor, ExpressionReplacementVisitor,
@@ -36,6 +42,7 @@
 from pyomo.common.collections import ComponentSet
 from pyomo.common.log import LoggingIntercept
 from io import StringIO
+from pyomo.core.expr.compare import compare_expressions
 
 
 class TestExpressionUtilities(unittest.TestCase):
@@ -296,8 +303,8 @@ def test_replacement_walker1(self):
         e = sin(M.x) + M.x*M.y + 3
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3", str(e))
-        self.assertEqual("sin(w[1]) + w[1]*w[2] + 3", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3, e))
+        self.assertTrue(compare_expressions(sin(M.w[1]) + M.w[1]*M.w[2] + 3, f))
 
     def test_replacement_walker2(self):
         M = ConcreteModel()
@@ -307,8 +314,8 @@ def test_replacement_walker2(self):
         e = M.x
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("x", str(e))
-        self.assertEqual("w[1]", str(f))
+        self.assertTrue(compare_expressions(M.x, e))
+        self.assertTrue(compare_expressions(M.w[1], f))
 
     def test_replacement_walker3(self):
         M = ConcreteModel()
@@ -319,8 +326,8 @@ def test_replacement_walker3(self):
         e = sin(M.x) + M.x*M.y + 3 <= 0
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3  <=  0", str(e))
-        self.assertEqual("sin(w[1]) + w[1]*w[2] + 3  <=  0", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3  <=  0, e))
+        self.assertTrue(compare_expressions(sin(M.w[1]) + M.w[1]*M.w[2] + 3  <=  0, f))
 
     def test_replacement_walker4(self):
         M = ConcreteModel()
@@ -331,8 +338,8 @@ def test_replacement_walker4(self):
         e = inequality(0, sin(M.x) + M.x*M.y + 3, 1)
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("0  <=  sin(x) + x*y + 3  <=  1", str(e))
-        self.assertEqual("0  <=  sin(w[1]) + w[1]*w[2] + 3  <=  1", str(f))
+        self.assertTrue(compare_expressions(inequality(0, sin(M.x) + M.x*M.y + 3, 1), e))
+        self.assertTrue(compare_expressions(inequality(0, sin(M.w[1]) + M.w[1]*M.w[2] + 3, 1), f))
 
     def test_replacement_walker0(self):
         M = ConcreteModel()
@@ -344,14 +351,21 @@ def test_replacement_walker0(self):
         self.assertIs(type(e), LinearExpression)
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("z[0]*x[0] + z[1]*x[1] + z[2]*x[2]", str(e))
-        self.assertEqual("z[0]*w[1] + z[1]*w[2] + z[2]*w[3]", str(f))
+        self.assertTrue(compare_expressions(LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                             linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                             linear_vars=[i for i in M.w.values()]), f))
 
+        del M.w
+        del M.w_index
+        M.w = VarList()
         e = 2*sum_product(M.z, M.x)
         walker = ReplacementWalkerTest1(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("2*(z[0]*x[0] + z[1]*x[1] + z[2]*x[2])", str(e))
-        self.assertEqual("2*(z[0]*w[4] + z[1]*w[5] + z[2]*w[6])", str(f))
+        self.assertTrue(compare_expressions(2*LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                               linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(2*LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                               linear_vars=[i for i in M.w.values()]), f))
 
     def test_replace_expressions_with_monomial_term(self):
         M = ConcreteModel()
@@ -443,8 +457,8 @@ def test_replacement_walker1(self):
         e = sin(M.x) + M.x*M.y + 3
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3", str(e))
-        self.assertEqual("sin(2*w[1]) + 2*w[1]*(2*w[2]) + 3", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3, e))
+        self.assertTrue(compare_expressions(sin(2*M.w[1]) + 2*M.w[1]*(2*M.w[2]) + 3, f))
 
     def test_replacement_walker2(self):
         M = ConcreteModel()
@@ -454,8 +468,8 @@ def test_replacement_walker2(self):
         e = M.x
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("x", str(e))
-        self.assertEqual("2*w[1]", str(f))
+        self.assertTrue(compare_expressions(M.x, e))
+        self.assertTrue(compare_expressions(2*M.w[1], f))
 
     def test_replacement_walker3(self):
         M = ConcreteModel()
@@ -466,8 +480,8 @@ def test_replacement_walker3(self):
         e = sin(M.x) + M.x*M.y + 3 <= 0
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3  <=  0", str(e))
-        self.assertEqual("sin(2*w[1]) + 2*w[1]*(2*w[2]) + 3  <=  0", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3  <=  0, e))
+        self.assertTrue(compare_expressions(sin(2*M.w[1]) + 2*M.w[1]*(2*M.w[2]) + 3  <=  0, f))
 
     def test_replacement_walker4(self):
         M = ConcreteModel()
@@ -478,8 +492,8 @@ def test_replacement_walker4(self):
         e = inequality(0, sin(M.x) + M.x*M.y + 3, 1)
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("0  <=  sin(x) + x*y + 3  <=  1", str(e))
-        self.assertEqual("0  <=  sin(2*w[1]) + 2*w[1]*(2*w[2]) + 3  <=  1", str(f))
+        self.assertTrue(compare_expressions(inequality(0  ,   sin(M.x) + M.x*M.y + 3  ,   1), e))
+        self.assertTrue(compare_expressions(inequality(0  ,   sin(2*M.w[1]) + 2*M.w[1]*(2*M.w[2]) + 3  ,   1), f))
 
     def test_replacement_walker5(self):
         M = ConcreteModel()
@@ -492,8 +506,8 @@ def test_replacement_walker5(self):
         f = walker.dfs_postorder_stack(e)
         self.assertTrue(e.__class__ is MonomialTermExpression)
         self.assertTrue(f.__class__ is ProductExpression)
-        self.assertEqual("z*x", str(e))
-        self.assertEqual("z*(2*w[1])", str(f))
+        self.assertTrue(compare_expressions(M.z*M.x, e))
+        self.assertTrue(compare_expressions(M.z*(2*M.w[1]), f))
 
     def test_replacement_walker0(self):
         M = ConcreteModel()
@@ -505,14 +519,16 @@ def test_replacement_walker0(self):
         self.assertIs(type(e), LinearExpression)
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("z[0]*x[0] + z[1]*x[1] + z[2]*x[2]", str(e))
-        self.assertEqual("z[0]*(2*w[1]) + z[1]*(2*w[2]) + z[2]*(2*w[3])", str(f))
+        self.assertTrue(compare_expressions(LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                             linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(M.z[0]*(2*M.w[1]) + M.z[1]*(2*M.w[2]) + M.z[2]*(2*M.w[3]), f))
 
         e = 2*sum_product(M.z, M.x)
         walker = ReplacementWalkerTest2(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("2*(z[0]*x[0] + z[1]*x[1] + z[2]*x[2])", str(e))
-        self.assertEqual("2*(z[0]*(2*w[4]) + z[1]*(2*w[5]) + z[2]*(2*w[6]))", str(f))
+        self.assertTrue(compare_expressions(2*LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                               linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(2*(M.z[0]*(2*M.w[4]) + M.z[1]*(2*M.w[5]) + M.z[2]*(2*M.w[6])), f))
 
 #
 # Replace all mutable parameters with variables
@@ -549,8 +565,8 @@ def test_replacement_walker1(self):
         e = sin(M.x) + M.x*M.y + 3
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3", str(e))
-        self.assertEqual("sin(2*w[1]) + 2*w[1]*y + 3", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3, e))
+        self.assertTrue(compare_expressions(sin(2*M.w[1]) + 2*M.w[1]*M.y + 3, f))
 
     def test_replacement_walker2(self):
         M = ConcreteModel()
@@ -560,8 +576,8 @@ def test_replacement_walker2(self):
         e = M.x
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("x", str(e))
-        self.assertEqual("2*w[1]", str(f))
+        self.assertTrue(compare_expressions(M.x, e))
+        self.assertTrue(compare_expressions(2*M.w[1], f))
 
     def test_replacement_walker3(self):
         M = ConcreteModel()
@@ -572,8 +588,8 @@ def test_replacement_walker3(self):
         e = sin(M.x) + M.x*M.y + 3 <= 0
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("sin(x) + x*y + 3  <=  0", str(e))
-        self.assertEqual("sin(2*w[1]) + 2*w[1]*y + 3  <=  0", str(f))
+        self.assertTrue(compare_expressions(sin(M.x) + M.x*M.y + 3  <=  0, e))
+        self.assertTrue(compare_expressions(sin(2*M.w[1]) + 2*M.w[1]*M.y + 3  <=  0, f))
 
     def test_replacement_walker4(self):
         M = ConcreteModel()
@@ -584,8 +600,8 @@ def test_replacement_walker4(self):
         e = inequality(0, sin(M.x) + M.x*M.y + 3, 1)
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("0  <=  sin(x) + x*y + 3  <=  1", str(e))
-        self.assertEqual("0  <=  sin(2*w[1]) + 2*w[1]*y + 3  <=  1", str(f))
+        self.assertTrue(compare_expressions(inequality(0, sin(M.x) + M.x*M.y + 3, 1), e))
+        self.assertTrue(compare_expressions(inequality(0, sin(2*M.w[1]) + 2*M.w[1]*M.y + 3, 1), f))
 
     def test_replacement_walker5(self):
         M = ConcreteModel()
@@ -599,8 +615,8 @@ def test_replacement_walker5(self):
         self.assertIs(e.__class__, MonomialTermExpression)
         self.assertIs(f.__class__, ProductExpression)
         self.assertTrue(f.arg(0).is_potentially_variable())
-        self.assertEqual("z*x", str(e))
-        self.assertEqual("2*w[1]*x", str(f))
+        self.assertTrue(compare_expressions(M.z*M.x, e))
+        self.assertTrue(compare_expressions(2*M.w[1]*M.x, f))
 
     def test_replacement_walker6(self):
         M = ConcreteModel()
@@ -613,8 +629,8 @@ def test_replacement_walker6(self):
         f = walker.dfs_postorder_stack(e)
         self.assertTrue(not e.is_potentially_variable())
         self.assertTrue(f.is_potentially_variable())
-        self.assertEqual("z*2*3", str(e))
-        self.assertEqual("2*w[1]*2*3", str(f))
+        self.assertTrue(compare_expressions(M.z*2*3, e))
+        self.assertTrue(compare_expressions(ProductExpression([ProductExpression([2*M.w[1], 2]), 3]), f))
 
     def test_replacement_walker7(self):
         M = ConcreteModel()
@@ -626,7 +642,7 @@ def test_replacement_walker7(self):
         e = M.x*M.e
         self.assertTrue(e.arg(1).is_potentially_variable())
         self.assertTrue(not e.arg(1).arg(0).is_potentially_variable())
-        self.assertEqual("x*(z*2)", str(e))
+        self.assertTrue(compare_expressions(ProductExpression([M.x, (NPV_ProductExpression([M.z, 2]))]), e, include_named_exprs=False))
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
         self.assertTrue(e.__class__ is ProductExpression)
@@ -634,7 +650,7 @@ def test_replacement_walker7(self):
         self.assertEqual(id(e), id(f))
         self.assertTrue(f.arg(1).is_potentially_variable())
         self.assertTrue(f.arg(1).arg(0).is_potentially_variable())
-        self.assertEqual("x*(2*w[1]*2)", str(f))
+        self.assertTrue(compare_expressions(M.x*ProductExpression([2*M.w[1], 2]), f, include_named_exprs=False))
 
     def test_replacement_walker0(self):
         M = ConcreteModel()
@@ -646,14 +662,16 @@ def test_replacement_walker0(self):
         self.assertIs(type(e), LinearExpression)
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("z[0]*x[0] + z[1]*x[1] + z[2]*x[2]", str(e))
-        self.assertEqual("2*w[1]*x[0] + 2*w[2]*x[1] + 2*w[3]*x[2]", str(f))
+        self.assertTrue(compare_expressions(LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                             linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(2*M.w[1]*M.x[0] + 2*M.w[2]*M.x[1] + 2*M.w[3]*M.x[2], f))
 
         e = 2*sum_product(M.z, M.x)
         walker = ReplacementWalkerTest3(M)
         f = walker.dfs_postorder_stack(e)
-        self.assertEqual("2*(z[0]*x[0] + z[1]*x[1] + z[2]*x[2])", str(e))
-        self.assertEqual("2*(2*w[4]*x[0] + 2*w[5]*x[1] + 2*w[6]*x[2])", str(f))
+        self.assertTrue(compare_expressions(2*LinearExpression(linear_coefs=[i for i in M.z.values()],
+                                                               linear_vars=[i for i in M.x.values()]), e))
+        self.assertTrue(compare_expressions(2*(2*M.w[4]*M.x[0] + 2*M.w[5]*M.x[1] + 2*M.w[6]*M.x[2]), f))
 
 
 #
@@ -677,8 +695,8 @@ def test_no_replacement(self):
 
         e = sum(m.y[i] for i in m.y) == 0
         f = ReplacementWalker_ReplaceInternal().dfs_postorder_stack(e)
-        self.assertEqual("y[1] + y[2] + y[3]  ==  0", str(e))
-        self.assertEqual("y[1] + y[2] + y[3]  ==  0", str(f))
+        self.assertTrue(compare_expressions(m.y[1] + m.y[2] + m.y[3]  ==  0, e))
+        self.assertTrue(compare_expressions(m.y[1] + m.y[2] + m.y[3]  ==  0, f))
         self.assertIs(e, f)
 
     def test_replace(self):
@@ -688,8 +706,8 @@ def test_replace(self):
 
         e = m.y[1]*m.y[2] + m.y[2]*m.y[3]  ==  0
         f = ReplacementWalker_ReplaceInternal().dfs_postorder_stack(e)
-        self.assertEqual("y[1]*y[2] + y[2]*y[3]  ==  0", str(e))
-        self.assertEqual("y[1] + y[2] + (y[2] + y[3])  ==  0", str(f))
+        self.assertTrue(compare_expressions(m.y[1]*m.y[2] + m.y[2]*m.y[3]  ==  0, e))
+        self.assertTrue(compare_expressions(SumExpression([SumExpression([m.y[1], m.y[2]]), SumExpression([m.y[2], m.y[3]])])  ==  0, f))
         self.assertIs(type(f.arg(0)), SumExpression)
         self.assertEqual(f.arg(0).nargs(), 2)
         self.assertIs(type(f.arg(0).arg(0)), SumExpression)
@@ -704,12 +722,106 @@ def test_replace_nested(self):
 
         e = m.y[1]*m.y[2]*m.y[2]*m.y[3]  ==  0
         f = ReplacementWalker_ReplaceInternal().dfs_postorder_stack(e)
-        self.assertEqual("y[1]*y[2]*y[2]*y[3]  ==  0", str(e))
-        self.assertEqual("y[1] + y[2] + y[2] + y[3]  ==  0", str(f))
+        self.assertTrue(compare_expressions(m.y[1]*m.y[2]*m.y[2]*m.y[3]  ==  0, e))
+        self.assertTrue(compare_expressions(m.y[1] + m.y[2] + m.y[2] + m.y[3]  ==  0, f))
         self.assertIs(type(f.arg(0)), SumExpression)
         self.assertEqual(f.arg(0).nargs(), 4)
 
 
+class TestReplacementWithNPV(unittest.TestCase):
+
+    def test_npv_sum(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = m.p1 + 2
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, m.p2 + 2))
+        self.assertTrue(compare_expressions(e3, m.x + 2))
+
+    def test_npv_negation(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = -m.p1
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, -m.p2))
+        self.assertTrue(compare_expressions(e3, NegationExpression([m.x])))
+
+    def test_npv_pow(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = m.p1**3
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, m.p2**3))
+        self.assertTrue(compare_expressions(e3, m.x**3))
+
+    def test_npv_product(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = m.p1*3
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, m.p2*3))
+        self.assertTrue(compare_expressions(e3, ProductExpression([m.x, 3])))
+
+    def test_npv_div(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = m.p1/3
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, m.p2/3))
+        self.assertTrue(compare_expressions(e3, DivisionExpression([m.x, 3])))
+
+    def test_npv_unary(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var(initialize=0)
+
+        e1 = sin(m.p1)
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, sin(m.p2)))
+        self.assertTrue(compare_expressions(e3, sin(m.x)))
+
+    def test_npv_abs(self):
+        m = ConcreteModel()
+        m.p1 = Param(mutable=True)
+        m.p2 = Param(mutable=True)
+        m.x = Var()
+
+        e1 = abs(m.p1)
+        e2 = replace_expressions(e1, {id(m.p1): m.p2})
+        e3 = replace_expressions(e1, {id(m.p1): m.x})
+
+        self.assertTrue(compare_expressions(e2, abs(m.p2)))
+        self.assertTrue(compare_expressions(e3, abs(m.x)))
+
+
 class TestStreamBasedExpressionVisitor(unittest.TestCase):
     def setUp(self):
         self.m = m = ConcreteModel()