Repository: Pyomo/pyomo

Issue:
Add a function that can robustly compare expressions.
## Summary
This will be useful for testing.

A fairly easy way to implement this would be to "serialize" an expression into a list of nodes or node types.

Current tests that should use this:
  - pyomo/core/tests/unit/test_numeric_expr.py: `decompose_linear_term_wrapper` and `decompose_term_wrapper`


Discussion:

Resolving patch:
diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py
new file mode 100644
--- /dev/null
+++ b/pyomo/core/expr/compare.py
@@ -0,0 +1,175 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+from .visitor import StreamBasedExpressionVisitor
+from .numvalue import nonpyomo_leaf_types
+from .numeric_expr import (
+    LinearExpression, MonomialTermExpression, SumExpression, ExpressionBase,
+    ProductExpression, DivisionExpression, ReciprocalExpression, PowExpression,
+    NegationExpression, UnaryFunctionExpression, ExternalFunctionExpression,
+    NPV_ProductExpression, NPV_DivisionExpression, NPV_ReciprocalExpression,
+    NPV_PowExpression, NPV_SumExpression, NPV_NegationExpression,
+    NPV_UnaryFunctionExpression, NPV_ExternalFunctionExpression,
+    Expr_ifExpression, AbsExpression, NPV_AbsExpression, NumericValue)
+from pyomo.core.expr.logical_expr import (
+    RangedExpression, InequalityExpression, EqualityExpression
+)
+from typing import List
+from pyomo.common.errors import PyomoException
+
+
+def handle_linear_expression(node: LinearExpression, pn: List):
+    pn.append((LinearExpression, 2*len(node.linear_vars) + 1))
+    pn.append(node.constant)
+    pn.extend(node.linear_coefs)
+    pn.extend(node.linear_vars)
+    return tuple()
+
+
+def handle_expression(node: ExpressionBase, pn: List):
+    pn.append((type(node), node.nargs()))
+    return node.args
+
+
+def handle_unary_expression(node: UnaryFunctionExpression, pn: List):
+    pn.append((UnaryFunctionExpression, 1, node.getname()))
+    return node.args
+
+
+def handle_external_function_expression(node: ExternalFunctionExpression, pn: List):
+    pn.append((ExternalFunctionExpression, node.nargs(), node._fcn))
+    return node.args
+
+
+handler = dict()
+handler[LinearExpression] = handle_linear_expression
+handler[SumExpression] = handle_expression
+handler[MonomialTermExpression] = handle_expression
+handler[ProductExpression] = handle_expression
+handler[DivisionExpression] = handle_expression
+handler[ReciprocalExpression] = handle_expression
+handler[PowExpression] = handle_expression
+handler[NegationExpression] = handle_expression
+handler[NPV_ProductExpression] = handle_expression
+handler[NPV_DivisionExpression] = handle_expression
+handler[NPV_ReciprocalExpression] = handle_expression
+handler[NPV_PowExpression] = handle_expression
+handler[NPV_SumExpression] = handle_expression
+handler[NPV_NegationExpression] = handle_expression
+handler[UnaryFunctionExpression] = handle_unary_expression
+handler[NPV_UnaryFunctionExpression] = handle_unary_expression
+handler[ExternalFunctionExpression] = handle_external_function_expression
+handler[NPV_ExternalFunctionExpression] = handle_external_function_expression
+handler[Expr_ifExpression] = handle_expression
+handler[AbsExpression] = handle_unary_expression
+handler[NPV_AbsExpression] = handle_unary_expression
+handler[RangedExpression] = handle_expression
+handler[InequalityExpression] = handle_expression
+handler[EqualityExpression] = handle_expression
+
+
+class PrefixVisitor(StreamBasedExpressionVisitor):
+    def __init__(self):
+        super().__init__()
+        self._result = None
+
+    def initializeWalker(self, expr):
+        self._result = []
+        return True, None
+
+    def enterNode(self, node):
+        ntype = type(node)
+        if ntype in nonpyomo_leaf_types:
+            self._result.append(node)
+            return tuple(), None
+
+        if node.is_expression_type():
+            return handler[ntype](node, self._result), None
+        else:
+            self._result.append(node)
+            return tuple(), None
+
+    def finalizeResult(self, result):
+        ans = self._result
+        self._result = None
+        return ans
+
+
+def convert_expression_to_prefix_notation(expr):
+    """
+    This function converts pyomo expressions to a list that looks very
+    much like prefix notation.  The result can be used in equality
+    comparisons to compare expression trees.
+
+    Note that the data structure returned by this function might be
+    changed in the future. However, we will maintain that the result
+    can be used in equality comparisons.
+
+    Also note that the result should really only be used in equality
+    comparisons if the equality comparison is expected to return
+    True. If the expressions being compared are expected to be
+    different, then the equality comparison will often result in an
+    error rather than returning False.
+
+    m = ConcreteModel()
+    m.x = Var()
+    m.y = Var()
+
+    e1 = m.x * m.y
+    e2 = m.x * m.y
+    e3 = m.x + m.y
+
+    convert_expression_to_prefix_notation(e1) == convert_expression_to_prefix_notation(e2)  # True
+    convert_expression_to_prefix_notation(e1) == convert_expression_to_prefix_notation(e3)  # Error
+
+    However, the compare_expressions function can be used:
+
+    compare_expressions(e1, e2)  # True
+    compare_expressions(e1, e3)  # False
+
+    Parameters
+    ----------
+    expr: NumericValue
+        A Pyomo expression, Var, or Param
+
+    Returns
+    -------
+    prefix_notation: list
+        The expression in prefix notation
+
+    """
+    visitor = PrefixVisitor()
+    return visitor.walk_expression(expr)
+
+
+def compare_expressions(expr1, expr2):
+    """
+    Returns True if 2 expression trees are identical. Returns False
+    otherwise.
+
+    Parameters
+    ----------
+    expr1: NumericValue
+        A Pyomo Var, Param, or expression
+    expr2: NumericValue
+        A PYomo Var, Param, or expression
+
+    Returns
+    -------
+    res: bool
+        A bool indicating whether or not the expressions are identical.
+
+    """
+    pn1 = convert_expression_to_prefix_notation(expr1)
+    pn2 = convert_expression_to_prefix_notation(expr2)
+    try:
+        res = pn1 == pn2
+    except PyomoException:
+        res = False
+    return res


