From 7ca7ca8bed488e1be27b8d811989338beb9d6be9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 15 Nov 2024 17:35:09 -0700 Subject: [PATCH 1/3] Update compare.PrefixVisitor to admit/compare sequences/numpy arrays --- pyomo/core/expr/compare.py | 44 +++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 105ef1db199..fc4bf17ec03 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -66,6 +66,11 @@ def handle_external_function_expression(node: ExternalFunctionExpression, pn: Li return node.args +def handle_sequence(node: collections.abc.Sequence, pn: List): + pn.append((collections.abc.Sequence, len(node))) + return list(node) + + def _generic_expression_handler(): return handle_expression @@ -79,6 +84,7 @@ def _generic_expression_handler(): handler[AbsExpression] = handle_unary_expression handler[NPV_AbsExpression] = handle_unary_expression handler[RangedExpression] = handle_expression +handler[list] = handle_sequence class PrefixVisitor(StreamBasedExpressionVisitor): @@ -97,19 +103,26 @@ def enterNode(self, node): self._result.append(node) return tuple(), None - if node.is_expression_type(): - 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 + if ntype in handler: + return handler[ntype](node, self._result), None + + if hasattr(node, 'is_expression_type'): + if node.is_expression_type(): + 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 + elif hasattr(node, '__len__'): + handler[ntype] = handle_sequence + return handle_sequence(node, self._result), None + + self._result.append(node) + return tuple(), None def finalizeResult(self, result): ans = self._result @@ -161,10 +174,7 @@ def convert_expression_to_prefix_notation(expr, include_named_exprs=True): """ visitor = PrefixVisitor(include_named_exprs=include_named_exprs) - if isinstance(expr, Sequence): - return expr.__class__(visitor.walk_expression(e) for e in expr) - else: - return visitor.walk_expression(expr) + return visitor.walk_expression(expr) def compare_expressions(expr1, expr2, include_named_exprs=True): From 345917e52b741f5e0552c710b618f3e058013dfe Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 15 Nov 2024 17:36:01 -0700 Subject: [PATCH 2/3] Update numpy compatibility to map ScalarVar to 0d ndarray Fixes #3418 --- pyomo/core/base/indexed_component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 8a19c35e3e0..ee1fe1e0037 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -1201,7 +1201,7 @@ def __array__(self, dtype=None): if not self.is_indexed(): ans = _ndarray.NumericNDArray(shape=(1,), dtype=object) ans[0] = self - return ans + return ans.reshape(()) _dim = self.dim() if _dim is None: From 05bdaf7666deb751532fd9de66d9cf2e213b1e0f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 15 Nov 2024 17:37:34 -0700 Subject: [PATCH 3/3] Add tests for numpy expression compatibility --- pyomo/core/tests/unit/test_expr_numpy.py | 95 ++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 pyomo/core/tests/unit/test_expr_numpy.py diff --git a/pyomo/core/tests/unit/test_expr_numpy.py b/pyomo/core/tests/unit/test_expr_numpy.py new file mode 100644 index 00000000000..08fcfbd7061 --- /dev/null +++ b/pyomo/core/tests/unit/test_expr_numpy.py @@ -0,0 +1,95 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# 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. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.environ import ConcreteModel, Var, Constraint + + +@unittest.skipUnless(numpy_available, "tests require numpy") +class TestNumpyExpr(unittest.TestCase): + def test_scalar_operations(self): + m = ConcreteModel() + m.x = Var() + + a = np.array(m.x) + self.assertEqual(a.shape, ()) + + self.assertExpressionsEqual(5 * a, 5 * m.x) + self.assertExpressionsEqual(np.array([2, 3]) * a, [2 * m.x, 3 * m.x]) + self.assertExpressionsEqual(np.array([5, 6]) * m.x, [5 * m.x, 6 * m.x]) + self.assertExpressionsEqual(np.array([8, m.x]) * m.x, [8 * m.x, m.x * m.x]) + + a = np.array([m.x]) + self.assertEqual(a.shape, (1,)) + + self.assertExpressionsEqual(5 * a, [5 * m.x]) + self.assertExpressionsEqual(np.array([2, 3]) * a, [2 * m.x, 3 * m.x]) + self.assertExpressionsEqual(np.array([5, 6]) * m.x, [5 * m.x, 6 * m.x]) + self.assertExpressionsEqual(np.array([8, m.x]) * m.x, [8 * m.x, m.x * m.x]) + + def test_vector_operations(self): + m = ConcreteModel() + m.x = Var() + m.y = Var([0, 1, 2]) + + with self.assertRaisesRegex(TypeError, "unsupported operand"): + # TODO: when we finally support a true matrix expression + # system, this test should work + self.assertExpressionsEqual(5 * m.y, [5 * m.y[0], 5 * m.y[1], 5 * m.y[2]]) + + a = np.array(5) + self.assertExpressionsEqual(a * m.y, [5 * m.y[0], 5 * m.y[1], 5 * m.y[2]]) + self.assertExpressionsEqual(m.y * a, [5 * m.y[0], 5 * m.y[1], 5 * m.y[2]]) + a = np.array([5]) + self.assertExpressionsEqual(a * m.y, [5 * m.y[0], 5 * m.y[1], 5 * m.y[2]]) + self.assertExpressionsEqual(m.y * a, [5 * m.y[0], 5 * m.y[1], 5 * m.y[2]]) + + a = np.array(5) + with self.assertRaisesRegex(TypeError, "unsupported operand"): + # TODO: when we finally support a true matrix expression + # system, this test should work + self.assertExpressionsEqual( + a * m.x * m.y, [5 * m.x * m.y[0], 5 * m.x * m.y[1], 5 * m.x * m.y[2]] + ) + self.assertExpressionsEqual( + a * m.y * m.x, [5 * m.y[0] * m.x, 5 * m.y[1] * m.x, 5 * m.y[2] * m.x] + ) + self.assertExpressionsEqual( + a * m.y * m.y, + [5 * m.y[0] * m.y[0], 5 * m.y[1] * m.y[1], 5 * m.y[2] * m.y[2]], + ) + self.assertExpressionsEqual( + m.y * a * m.x, [5 * m.y[0] * m.x, 5 * m.y[1] * m.x, 5 * m.y[2] * m.x] + ) + with self.assertRaisesRegex(TypeError, "unsupported operand"): + # TODO: when we finally support a true matrix expression + # system, this test should work + self.assertExpressionsEqual( + m.y * m.x * a, [5 * m.y[0] * m.x, 5 * m.y[1] * m.x, 5 * m.y[2] * m.x] + ) + with self.assertRaisesRegex(TypeError, "unsupported operand"): + # TODO: when we finally support a true matrix expression + # system, this test should work + self.assertExpressionsEqual( + m.x * a * m.y, [5 * m.y[0] * m.x, 5 * m.y[1] * m.x, 5 * m.y[2] * m.x] + ) + with self.assertRaisesRegex(TypeError, "unsupported operand"): + # TODO: when we finally support a true matrix expression + # system, this test should work + self.assertExpressionsEqual( + m.x * m.y * a, [5 * m.y[0] * m.x, 5 * m.y[1] * m.x, 5 * m.y[2] * m.x] + ) + + +if __name__ == "__main__": + unittest.main()