Test patch:
diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py
new file mode 100644
--- /dev/null
+++ b/pyomo/core/tests/unit/test_compare.py
@@ -0,0 +1,160 @@
+#  ___________________________________________________________________________
+#
+#  Pyomo: Python Optimization Modeling Objects
+#  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
+#  rights in this software.
+#  This software is distributed under the 3-clause BSD License.
+#  ___________________________________________________________________________
+from pyomo.common import unittest
+import pyomo.environ as pe
+from pyomo.core.expr.numeric_expr import (
+    LinearExpression, MonomialTermExpression, SumExpression,
+    ProductExpression, DivisionExpression, PowExpression,
+    NegationExpression, UnaryFunctionExpression, ExternalFunctionExpression,
+    Expr_ifExpression
+)
+from pyomo.core.expr.logical_expr import (
+    InequalityExpression, EqualityExpression, RangedExpression
+)
+from pyomo.core.expr.compare import (
+    convert_expression_to_prefix_notation, compare_expressions
+)
+from pyomo.common.getGSL import find_GSL
+
+
+class TestConvertToPrefixNotation(unittest.TestCase):
+    def test_linear_expression(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var([1, 2, 3, 4])
+        e = LinearExpression(constant=3, linear_coefs=m.x.keys(), linear_vars=m.x.values())
+        expected = [(LinearExpression, 9), 3, 1, 2, 3, 4, m.x[1], m.x[2], m.x[3], m.x[4]]
+        pn = convert_expression_to_prefix_notation(e)
+        self.assertEqual(pn, expected)
+
+    def test_multiple(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        m.y = pe.Var()
+
+        e = m.x**2 + m.x*m.y/3 + 4
+        expected = [(SumExpression, 3),
+                    (PowExpression, 2),
+                    m.x,
+                    2,
+                    (DivisionExpression, 2),
+                    (ProductExpression, 2),
+                    m.x,
+                    m.y,
+                    3,
+                    4]
+        pn = convert_expression_to_prefix_notation(e)
+        self.assertEqual(pn, expected)
+        e2 = m.x**2 + m.x*m.y/3 + 4
+        e3 = m.y**2 + m.x*m.y/3 + 4
+        self.assertTrue(compare_expressions(e, e2))
+        self.assertFalse(compare_expressions(e, e3))
+
+    def test_unary(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        e = pe.log(m.x)
+        expected = [(UnaryFunctionExpression, 1, 'log'), m.x]
+        pn = convert_expression_to_prefix_notation(e)
+        self.assertEqual(expected, pn)
+
+    def test_external_function(self):
+        DLL = find_GSL()
+        if not DLL:
+            self.skipTest('Could not find the amplgsl.dll library')
+
+        m = pe.ConcreteModel()
+        m.hypot = pe.ExternalFunction(library=DLL, function='gsl_hypot')
+        m.x = pe.Var(initialize=0.5)
+        m.y = pe.Var(initialize=1.5)
+        e = 2 * m.hypot(m.x, m.x*m.y)
+        expected = [(ProductExpression, 2),
+                    2,
+                    (ExternalFunctionExpression, 2, m.hypot),
+                    m.x,
+                    (ProductExpression, 2),
+                    m.x,
+                    m.y]
+        pn = convert_expression_to_prefix_notation(e)
+        self.assertEqual(expected, pn)
+
+    def test_var(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        pn = convert_expression_to_prefix_notation(m.x)
+        self.assertEqual(pn, [m.x])
+
+    def test_float(self):
+        pn = convert_expression_to_prefix_notation(4.3)
+        self.assertEqual(pn, [4.3])
+
+    def test_monomial(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        e = 2*m.x
+        pn = convert_expression_to_prefix_notation(e)
+        expected = [(MonomialTermExpression, 2),
+                    2,
+                    m.x]
+        self.assertEqual(pn, expected)
+
+    def test_negation(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        e = -m.x**2
+        pn = convert_expression_to_prefix_notation(e)
+        expected = [(NegationExpression, 1),
+                    (PowExpression, 2),
+                    m.x,
+                    2]
+        self.assertEqual(pn, expected)
+
+    def test_abs(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        e = abs(m.x)
+        pn = convert_expression_to_prefix_notation(e)
+        expected = [(UnaryFunctionExpression, 1, 'abs'),
+                    m.x]
+        self.assertEqual(pn, expected)
+
+    def test_expr_if(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        m.y = pe.Var()
+        e = pe.Expr_if(m.x <= 0, m.y + m.x == 0, m.y - m.x == 0)
+        pn = convert_expression_to_prefix_notation(e)
+        expected = [(Expr_ifExpression, 3),
+                    (InequalityExpression, 2),
+                    m.x,
+                    0,
+                    (EqualityExpression, 2),
+                    (SumExpression, 2),
+                    m.y,
+                    m.x,
+                    0,
+                    (EqualityExpression, 2),
+                    (SumExpression, 2),
+                    m.y,
+                    (MonomialTermExpression, 2),
+                    -1,
+                    m.x,
+                    0]
+        self.assertEqual(pn, expected)
+
+    def test_ranged_expression(self):
+        m = pe.ConcreteModel()
+        m.x = pe.Var()
+        e = pe.inequality(-1, m.x, 1)
+        pn = convert_expression_to_prefix_notation(e)
+        expected = [(RangedExpression, 3),
+                    -1,
+                    m.x,
+                    1]
+        self.assertEqual(pn, expected)