From a32149d74fc24a02a52f8ad5ca2b71e34c7d831c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 15:09:26 -0600 Subject: [PATCH 01/15] Move TemplateExpressionError to common.errros --- pyomo/common/errors.py | 16 ++++++++++++++++ pyomo/core/base/indexed_component.py | 3 +-- pyomo/core/expr/expr_errors.py | 11 +++++++---- pyomo/core/expr/template_expr.py | 2 +- pyomo/core/expr/visitor.py | 9 ++++----- pyomo/core/tests/unit/test_visitor.py | 2 +- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/pyomo/common/errors.py b/pyomo/common/errors.py index 55cd2ce0723..5127ce5315c 100644 --- a/pyomo/common/errors.py +++ b/pyomo/common/errors.py @@ -100,6 +100,22 @@ class PyomoException(Exception): pass +class TemplateExpressionError(ValueError): + """Special ValueError raised by getitem for template arguments + + This exception is triggered by the Pyomo expression system when + attempting to get a member of an IndexedComponent using either a + TemplateIndex, or an expression vcontaining an TemplateIndex. + + Users should never see this exception. + + """ + + def __init__(self, template, *args, **kwds): + self.template = template + super(TemplateExpressionError, self).__init__(*args, **kwds) + + class DeveloperError(PyomoException, NotImplementedError): """ Exception class used to throw errors that result from Pyomo diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 01a175a184e..0e063f14e4c 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -20,7 +20,6 @@ from copy import deepcopy from pyomo.core.expr import current as EXPR -from pyomo.core.expr.expr_errors import TemplateExpressionError from pyomo.core.expr.numvalue import native_types, NumericNDArray from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.initializer import Initializer @@ -28,10 +27,10 @@ from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_set -from pyomo.common import DeveloperError from pyomo.common.autoslots import fast_deepcopy from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.errors import DeveloperError, TemplateExpressionError from pyomo.common.modeling import NOTSET from pyomo.common.sorting import sorted_robust diff --git a/pyomo/core/expr/expr_errors.py b/pyomo/core/expr/expr_errors.py index 95c982b3679..cc540c69b47 100644 --- a/pyomo/core/expr/expr_errors.py +++ b/pyomo/core/expr/expr_errors.py @@ -9,8 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.deprecation import relocated_module_attribute -class TemplateExpressionError(ValueError): - def __init__(self, template, *args, **kwds): - self.template = template - super(TemplateExpressionError, self).__init__(*args, **kwds) +relocated_module_attribute( + 'TemplateExpressionError', + 'pyomo.common.errors.TemplateExpressionError', + version='6.6.2.dev0', + f_globals=globals(), +) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index 97b0bfa0912..4682844fef0 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -16,8 +16,8 @@ import builtins from pyomo.common.backports import nullcontext +from pyomo.common.errors import TemplateExpressionError from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin -from pyomo.core.expr.expr_errors import TemplateExpressionError from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( NumericExpression, diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 2cdf3a1a39c..c8f22ba1d3a 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -19,17 +19,16 @@ logger = logging.getLogger('pyomo.core') -from .symbol_map import SymbolMap -from . import expr_common as common -from .expr_errors import TemplateExpressionError from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.errors import DeveloperError -from pyomo.core.expr.numvalue import ( +from pyomo.common.errors import DeveloperError, TemplateExpressionError +from pyomo.common.numeric_types import ( nonpyomo_leaf_types, native_types, native_numeric_types, value, ) +import pyomo.core.expr.expr_common as common +from pyomo.core.expr.symbol_map import SymbolMap try: # sys._getframe is slightly faster than inspect's currentframe, but diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 0dc7e6ebc08..b70996a13dc 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -74,8 +74,8 @@ ) from pyomo.core.base.param import _ParamData, ScalarParam 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.errors import TemplateExpressionError from pyomo.common.log import LoggingIntercept from io import StringIO from pyomo.core.expr.compare import assertExpressionsEqual From bcceb9074434ff0fc086e94cd28bf87640eb5e6b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 15:19:10 -0600 Subject: [PATCH 02/15] Move value(), check_if_numeric_type() to common.numeric_types --- pyomo/common/numeric_types.py | 141 +++++++++++++++++++++++++++++ pyomo/core/base/PyomoModel.py | 2 +- pyomo/core/base/connector.py | 4 +- pyomo/core/base/expression.py | 10 +- pyomo/core/base/param.py | 4 +- pyomo/core/base/piecewise.py | 2 +- pyomo/core/expr/numvalue.py | 137 +--------------------------- pyomo/core/expr/relational_expr.py | 13 ++- pyomo/dae/contset.py | 2 +- pyomo/network/port.py | 3 +- pyomo/repn/beta/matrix.py | 3 +- pyomo/repn/standard_repn.py | 3 +- 12 files changed, 169 insertions(+), 155 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 27eec3a96f2..dbad3ef0853 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -9,7 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging +import sys + from pyomo.common.deprecation import deprecated, relocated_module_attribute +from pyomo.common.errors import TemplateExpressionError + +logger = logging.getLogger(__name__) #: Python set used to identify numeric constants, boolean values, strings #: and instances of @@ -123,3 +129,138 @@ def RegisterLogicalType(new_type): native_logical_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type) + + +def check_if_numeric_type(obj): + """Test if the argument behaves like a numeric type. + + We check for "numeric types" by checking if we can add zero to it + without changing the object's type. If that works, then we register + the type in native_numeric_types. + + """ + obj_class = obj.__class__ + # Do not re-evaluate known native types + if obj_class in native_types: + return obj_class in native_numeric_types + + try: + obj_plus_0 = obj + 0 + obj_p0_class = obj_plus_0.__class__ + # ensure that the object is comparable to 0 in a meaningful way + # (among other things, this prevents numpy.ndarray objects from + # being added to native_numeric_types) + if not ((obj < 0) ^ (obj >= 0)): + return False + # Native types *must* be hashable + hash(obj) + except: + return False + if obj_p0_class is obj_class or obj_p0_class in native_numeric_types: + # + # If we get here, this is a reasonably well-behaving + # numeric type: add it to the native numeric types + # so that future lookups will be faster. + # + RegisterNumericType(obj_class) + # + # Generate a warning, since Pyomo's management of third-party + # numeric types is more robust when registering explicitly. + # + logger.warning( + f"""Dynamically registering the following numeric type: + {obj_class.__module__}.{obj_class.__name__} +Dynamic registration is supported for convenience, but there are known +limitations to this approach. We recommend explicitly registering +numeric types using RegisterNumericType() or RegisterIntegerType().""" + ) + return True + else: + return False + + +def value(obj, exception=True): + """ + A utility function that returns the value of a Pyomo object or + expression. + + Args: + obj: The argument to evaluate. If it is None, a + string, or any other primitive numeric type, + then this function simply returns the argument. + Otherwise, if the argument is a NumericValue + then the __call__ method is executed. + exception (bool): If :const:`True`, then an exception should + be raised when instances of NumericValue fail to + s evaluate due to one or more objects not being + initialized to a numeric value (e.g, one or more + variables in an algebraic expression having the + value None). If :const:`False`, then the function + returns :const:`None` when an exception occurs. + Default is True. + + Returns: A numeric value or None. + """ + if obj.__class__ in native_types: + return obj + if obj.__class__ in pyomo_constant_types: + # + # I'm commenting this out for now, but I think we should never expect + # to see a numeric constant with value None. + # + # if exception and obj.value is None: + # raise ValueError( + # "No value for uninitialized NumericConstant object %s" + # % (obj.name,)) + return obj.value + # + # Test if we have a duck typed Pyomo expression + # + try: + obj.is_numeric_type() + except AttributeError: + # + # TODO: Historically we checked for new *numeric* types and + # raised exceptions for anything else. That is inconsistent + # with allowing native_types like None/str/bool to be returned + # from value(). We should revisit if that is worthwhile to do + # here. + # + if check_if_numeric_type(obj): + return obj + else: + if not exception: + return None + raise TypeError( + "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__ + ) from None + # + # Evaluate the expression object + # + if exception: + # + # Here, we try to catch the exception + # + try: + tmp = obj(exception=True) + if tmp is None: + raise ValueError( + "No value for uninitialized NumericValue object %s" % (obj.name,) + ) + return tmp + except TemplateExpressionError: + # Template expressions work by catching this error type. So + # we should defer this error handling and not log an error + # message. + raise + except: + logger.error( + "evaluating object as numeric value: %s\n (object: %s)\n%s" + % (obj, type(obj), sys.exc_info()[1]) + ) + raise + else: + # + # Here, we do not try to catch the exception + # + return obj(exception=False) diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index f6f0713ab1f..f8b2710b9f2 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -23,6 +23,7 @@ from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set +from pyomo.common.numeric_types import value from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.symbol_map import SymbolMap from pyomo.core.base.component import ModelComponentFactory @@ -30,7 +31,6 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.objective import Objective from pyomo.core.base.suffix import active_import_suffix_generator -from pyomo.core.base.numvalue import value from pyomo.core.base.block import ScalarBlock from pyomo.core.base.set import Set from pyomo.core.base.componentuid import ComponentUID diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 63513bc1765..f3d4833b837 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -19,13 +19,13 @@ from pyomo.common.formatting import tabular_writer from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET +from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer - +from pyomo.core.expr.numvalue import NumericValue from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent from pyomo.core.base.misc import apply_indexed_rule -from pyomo.core.base.numvalue import NumericValue, value from pyomo.core.base.transformation import TransformationFactory logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index a724932ecc7..320d759a9db 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -21,7 +21,11 @@ from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer from pyomo.common.timing import ConstructionTimer -from pyomo.common.numeric_types import native_types, native_numeric_types +from pyomo.common.numeric_types import ( + native_types, + native_numeric_types, + check_if_numeric_type, +) from pyomo.core.expr import current as EXPR import pyomo.core.expr.numeric_expr as numeric_expr @@ -29,13 +33,13 @@ from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set from pyomo.core.base.misc import apply_indexed_rule -from pyomo.core.base.numvalue import NumericValue, as_numeric +from pyomo.core.expr.numvalue import as_numeric from pyomo.core.base.initializer import Initializer logger = logging.getLogger('pyomo.core') -class _ExpressionData(NumericValue): +class _ExpressionData(numeric_expr.NumericValue): """ An object that defines a named expression. diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 273deda390f..288be2c7b4f 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -21,8 +21,9 @@ from pyomo.common.deprecation import deprecation_warning, RenamedClass from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET +from pyomo.common.numeric_types import native_types, value as expr_value from pyomo.common.timing import ConstructionTimer - +from pyomo.core.expr.numvalue import NumericValue from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ( @@ -32,7 +33,6 @@ ) from pyomo.core.base.initializer import Initializer from pyomo.core.base.misc import apply_indexed_rule, apply_parameterized_indexed_rule -from pyomo.core.base.numvalue import NumericValue, native_types, value as expr_value from pyomo.core.base.set import Reals, _AnySet from pyomo.core.base.units_container import units from pyomo.core.expr.current import GetItemExpression diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index b77041ee0ae..8ab6ce38ca5 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -48,6 +48,7 @@ from pyomo.common.log import is_debug_set from pyomo.common.deprecation import deprecation_warning +from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer from pyomo.core.base.block import Block, _BlockData from pyomo.core.base.component import ModelComponentFactory @@ -55,7 +56,6 @@ from pyomo.core.base.sos import SOSConstraint from pyomo.core.base.var import Var, _VarData, IndexedVar from pyomo.core.base.set_types import PositiveReals, NonNegativeReals, Binary -from pyomo.core.base.numvalue import value from pyomo.core.base.util import flatten_tuple logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 1df9de777f0..75af76a9bda 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -68,6 +68,8 @@ native_integer_types, native_logical_types, pyomo_constant_types, + check_if_numeric_type, + value, ) from pyomo.core.pyomoobject import PyomoObject from pyomo.core.expr.expr_errors import TemplateExpressionError @@ -147,93 +149,6 @@ def __str__(self): nonpyomo_leaf_types.add(NonNumericValue) -def value(obj, exception=True): - """ - A utility function that returns the value of a Pyomo object or - expression. - - Args: - obj: The argument to evaluate. If it is None, a - string, or any other primitive numeric type, - then this function simply returns the argument. - Otherwise, if the argument is a NumericValue - then the __call__ method is executed. - exception (bool): If :const:`True`, then an exception should - be raised when instances of NumericValue fail to - evaluate due to one or more objects not being - initialized to a numeric value (e.g, one or more - variables in an algebraic expression having the - value None). If :const:`False`, then the function - returns :const:`None` when an exception occurs. - Default is True. - - Returns: A numeric value or None. - """ - if obj.__class__ in native_types: - return obj - if obj.__class__ in pyomo_constant_types: - # - # I'm commenting this out for now, but I think we should never expect - # to see a numeric constant with value None. - # - # if exception and obj.value is None: - # raise ValueError( - # "No value for uninitialized NumericConstant object %s" - # % (obj.name,)) - return obj.value - # - # Test if we have a duck typed Pyomo expression - # - try: - obj.is_numeric_type() - except AttributeError: - # - # TODO: Historically we checked for new *numeric* types and - # raised exceptions for anything else. That is inconsistent - # with allowing native_types like None/str/bool to be returned - # from value(). We should revisit if that is worthwhile to do - # here. - # - if check_if_numeric_type(obj): - return obj - else: - if not exception: - return None - raise TypeError( - "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__ - ) from None - # - # Evaluate the expression object - # - if exception: - # - # Here, we try to catch the exception - # - try: - tmp = obj(exception=True) - if tmp is None: - raise ValueError( - "No value for uninitialized NumericValue object %s" % (obj.name,) - ) - return tmp - except TemplateExpressionError: - # Template expressions work by catching this error type. So - # we should defer this error handling and not log an error - # message. - raise - except: - logger.error( - "evaluating object as numeric value: %s\n (object: %s)\n%s" - % (obj, type(obj), sys.exc_info()[1]) - ) - raise - else: - # - # Here, we do not try to catch the exception - # - return obj(exception=False) - - def is_constant(obj): """ A utility function that returns a boolean that indicates @@ -490,54 +405,6 @@ def as_numeric(obj): ) -def check_if_numeric_type(obj): - """Test if the argument behaves like a numeric type. - - We check for "numeric types" by checking if we can add zero to it - without changing the object's type. If that works, then we register - the type in native_numeric_types. - - """ - obj_class = obj.__class__ - # Do not re-evaluate known native types - if obj_class in native_types: - return obj_class in native_numeric_types - - try: - obj_plus_0 = obj + 0 - obj_p0_class = obj_plus_0.__class__ - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) - if not ((obj < 0) ^ (obj >= 0)): - return False - # Native types *must* be hashable - hash(obj) - except: - return False - if obj_p0_class is obj_class or obj_p0_class in native_numeric_types: - # - # If we get here, this is a reasonably well-behaving - # numeric type: add it to the native numeric types - # so that future lookups will be faster. - # - _numeric_types.RegisterNumericType(obj_class) - # - # Generate a warning, since Pyomo's management of third-party - # numeric types is more robust when registering explicitly. - # - logger.warning( - f"""Dynamically registering the following numeric type: - {obj_class.__module__}.{obj_class.__name__} -Dynamic registration is supported for convenience, but there are known -limitations to this approach. We recommend explicitly registering -numeric types using RegisterNumericType() or RegisterIntegerType().""" - ) - return True - else: - return False - - @deprecated( "check_if_numeric_type_and_cache() has been deprecated in " "favor of just calling as_numeric()", diff --git a/pyomo/core/expr/relational_expr.py b/pyomo/core/expr/relational_expr.py index 89bcdcc0411..2909be95c5a 100644 --- a/pyomo/core/expr/relational_expr.py +++ b/pyomo/core/expr/relational_expr.py @@ -14,17 +14,16 @@ from pyomo.common.deprecation import deprecated from pyomo.common.errors import PyomoException, DeveloperError +from pyomo.common.numeric_types import ( + native_numeric_types, + check_if_numeric_type, + value, +) from .base import ExpressionBase from .boolean_value import BooleanValue from .expr_common import _lt, _le, _eq, ExpressionType -from .numvalue import ( - native_numeric_types, - is_potentially_variable, - is_constant, - value, - check_if_numeric_type, -) +from .numvalue import is_potentially_variable, is_constant from .visitor import polynomial_degree # ------------------------------------------------------- diff --git a/pyomo/dae/contset.py b/pyomo/dae/contset.py index f9d052dbf9b..ee4c9f79e89 100644 --- a/pyomo/dae/contset.py +++ b/pyomo/dae/contset.py @@ -11,10 +11,10 @@ import logging import bisect +from pyomo.common.numeric_types import native_numeric_types from pyomo.common.timing import ConstructionTimer from pyomo.core.base.set import SortedScalarSet from pyomo.core.base.component import ModelComponentFactory -from pyomo.core.base.numvalue import native_numeric_types logger = logging.getLogger('pyomo.dae') __all__ = ['ContinuousSet'] diff --git a/pyomo/network/port.py b/pyomo/network/port.py index e68ee4a7930..dfe9cd54441 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -20,6 +20,7 @@ from pyomo.common.formatting import tabular_writer from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name, NOTSET +from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer from pyomo.core.base.var import Var @@ -28,7 +29,7 @@ from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set from pyomo.core.base.misc import apply_indexed_rule -from pyomo.core.base.numvalue import as_numeric, value +from pyomo.core.expr.numvalue import as_numeric from pyomo.core.expr.current import identify_variables from pyomo.core.base.label import alphanum_label_from_name diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 76a344ecb45..ff2d6857bd6 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -21,9 +21,10 @@ from weakref import ref as weakref_ref from pyomo.common.log import is_debug_set +from pyomo.common.numeric_types import value +from pyomo.core.expr.numvalue import is_fixed, ZeroConstant from pyomo.core.base.set_types import Any from pyomo.core.base import SortComponents, Var -from pyomo.core.base.numvalue import is_fixed, value, ZeroConstant from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import ( Constraint, diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 66585a13869..5b3dedd4462 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -18,15 +18,16 @@ import logging import itertools +from pyomo.common.numeric_types import native_numeric_types from pyomo.core.base import Constraint, Objective, ComponentMap from pyomo.core.expr import current as EXPR +from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective from pyomo.core.base import _ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData -from pyomo.core.base.numvalue import NumericConstant, native_numeric_types from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable from pyomo.core.kernel.objective import objective From 374a1cd5830c3a81e0f375c8effb8b7c64745676 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 16:00:25 -0600 Subject: [PATCH 03/15] Move NumericValue, NumericNDArray to expr.numeric_expr --- pyomo/core/base/indexed_component.py | 3 +- pyomo/core/expr/__init__.py | 16 +- pyomo/core/expr/numeric_expr.py | 497 ++++++++++++++++++++ pyomo/core/expr/numvalue.py | 548 +---------------------- pyomo/core/tests/unit/test_numpy_expr.py | 3 +- 5 files changed, 518 insertions(+), 549 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 0e063f14e4c..2afbc727303 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -20,7 +20,8 @@ from copy import deepcopy from pyomo.core.expr import current as EXPR -from pyomo.core.expr.numvalue import native_types, NumericNDArray +from pyomo.core.expr.numeric_expr import NumericNDArray +from pyomo.core.expr.numvalue import native_types from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.initializer import Initializer from pyomo.core.base.component import Component, ActiveComponent diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 634f6f0b0f1..b1bba0615cd 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -27,22 +27,12 @@ current, ) -# FIXME: remove circular dependencies between numvalue and numeric_expr # -# We unfortunately have circular dependencies between the numvalue -# module (which defines the base class for all numeric expression -# components, and implements the operator overloading methods) and the -# numeric_expr module (the dispatchers and the expression node -# definitions) -numvalue._add_dispatcher = numeric_expr._add_dispatcher -numvalue._neg_dispatcher = numeric_expr._neg_dispatcher -numvalue._mul_dispatcher = numeric_expr._mul_dispatcher -numvalue._div_dispatcher = numeric_expr._div_dispatcher -numvalue._abs_dispatcher = numeric_expr._abs_dispatcher -numvalue._pow_dispatcher = numeric_expr._pow_dispatcher +# FIXME: remove circular dependencies between relational_expr and numeric_expr +# # Initialize numvalue functions -numvalue._generate_relational_expression = ( +numeric_expr._generate_relational_expression = ( relational_expr._generate_relational_expression ) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 9bb089e7337..8f0e21e9241 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -45,6 +45,7 @@ value, is_potentially_variable, check_if_numeric_type, + value, ) from .visitor import ( @@ -59,6 +60,11 @@ _zero_one_optimizations = {1} +# Stub in the dispatchers +def _generate_relational_expression(etype, lhs, rhs): + raise RuntimeError("incomplete import of Pyomo expression system") + + def enable_expression_optimizations(zero=None, one=None): """Enable(disable) expression generation optimizations @@ -164,6 +170,497 @@ class linear_expression(mutable_expression): """ +class NumericValue(PyomoObject): + """ + This is the base class for numeric values used in Pyomo. + """ + + __slots__ = () + + # This is required because we define __eq__ + __hash__ = None + + def getname(self, fully_qualified=False, name_buffer=None): + """ + If this is a component, return the component's name on the owning + block; otherwise return the value converted to a string + """ + _base = super(NumericValue, self) + if hasattr(_base, 'getname'): + return _base.getname(fully_qualified, name_buffer) + else: + return str(type(self)) + + @property + def name(self): + return self.getname(fully_qualified=True) + + @property + def local_name(self): + return self.getname(fully_qualified=False) + + def is_numeric_type(self): + """Return True if this class is a Pyomo numeric object""" + return True + + def is_constant(self): + """Return True if this numeric value is a constant value""" + return False + + def is_fixed(self): + """Return True if this is a non-constant value that has been fixed""" + return False + + def is_potentially_variable(self): + """Return True if variables can appear in this expression""" + return False + + @deprecated( + "is_relational() is deprecated in favor of " + "is_expression_type(ExpressionType.RELATIONAL)", + version='6.4.3', + ) + def is_relational(self): + """ + Return True if this numeric value represents a relational expression. + """ + return False + + def is_indexed(self): + """Return True if this numeric value is an indexed object""" + return False + + def polynomial_degree(self): + """ + Return the polynomial degree of the expression. + + Returns: + :const:`None` + """ + return self._compute_polynomial_degree(None) + + def _compute_polynomial_degree(self, values): + """ + Compute the polynomial degree of this expression given + the degree values of its children. + + Args: + values (list): A list of values that indicate the degree + of the children expression. + + Returns: + :const:`None` + """ + return None + + def __bool__(self): + """Coerce the value to a bool + + Numeric values can be coerced to bool only if the value / + expression is constant. Fixed (but non-constant) or variable + values will raise an exception. + + Raises: + PyomoException + + """ + # Note that we want to implement __bool__, as scalar numeric + # components (e.g., Param, Var) implement __len__ (since they + # are implicit containers), and Python falls back on __len__ if + # __bool__ is not defined. + if self.is_constant(): + return bool(self()) + raise PyomoException( + """ +Cannot convert non-constant Pyomo numeric value (%s) to bool. +This error is usually caused by using a Var, unit, or mutable Param in a +Boolean context such as an "if" statement. For example, + >>> m.x = Var() + >>> if not m.x: + ... pass +would cause this exception.""".strip() + % (self,) + ) + + def __float__(self): + """Coerce the value to a floating point + + Numeric values can be coerced to float only if the value / + expression is constant. Fixed (but non-constant) or variable + values will raise an exception. + + Raises: + TypeError + + """ + if self.is_constant(): + return float(self()) + raise TypeError( + """ +Implicit conversion of Pyomo numeric value (%s) to float is disabled. +This error is often the result of using Pyomo components as arguments to +one of the Python built-in math module functions when defining +expressions. Avoid this error by using Pyomo-provided math functions or +explicitly resolving the numeric value using the Pyomo value() function. +""".strip() + % (self,) + ) + + def __int__(self): + """Coerce the value to an integer + + Numeric values can be coerced to int only if the value / + expression is constant. Fixed (but non-constant) or variable + values will raise an exception. + + Raises: + TypeError + + """ + if self.is_constant(): + return int(self()) + raise TypeError( + """ +Implicit conversion of Pyomo numeric value (%s) to int is disabled. +This error is often the result of using Pyomo components as arguments to +one of the Python built-in math module functions when defining +expressions. Avoid this error by using Pyomo-provided math functions or +explicitly resolving the numeric value using the Pyomo value() function. +""".strip() + % (self,) + ) + + def __lt__(self, other): + """ + Less than operator + + This method is called when Python processes statements of the form:: + + self < other + other > self + """ + return _generate_relational_expression(_lt, self, other) + + def __gt__(self, other): + """ + Greater than operator + + This method is called when Python processes statements of the form:: + + self > other + other < self + """ + return _generate_relational_expression(_lt, other, self) + + def __le__(self, other): + """ + Less than or equal operator + + This method is called when Python processes statements of the form:: + + self <= other + other >= self + """ + return _generate_relational_expression(_le, self, other) + + def __ge__(self, other): + """ + Greater than or equal operator + + This method is called when Python processes statements of the form:: + + self >= other + other <= self + """ + return _generate_relational_expression(_le, other, self) + + def __eq__(self, other): + """ + Equal to operator + + This method is called when Python processes the statement:: + + self == other + """ + return _generate_relational_expression(_eq, self, other) + + def __add__(self, other): + """ + Binary addition + + This method is called when Python processes the statement:: + + self + other + """ + return _add_dispatcher[self.__class__, other.__class__](self, other) + + def __sub__(self, other): + """ + Binary subtraction + + This method is called when Python processes the statement:: + + self - other + """ + return self.__add__(-other) + + def __mul__(self, other): + """ + Binary multiplication + + This method is called when Python processes the statement:: + + self * other + """ + return _mul_dispatcher[self.__class__, other.__class__](self, other) + + def __div__(self, other): + """ + Binary division + + This method is called when Python processes the statement:: + + self / other + """ + return _div_dispatcher[self.__class__, other.__class__](self, other) + + def __truediv__(self, other): + """ + Binary division (when __future__.division is in effect) + + This method is called when Python processes the statement:: + + self / other + """ + return _div_dispatcher[self.__class__, other.__class__](self, other) + + def __pow__(self, other): + """ + Binary power + + This method is called when Python processes the statement:: + + self ** other + """ + return _pow_dispatcher[self.__class__, other.__class__](self, other) + + def __radd__(self, other): + """ + Binary addition + + This method is called when Python processes the statement:: + + other + self + """ + return _add_dispatcher[other.__class__, self.__class__](other, self) + + def __rsub__(self, other): + """ + Binary subtraction + + This method is called when Python processes the statement:: + + other - self + """ + return other + (-self) + + def __rmul__(self, other): + """ + Binary multiplication + + This method is called when Python processes the statement:: + + other * self + + when other is not a :class:`NumericValue ` object. + """ + return _mul_dispatcher[other.__class__, self.__class__](other, self) + + def __rdiv__(self, other): + """Binary division + + This method is called when Python processes the statement:: + + other / self + """ + return _div_dispatcher[other.__class__, self.__class__](other, self) + + def __rtruediv__(self, other): + """ + Binary division (when __future__.division is in effect) + + This method is called when Python processes the statement:: + + other / self + """ + return _div_dispatcher[other.__class__, self.__class__](other, self) + + def __rpow__(self, other): + """ + Binary power + + This method is called when Python processes the statement:: + + other ** self + """ + return _pow_dispatcher[other.__class__, self.__class__](other, self) + + def __iadd__(self, other): + """ + Binary addition + + This method is called when Python processes the statement:: + + self += other + """ + return _add_dispatcher[self.__class__, other.__class__](self, other) + + def __isub__(self, other): + """ + Binary subtraction + + This method is called when Python processes the statement:: + + self -= other + """ + return self.__iadd__(-other) + + def __imul__(self, other): + """ + Binary multiplication + + This method is called when Python processes the statement:: + + self *= other + """ + return _mul_dispatcher[self.__class__, other.__class__](self, other) + + def __idiv__(self, other): + """ + Binary division + + This method is called when Python processes the statement:: + + self /= other + """ + return _div_dispatcher[self.__class__, other.__class__](self, other) + + def __itruediv__(self, other): + """ + Binary division (when __future__.division is in effect) + + This method is called when Python processes the statement:: + + self /= other + """ + return _div_dispatcher[self.__class__, other.__class__](self, other) + + def __ipow__(self, other): + """ + Binary power + + This method is called when Python processes the statement:: + + self **= other + """ + return _pow_dispatcher[self.__class__, other.__class__](self, other) + + def __neg__(self): + """ + Negation + + This method is called when Python processes the statement:: + + - self + """ + return _neg_dispatcher[self.__class__](self) + + def __pos__(self): + """ + Positive expression + + This method is called when Python processes the statement:: + + + self + """ + return self + + def __abs__(self): + """Absolute value + + This method is called when Python processes the statement:: + + abs(self) + """ + return _abs_dispatcher[self.__class__](self) + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + return NumericNDArray.__array_ufunc__(None, ufunc, method, *inputs, **kwargs) + + def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False): + """Return a string representation of the expression tree. + + Args: + verbose (bool): If :const:`True`, then the string + representation consists of nested functions. Otherwise, + the string representation is an infix algebraic equation. + Defaults to :const:`False`. + labeler: An object that generates string labels for + non-constant in the expression tree. Defaults to + :const:`None`. + smap: A SymbolMap instance that stores string labels for + non-constant nodes in the expression tree. Defaults to + :const:`None`. + compute_values (bool): If :const:`True`, then fixed + expressions are evaluated and the string representation + of the resulting value is returned. + + Returns: + A string representation for the expression tree. + + """ + if compute_values and self.is_fixed(): + try: + return str(self()) + except: + pass + if not self.is_constant(): + if smap is not None: + return smap.getSymbol(self, labeler) + elif labeler is not None: + return labeler(self) + return str(self) + + +# +# Note: the "if numpy_available" in the class definition also ensures +# that the numpy types are registered if numpy is in fact available +# +# TODO: Move this to a separate module to support avoiding the numpy +# import if numpy is not actually used. +class NumericNDArray(np.ndarray if numpy_available else object): + """An ndarray subclass that stores Pyomo numeric expressions""" + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method == '__call__': + # Convert all incoming types to ndarray (to prevent recursion) + args = [np.asarray(i) for i in inputs] + # Set the return type to be an 'object'. This prevents the + # logical operators from casting the result to a bool. This + # requires numpy >= 1.6 + kwargs['dtype'] = object + + # Delegate to the base ufunc, but return an instance of this + # class so that additional operators hit this method. + ans = getattr(ufunc, method)(*args, **kwargs) + if isinstance(ans, np.ndarray): + if ans.size == 1: + return ans[0] + return ans.view(NumericNDArray) + else: + return ans + + # ------------------------------------------------------- # # Expression classes diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 75af76a9bda..ba008475b86 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -27,36 +27,13 @@ import sys import logging -from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.deprecation import ( deprecated, deprecation_warning, relocated_module_attribute, ) -from pyomo.common.errors import PyomoException -from pyomo.core.expr.expr_common import ( - _add, - _sub, - _mul, - _div, - _pow, - _neg, - _abs, - _radd, - _rsub, - _rmul, - _rdiv, - _rpow, - _iadd, - _isub, - _imul, - _idiv, - _ipow, - _lt, - _le, - _eq, - ExpressionType, -) +from pyomo.core.expr.expr_common import ExpressionType +from pyomo.core.expr.numeric_expr import NumericValue import pyomo.common.numeric_types as _numeric_types # TODO: update Pyomo to import these objects from common.numeric_types @@ -72,7 +49,6 @@ value, ) from pyomo.core.pyomoobject import PyomoObject -from pyomo.core.expr.expr_errors import TemplateExpressionError relocated_module_attribute( 'native_boolean_types', @@ -102,27 +78,22 @@ version='6.6.0', f_globals=globals(), ) +relocated_module_attribute( + 'NumericValue', + 'pyomo.core.expr.numeric_expr.NumericValue', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'NumericNDArray', + 'pyomo.core.expr.numeric_expr.NumericNDArray', + version='6.6.2.dev0', + f_globals=globals(), +) logger = logging.getLogger('pyomo.core') -# Stub in the dispatchers -def _incomplete_import(*args): - raise RuntimeError("incomplete import of Pyomo expression system") - - -_add_dispatcher = collections.defaultdict(_incomplete_import) -_mul_dispatcher = collections.defaultdict(_incomplete_import) -_div_dispatcher = collections.defaultdict(_incomplete_import) -_pow_dispatcher = collections.defaultdict(_incomplete_import) -_neg_dispatcher = collections.defaultdict(_incomplete_import) -_abs_dispatcher = collections.defaultdict(_incomplete_import) - - -def _generate_relational_expression(etype, lhs, rhs): - raise RuntimeError("incomplete import of Pyomo expression system") - - ##------------------------------------------------------------------------ ## ## Standard types of expressions @@ -422,468 +393,6 @@ def check_if_numeric_type_and_cache(obj): return obj -class NumericValue(PyomoObject): - """ - This is the base class for numeric values used in Pyomo. - """ - - __slots__ = () - - # This is required because we define __eq__ - __hash__ = None - - def getname(self, fully_qualified=False, name_buffer=None): - """ - If this is a component, return the component's name on the owning - block; otherwise return the value converted to a string - """ - _base = super(NumericValue, self) - if hasattr(_base, 'getname'): - return _base.getname(fully_qualified, name_buffer) - else: - return str(type(self)) - - @property - def name(self): - return self.getname(fully_qualified=True) - - @property - def local_name(self): - return self.getname(fully_qualified=False) - - def is_numeric_type(self): - """Return True if this class is a Pyomo numeric object""" - return True - - def is_constant(self): - """Return True if this numeric value is a constant value""" - return False - - def is_fixed(self): - """Return True if this is a non-constant value that has been fixed""" - return False - - def is_potentially_variable(self): - """Return True if variables can appear in this expression""" - return False - - @deprecated( - "is_relational() is deprecated in favor of " - "is_expression_type(ExpressionType.RELATIONAL)", - version='6.4.3', - ) - def is_relational(self): - """ - Return True if this numeric value represents a relational expression. - """ - return False - - def is_indexed(self): - """Return True if this numeric value is an indexed object""" - return False - - def polynomial_degree(self): - """ - Return the polynomial degree of the expression. - - Returns: - :const:`None` - """ - return self._compute_polynomial_degree(None) - - def _compute_polynomial_degree(self, values): - """ - Compute the polynomial degree of this expression given - the degree values of its children. - - Args: - values (list): A list of values that indicate the degree - of the children expression. - - Returns: - :const:`None` - """ - return None - - def __bool__(self): - """Coerce the value to a bool - - Numeric values can be coerced to bool only if the value / - expression is constant. Fixed (but non-constant) or variable - values will raise an exception. - - Raises: - PyomoException - - """ - # Note that we want to implement __bool__, as scalar numeric - # components (e.g., Param, Var) implement __len__ (since they - # are implicit containers), and Python falls back on __len__ if - # __bool__ is not defined. - if self.is_constant(): - return bool(self()) - raise PyomoException( - """ -Cannot convert non-constant Pyomo numeric value (%s) to bool. -This error is usually caused by using a Var, unit, or mutable Param in a -Boolean context such as an "if" statement. For example, - >>> m.x = Var() - >>> if not m.x: - ... pass -would cause this exception.""".strip() - % (self,) - ) - - def __float__(self): - """Coerce the value to a floating point - - Numeric values can be coerced to float only if the value / - expression is constant. Fixed (but non-constant) or variable - values will raise an exception. - - Raises: - TypeError - - """ - if self.is_constant(): - return float(self()) - raise TypeError( - """ -Implicit conversion of Pyomo numeric value (%s) to float is disabled. -This error is often the result of using Pyomo components as arguments to -one of the Python built-in math module functions when defining -expressions. Avoid this error by using Pyomo-provided math functions or -explicitly resolving the numeric value using the Pyomo value() function. -""".strip() - % (self,) - ) - - def __int__(self): - """Coerce the value to an integer - - Numeric values can be coerced to int only if the value / - expression is constant. Fixed (but non-constant) or variable - values will raise an exception. - - Raises: - TypeError - - """ - if self.is_constant(): - return int(self()) - raise TypeError( - """ -Implicit conversion of Pyomo numeric value (%s) to int is disabled. -This error is often the result of using Pyomo components as arguments to -one of the Python built-in math module functions when defining -expressions. Avoid this error by using Pyomo-provided math functions or -explicitly resolving the numeric value using the Pyomo value() function. -""".strip() - % (self,) - ) - - def __lt__(self, other): - """ - Less than operator - - This method is called when Python processes statements of the form:: - - self < other - other > self - """ - return _generate_relational_expression(_lt, self, other) - - def __gt__(self, other): - """ - Greater than operator - - This method is called when Python processes statements of the form:: - - self > other - other < self - """ - return _generate_relational_expression(_lt, other, self) - - def __le__(self, other): - """ - Less than or equal operator - - This method is called when Python processes statements of the form:: - - self <= other - other >= self - """ - return _generate_relational_expression(_le, self, other) - - def __ge__(self, other): - """ - Greater than or equal operator - - This method is called when Python processes statements of the form:: - - self >= other - other <= self - """ - return _generate_relational_expression(_le, other, self) - - def __eq__(self, other): - """ - Equal to operator - - This method is called when Python processes the statement:: - - self == other - """ - return _generate_relational_expression(_eq, self, other) - - def __add__(self, other): - """ - Binary addition - - This method is called when Python processes the statement:: - - self + other - """ - return _add_dispatcher[self.__class__, other.__class__](self, other) - - def __sub__(self, other): - """ - Binary subtraction - - This method is called when Python processes the statement:: - - self - other - """ - return self.__add__(-other) - - def __mul__(self, other): - """ - Binary multiplication - - This method is called when Python processes the statement:: - - self * other - """ - return _mul_dispatcher[self.__class__, other.__class__](self, other) - - def __div__(self, other): - """ - Binary division - - This method is called when Python processes the statement:: - - self / other - """ - return _div_dispatcher[self.__class__, other.__class__](self, other) - - def __truediv__(self, other): - """ - Binary division (when __future__.division is in effect) - - This method is called when Python processes the statement:: - - self / other - """ - return _div_dispatcher[self.__class__, other.__class__](self, other) - - def __pow__(self, other): - """ - Binary power - - This method is called when Python processes the statement:: - - self ** other - """ - return _pow_dispatcher[self.__class__, other.__class__](self, other) - - def __radd__(self, other): - """ - Binary addition - - This method is called when Python processes the statement:: - - other + self - """ - return _add_dispatcher[other.__class__, self.__class__](other, self) - - def __rsub__(self, other): - """ - Binary subtraction - - This method is called when Python processes the statement:: - - other - self - """ - return other + (-self) - - def __rmul__(self, other): - """ - Binary multiplication - - This method is called when Python processes the statement:: - - other * self - - when other is not a :class:`NumericValue ` object. - """ - return _mul_dispatcher[other.__class__, self.__class__](other, self) - - def __rdiv__(self, other): - """Binary division - - This method is called when Python processes the statement:: - - other / self - """ - return _div_dispatcher[other.__class__, self.__class__](other, self) - - def __rtruediv__(self, other): - """ - Binary division (when __future__.division is in effect) - - This method is called when Python processes the statement:: - - other / self - """ - return _div_dispatcher[other.__class__, self.__class__](other, self) - - def __rpow__(self, other): - """ - Binary power - - This method is called when Python processes the statement:: - - other ** self - """ - return _pow_dispatcher[other.__class__, self.__class__](other, self) - - def __iadd__(self, other): - """ - Binary addition - - This method is called when Python processes the statement:: - - self += other - """ - return _add_dispatcher[self.__class__, other.__class__](self, other) - - def __isub__(self, other): - """ - Binary subtraction - - This method is called when Python processes the statement:: - - self -= other - """ - return self.__iadd__(-other) - - def __imul__(self, other): - """ - Binary multiplication - - This method is called when Python processes the statement:: - - self *= other - """ - return _mul_dispatcher[self.__class__, other.__class__](self, other) - - def __idiv__(self, other): - """ - Binary division - - This method is called when Python processes the statement:: - - self /= other - """ - return _div_dispatcher[self.__class__, other.__class__](self, other) - - def __itruediv__(self, other): - """ - Binary division (when __future__.division is in effect) - - This method is called when Python processes the statement:: - - self /= other - """ - return _div_dispatcher[self.__class__, other.__class__](self, other) - - def __ipow__(self, other): - """ - Binary power - - This method is called when Python processes the statement:: - - self **= other - """ - return _pow_dispatcher[self.__class__, other.__class__](self, other) - - def __neg__(self): - """ - Negation - - This method is called when Python processes the statement:: - - - self - """ - return _neg_dispatcher[self.__class__](self) - - def __pos__(self): - """ - Positive expression - - This method is called when Python processes the statement:: - - + self - """ - return self - - def __abs__(self): - """Absolute value - - This method is called when Python processes the statement:: - - abs(self) - """ - return _abs_dispatcher[self.__class__](self) - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - return NumericNDArray.__array_ufunc__(None, ufunc, method, *inputs, **kwargs) - - def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False): - """Return a string representation of the expression tree. - - Args: - verbose (bool): If :const:`True`, then the string - representation consists of nested functions. Otherwise, - the string representation is an infix algebraic equation. - Defaults to :const:`False`. - labeler: An object that generates string labels for - non-constant in the expression tree. Defaults to - :const:`None`. - smap: A SymbolMap instance that stores string labels for - non-constant nodes in the expression tree. Defaults to - :const:`None`. - compute_values (bool): If :const:`True`, then fixed - expressions are evaluated and the string representation - of the resulting value is returned. - - Returns: - A string representation for the expression tree. - - """ - if compute_values and self.is_fixed(): - try: - return str(self()) - except: - pass - if not self.is_constant(): - if smap is not None: - return smap.getSymbol(self, labeler) - elif labeler is not None: - return labeler(self) - return str(self) - - class NumericConstant(NumericValue): """An object that contains a constant numeric value. @@ -922,32 +431,3 @@ def pprint(self, ostream=None, verbose=False): # We use as_numeric() so that the constant is also in the cache ZeroConstant = as_numeric(0) - - -# -# Note: the "if numpy_available" in the class definition also ensures -# that the numpy types are registered if numpy is in fact available -# - - -class NumericNDArray(np.ndarray if numpy_available else object): - """An ndarray subclass that stores Pyomo numeric expressions""" - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - if method == '__call__': - # Convert all incoming types to ndarray (to prevent recursion) - args = [np.asarray(i) for i in inputs] - # Set the return type to be an 'object'. This prevents the - # logical operators from casting the result to a bool. This - # requires numpy >= 1.6 - kwargs['dtype'] = object - - # Delegate to the base ufunc, but return an instance of this - # class so that additional operators hit this method. - ans = getattr(ufunc, method)(*args, **kwargs) - if isinstance(ans, np.ndarray): - if ans.size == 1: - return ans[0] - return ans.view(NumericNDArray) - else: - return ans diff --git a/pyomo/core/tests/unit/test_numpy_expr.py b/pyomo/core/tests/unit/test_numpy_expr.py index 60ccda5fd57..e9afdc9b2a8 100644 --- a/pyomo/core/tests/unit/test_numpy_expr.py +++ b/pyomo/core/tests/unit/test_numpy_expr.py @@ -29,7 +29,8 @@ Reals, ) from pyomo.core.expr.current import MonomialTermExpression -from pyomo.core.expr.numvalue import NumericNDArray, as_numeric +from pyomo.core.expr.numeric_expr import NumericNDArray +from pyomo.core.expr.numvalue import as_numeric from pyomo.core.expr.compare import compare_expressions from pyomo.core.expr.relational_expr import InequalityExpression From 1d3bc9313c0eb77d17b0124200369a44b151c5af Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 16:06:12 -0600 Subject: [PATCH 04/15] Break import loop by deferring the visitor import --- pyomo/core/expr/base.py | 26 +++-- pyomo/core/expr/current.py | 32 +++--- pyomo/core/expr/expr_common.py | 21 ---- pyomo/core/expr/numeric_expr.py | 97 +++++++++++++------ .../plugins/transform/radix_linearization.py | 2 +- pyomo/core/tests/unit/test_numeric_expr.py | 3 +- 6 files changed, 93 insertions(+), 88 deletions(-) diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index 2939250ce02..b74bbff4e3c 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -11,16 +11,12 @@ import enum +from pyomo.common.dependencies import attempt_import from pyomo.common.numeric_types import native_types from pyomo.core.pyomoobject import PyomoObject -from . import expr_common as common -from .visitor import ( - expression_to_string, - evaluate_expression, - clone_expression, - _expression_is_fixed, - sizeof_expression, -) +from pyomo.core.expr.expr_common import OperatorAssociativity + +visitor, _ = attempt_import('pyomo.core.expr.visitor') class ExpressionBase(PyomoObject): @@ -44,7 +40,7 @@ class in with their fundamental base data type (NumericValue, interpreted as "not associative" (implying any arguments that are at this operator's PRECEDENCE will be enclosed in parens). """ - ASSOCIATIVITY = common.OperatorAssociativity.LEFT_TO_RIGHT + ASSOCIATIVITY = OperatorAssociativity.LEFT_TO_RIGHT def nargs(self): """Returns the number of child nodes. @@ -119,7 +115,7 @@ def __call__(self, exception=True): The value of the expression or :const:`None`. """ - return evaluate_expression(self, exception) + return visitor.evaluate_expression(self, exception) def __str__(self): """Returns a string description of the expression. @@ -137,7 +133,7 @@ def __str__(self): ------- str """ - return expression_to_string(self) + return visitor.expression_to_string(self) def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False): """Return a string representation of the expression tree. @@ -168,7 +164,7 @@ def to_string(self, verbose=None, labeler=None, smap=None, compute_values=False) A string representation for the expression tree. """ - return expression_to_string( + return visitor.expression_to_string( self, verbose=verbose, labeler=labeler, @@ -240,7 +236,7 @@ def clone(self, substitute=None): Returns: A new expression tree. """ - return clone_expression(self, substitute=substitute) + return visitor.clone_expression(self, substitute=substitute) def create_node_with_local_data(self, args, classtype=None): """ @@ -287,7 +283,7 @@ def is_fixed(self): Returns: A boolean. """ - return _expression_is_fixed(self) + return visitor._expression_is_fixed(self) def _is_fixed(self, values): """ @@ -362,7 +358,7 @@ def size(self): A nonnegative integer that is the number of interior and leaf nodes in the expression tree. """ - return sizeof_expression(self) + return visitor.sizeof_expression(self) def _apply_operation(self, result): # pragma: no cover """ diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 1eb3f8a35b9..2d7cce59b50 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -51,33 +51,25 @@ class Mode(enum.IntEnum): # from pyomo.core.expr import numvalue as _numvalue from pyomo.core.expr import boolean_value as _logicalvalue - from pyomo.core.expr import numeric_expr as _numeric_expr -from .base import ExpressionBase + +from pyomo.core.expr.expr_common import clone_counter +from pyomo.core.expr.base import ExpressionBase +from pyomo.core.expr.visitor import ( + evaluate_expression, + expression_to_string, + polynomial_degree, + clone_expression, + sizeof_expression, + _expression_is_fixed, +) from pyomo.core.expr.numeric_expr import ( - _add, - _sub, - _mul, - _div, - _pow, - _neg, - _abs, - _inplace, - _unary, NumericExpression, NumericValue, native_types, nonpyomo_leaf_types, native_numeric_types, - as_numeric, value, - evaluate_expression, - expression_to_string, - polynomial_degree, - clone_expression, - sizeof_expression, - _expression_is_fixed, - clone_counter, nonlinear_expression, linear_expression, NegationExpression, @@ -110,6 +102,7 @@ class Mode(enum.IntEnum): NPV_expression_types, _fcn_dispatcher, ) +from pyomo.core.expr.numvalue import as_numeric from pyomo.core.expr import logical_expr as _logical_expr from pyomo.core.expr.logical_expr import ( native_logical_types, @@ -201,7 +194,6 @@ class Mode(enum.IntEnum): _EvaluateConstantExpressionVisitor, _ComponentVisitor, identify_components, - _VariableVisitor, identify_variables, _MutableParamVisitor, identify_mutable_parameters, diff --git a/pyomo/core/expr/expr_common.py b/pyomo/core/expr/expr_common.py index 6292a5c08f8..e5430158244 100644 --- a/pyomo/core/expr/expr_common.py +++ b/pyomo/core/expr/expr_common.py @@ -16,27 +16,6 @@ TO_STRING_VERBOSE = False -_add = 1 -_sub = 2 -_mul = 3 -_div = 4 -_pow = 5 -_neg = 6 -_abs = 7 -_inplace = 10 -_unary = _neg - -_radd = -_add -_iadd = _inplace + _add -_rsub = -_sub -_isub = _inplace + _sub -_rmul = -_mul -_imul = _inplace + _mul -_rdiv = -_div -_idiv = _inplace + _div -_rpow = -_pow -_ipow = _inplace + _pow - _eq = 0 _le = 1 _lt = 2 diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 8f0e21e9241..d68f086e24f 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -18,43 +18,82 @@ logger = logging.getLogger('pyomo.core') from math import isclose -from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.formatting import tostr -from .expr_common import ( - OperatorAssociativity, - ExpressionType, - clone_counter, - _add, - _sub, - _mul, - _div, - _pow, - _neg, - _abs, - _inplace, - _unary, +from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.common.deprecation import ( + deprecated, + deprecation_warning, + relocated_module_attribute, ) -from .base import ExpressionBase, NPV_Mixin -from .numvalue import ( - NumericValue, +from pyomo.common.errors import PyomoException +from pyomo.common.formatting import tostr +from pyomo.common.numeric_types import ( native_types, nonpyomo_leaf_types, native_numeric_types, - as_numeric, - value, - is_potentially_variable, check_if_numeric_type, value, ) -from .visitor import ( - evaluate_expression, - expression_to_string, - polynomial_degree, - clone_expression, - sizeof_expression, - _expression_is_fixed, +from pyomo.core.pyomoobject import PyomoObject +from pyomo.core.expr.expr_common import ( + OperatorAssociativity, + ExpressionType, + _lt, + _le, + _eq, +) + +# Note: pyggyback on expr.base's use of attempt_import(visitor) +from pyomo.core.expr.base import ExpressionBase, NPV_Mixin, visitor + +relocated_module_attribute( + 'is_potentially_variable', + 'pyomo.core.expr.numvalue.is_potentially_variable', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'as_numeric', + 'pyomo.core.expr.numvalue.as_numeric', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'clone_counter', + 'pyomo.core.expr.expr_common.clone_counter', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'evaluate_expression', + 'pyomo.core.expr.visitor.evaluate_expression', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'expression_to_string', + 'pyomo.core.expr.visitor.expression_to_string', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'polynomial_degree', + 'pyomo.core.expr.visitor.polynomial_degree', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'clone_expression', + 'pyomo.core.expr.visitor.clone_expression', + version='6.6.2.dev0', + f_globals=globals(), +) +relocated_module_attribute( + 'sizeof_expression', + 'pyomo.core.expr.visitor.sizeof_expression', + version='6.6.2.dev0', + f_globals=globals(), ) _zero_one_optimizations = {1} @@ -751,7 +790,7 @@ def polynomial_degree(self): A non-negative integer that is the polynomial degree if the expression is polynomial, or :const:`None` otherwise. """ - return polynomial_degree(self) + return visitor.polynomial_degree(self) def _compute_polynomial_degree(self, values): """ diff --git a/pyomo/core/plugins/transform/radix_linearization.py b/pyomo/core/plugins/transform/radix_linearization.py index 697be203f2a..98f00573974 100644 --- a/pyomo/core/plugins/transform/radix_linearization.py +++ b/pyomo/core/plugins/transform/radix_linearization.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from pyomo.core.expr.current import ProductExpression, PowExpression +from pyomo.core.expr.numvalue import as_numeric from pyomo.core import Binary, value from pyomo.core.base import ( Transformation, @@ -20,7 +21,6 @@ Block, RangeSet, ) -from pyomo.core.base.numvalue import as_numeric from pyomo.core.base.var import _VarData import logging diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 156dba33214..808420e4ef6 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -67,7 +67,7 @@ ) from pyomo.kernel import variable, expression, objective -from pyomo.core.expr.expr_common import ExpressionType +from pyomo.core.expr.expr_common import ExpressionType, clone_counter from pyomo.core.expr.numvalue import ( NumericConstant, as_numeric, @@ -94,7 +94,6 @@ NPV_DivisionExpression, NPV_SumExpression, decompose_term, - clone_counter, nonlinear_expression, _MutableLinearExpression, _MutableSumExpression, From b6cdf40f40004c216144e3a4060ebf6a9de99602 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 16:07:16 -0600 Subject: [PATCH 05/15] Silence (unnecessary) test output to console --- .../tests/unit/kernel/test_dict_container.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pyomo/core/tests/unit/kernel/test_dict_container.py b/pyomo/core/tests/unit/kernel/test_dict_container.py index ea419136053..e6b6f8d7aab 100644 --- a/pyomo/core/tests/unit/kernel/test_dict_container.py +++ b/pyomo/core/tests/unit/kernel/test_dict_container.py @@ -16,6 +16,7 @@ import pyomo.common.unittest as unittest import pyomo.kernel as pmo from pyomo.common.log import LoggingIntercept +from pyomo.common.tee import capture_output from pyomo.core.kernel.base import ICategorizedObject, ICategorizedObjectContainer from pyomo.core.kernel.homogeneous_container import IHomogeneousContainer from pyomo.core.kernel.dict_container import DictContainer @@ -72,17 +73,18 @@ def test_pprint(self): # Not really testing what the output is, just that # an error does not occur. The pprint functionality # is still in the early stages. - cdict = self._container_type({None: self._ctype_factory()}) - pyomo.kernel.pprint(cdict) - b = block() - b.cdict = cdict - pyomo.kernel.pprint(cdict) - pyomo.kernel.pprint(b) - m = block() - m.b = b - pyomo.kernel.pprint(cdict) - pyomo.kernel.pprint(b) - pyomo.kernel.pprint(m) + with capture_output() as OUT: + cdict = self._container_type({None: self._ctype_factory()}) + pyomo.kernel.pprint(cdict) + b = block() + b.cdict = cdict + pyomo.kernel.pprint(cdict) + pyomo.kernel.pprint(b) + m = block() + m.b = b + pyomo.kernel.pprint(cdict) + pyomo.kernel.pprint(b) + pyomo.kernel.pprint(m) def test_ctype(self): c = self._container_type() @@ -863,7 +865,7 @@ def descend(x): return not x._is_heterogeneous_container descend.seen = [] - pmo.pprint(cdict) + # pmo.pprint(cdict) order = list(pmo.preorder_traversal(cdict, active=True, descend=descend)) self.assertEqual([None, '[0]', '[2]'], [c.name for c in order]) self.assertEqual( From 7577f475ce345e16b133932f7d6d6fde533a864b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 17:04:50 -0600 Subject: [PATCH 06/15] Remove private classes/functions from expr.current --- pyomo/core/expr/current.py | 26 -------------------------- pyomo/repn/plugins/baron_writer.py | 3 ++- pyomo/repn/plugins/gams_writer.py | 3 ++- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 2d7cce59b50..0a1ae866dcb 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -61,7 +61,6 @@ class Mode(enum.IntEnum): polynomial_degree, clone_expression, sizeof_expression, - _expression_is_fixed, ) from pyomo.core.expr.numeric_expr import ( NumericExpression, @@ -86,7 +85,6 @@ class Mode(enum.IntEnum): SumExpressionBase, NPV_SumExpression, SumExpression, - _MutableSumExpression, Expr_ifExpression, NPV_Expr_ifExpression, UnaryFunctionExpression, @@ -94,11 +92,8 @@ class Mode(enum.IntEnum): AbsExpression, NPV_AbsExpression, LinearExpression, - _MutableLinearExpression, decompose_term, LinearDecompositionError, - _decompose_linear_terms, - _balanced_parens, NPV_expression_types, _fcn_dispatcher, ) @@ -108,19 +103,11 @@ class Mode(enum.IntEnum): native_logical_types, BooleanValue, BooleanConstant, - _and, - _or, - _equiv, - _inv, - _xor, - _impl, - _generate_logical_proposition, BooleanExpressionBase, lnot, equivalent, xor, implies, - _flattened, land, lor, exactly, @@ -133,7 +120,6 @@ class Mode(enum.IntEnum): XorExpression, ImplicationExpression, NaryBooleanExpression, - _add_to_and_or_expression, AndExpression, OrExpression, ExactlyExpression, @@ -150,7 +136,6 @@ class Mode(enum.IntEnum): ) from pyomo.core.expr.template_expr import ( TemplateExpressionError, - _NotSpecified, GetItemExpression, Numeric_GetItemExpression, Boolean_GetItemExpression, @@ -166,17 +151,13 @@ class Mode(enum.IntEnum): NPV_Boolean_GetAttrExpression, NPV_Structural_GetAttrExpression, CallExpression, - _TemplateSumExpression_argList, TemplateSumExpression, IndexTemplate, resolve_template, ReplaceTemplateExpression, substitute_template_expression, - _GetItemIndexer, substitute_getitem_with_param, substitute_template_with_value, - _set_iterator_template_generator, - _template_iter_context, templatize_rule, templatize_constraint, ) @@ -188,18 +169,11 @@ class Mode(enum.IntEnum): ExpressionValueVisitor, replace_expressions, ExpressionReplacementVisitor, - _EvaluationVisitor, FixedExpressionError, NonConstantExpressionError, - _EvaluateConstantExpressionVisitor, - _ComponentVisitor, identify_components, identify_variables, - _MutableParamVisitor, identify_mutable_parameters, - _PolynomialDegreeVisitor, - _IsFixedVisitor, - _ToStringVisitor, ) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index 72802fbc6bc..63c5f44d4bd 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -28,6 +28,7 @@ native_types, nonpyomo_leaf_types, ) +from pyomo.core.expr.visitor import _ToStringVisitor from pyomo.core.expr import current as EXPR from pyomo.core.base import ( SortComponents, @@ -106,7 +107,7 @@ def _handle_AbsExpression(visitor, node, values): # A visitor pattern that creates a string for an expression # that is compatible with the BARON syntax. # -class ToBaronVisitor(EXPR._ToStringVisitor): +class ToBaronVisitor(_ToStringVisitor): _expression_handlers = { EXPR.PowExpression: _handle_PowExpression, EXPR.UnaryFunctionExpression: _handle_UnaryFunctionExpression, diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index d16d65d6c54..c7f4f2a0fde 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -24,6 +24,7 @@ native_numeric_types, nonpyomo_leaf_types, ) +from pyomo.core.expr.visitor import _ToStringVisitor from pyomo.core.base import ( SymbolMap, ShortNameLabeler, @@ -99,7 +100,7 @@ def _handle_AbsExpression(visitor, node, values): # A visitor pattern that creates a string for an expression # that is compatible with the GAMS syntax. # -class ToGamsVisitor(EXPR._ToStringVisitor): +class ToGamsVisitor(_ToStringVisitor): _expression_handlers = { EXPR.PowExpression: _handle_PowExpression, EXPR.UnaryFunctionExpression: _handle_UnaryFunctionExpression, From 769affeb0419662cdbabc5afde64d6d5da6aa1b3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 10 Jul 2023 20:22:10 -0600 Subject: [PATCH 07/15] Move numerif function defitions into expr.numeric_expr --- pyomo/core/expr/__init__.py | 30 ++++---- pyomo/core/expr/current.py | 111 +++++---------------------- pyomo/core/expr/numeric_expr.py | 130 +++++++++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 107 deletions(-) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index b1bba0615cd..0f0180b314f 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -54,20 +54,10 @@ from .boolean_value import BooleanValue -from .numeric_expr import linear_expression, nonlinear_expression, mutable_expression -from .logical_expr import ( - land, - lnot, - lor, - xor, - equivalent, - exactly, - atleast, - atmost, - implies, -) -from .relational_expr import inequality -from .current import ( +from .numeric_expr import ( + linear_expression, + nonlinear_expression, + mutable_expression, log, log10, sin, @@ -88,6 +78,18 @@ floor, Expr_if, ) +from .logical_expr import ( + land, + lnot, + lor, + xor, + equivalent, + exactly, + atleast, + atmost, + implies, +) +from .relational_expr import inequality from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 0a1ae866dcb..272d760c132 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -95,7 +95,25 @@ class Mode(enum.IntEnum): decompose_term, LinearDecompositionError, NPV_expression_types, - _fcn_dispatcher, + Expr_if, + ceil, + floor, + exp, + log, + log10, + sqrt, + sin, + cos, + tan, + sinh, + cosh, + tanh, + asin, + acos, + atan, + asinh, + acosh, + atanh, ) from pyomo.core.expr.numvalue import as_numeric from pyomo.core.expr import logical_expr as _logical_expr @@ -175,94 +193,3 @@ class Mode(enum.IntEnum): identify_variables, identify_mutable_parameters, ) - - -def Expr_if(IF=None, THEN=None, ELSE=None): - """ - Function used to construct a logical conditional expression. - """ - if _numvalue.is_constant(IF): - return THEN if value(IF) else ELSE - if not any(map(_numvalue.is_potentially_variable, (IF, THEN, ELSE))): - return NPV_Expr_ifExpression((IF, THEN, ELSE)) - return Expr_ifExpression((IF, THEN, ELSE)) - - -# -# NOTE: abs() and pow() are not defined here, because they are -# Python operators. -# -def ceil(arg): - return _fcn_dispatcher[arg.__class__](arg, 'ceil', math.ceil) - - -def floor(arg): - return _fcn_dispatcher[arg.__class__](arg, 'floor', math.floor) - - -# e ** x -def exp(arg): - return _fcn_dispatcher[arg.__class__](arg, 'exp', math.exp) - - -def log(arg): - return _fcn_dispatcher[arg.__class__](arg, 'log', math.log) - - -def log10(arg): - return _fcn_dispatcher[arg.__class__](arg, 'log10', math.log10) - - -# FIXME: this is nominally the same as x ** 0.5, but follows a different -# path and produces a different NL file! -def sqrt(arg): - return _fcn_dispatcher[arg.__class__](arg, 'sqrt', math.sqrt) - # return _pow_dispatcher[arg.__class__, float](arg, 0.5) - - -def sin(arg): - return _fcn_dispatcher[arg.__class__](arg, 'sin', math.sin) - - -def cos(arg): - return _fcn_dispatcher[arg.__class__](arg, 'cos', math.cos) - - -def tan(arg): - return _fcn_dispatcher[arg.__class__](arg, 'tan', math.tan) - - -def sinh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'sinh', math.sinh) - - -def cosh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'cosh', math.cosh) - - -def tanh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'tanh', math.tanh) - - -def asin(arg): - return _fcn_dispatcher[arg.__class__](arg, 'asin', math.asin) - - -def acos(arg): - return _fcn_dispatcher[arg.__class__](arg, 'acos', math.acos) - - -def atan(arg): - return _fcn_dispatcher[arg.__class__](arg, 'atan', math.atan) - - -def asinh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'asinh', math.asinh) - - -def acosh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'acosh', math.acosh) - - -def atanh(arg): - return _fcn_dispatcher[arg.__class__](arg, 'atanh', math.atanh) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index d68f086e24f..e00199103e1 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1644,7 +1644,7 @@ def _decompose_linear_terms(expr, multiplier=1): # ------------------------------------------------------- -class ARG_TYPE(enum.Enum): +class ARG_TYPE(enum.IntEnum): MUTABLE = -2 ASNUMERIC = -1 INVALID = 0 @@ -3905,6 +3905,7 @@ def _fcn_mutable(a, name, fcn): def _fcn_invalid(a, name, fcn): fcn(a) + # returns None def _fcn_native(a, name, fcn): @@ -3954,6 +3955,133 @@ def _register_new_fcn_dispatcher(a, name, fcn): } +# +# NOTE: abs() and pow() are not defined here, because they are +# Python operators. +# +def ceil(arg): + return _fcn_dispatcher[arg.__class__](arg, 'ceil', math.ceil) + + +def floor(arg): + return _fcn_dispatcher[arg.__class__](arg, 'floor', math.floor) + + +# e ** x +def exp(arg): + return _fcn_dispatcher[arg.__class__](arg, 'exp', math.exp) + + +def log(arg): + return _fcn_dispatcher[arg.__class__](arg, 'log', math.log) + + +def log10(arg): + return _fcn_dispatcher[arg.__class__](arg, 'log10', math.log10) + + +# FIXME: this is nominally the same as x ** 0.5, but follows a different +# path and produces a different NL file! +def sqrt(arg): + return _fcn_dispatcher[arg.__class__](arg, 'sqrt', math.sqrt) + # return _pow_dispatcher[arg.__class__, float](arg, 0.5) + + +def sin(arg): + return _fcn_dispatcher[arg.__class__](arg, 'sin', math.sin) + + +def cos(arg): + return _fcn_dispatcher[arg.__class__](arg, 'cos', math.cos) + + +def tan(arg): + return _fcn_dispatcher[arg.__class__](arg, 'tan', math.tan) + + +def sinh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'sinh', math.sinh) + + +def cosh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'cosh', math.cosh) + + +def tanh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'tanh', math.tanh) + + +def asin(arg): + return _fcn_dispatcher[arg.__class__](arg, 'asin', math.asin) + + +def acos(arg): + return _fcn_dispatcher[arg.__class__](arg, 'acos', math.acos) + + +def atan(arg): + return _fcn_dispatcher[arg.__class__](arg, 'atan', math.atan) + + +def asinh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'asinh', math.asinh) + + +def acosh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'acosh', math.acosh) + + +def atanh(arg): + return _fcn_dispatcher[arg.__class__](arg, 'atanh', math.atanh) + + +# +# Function interface to Expr_ifExpression +# + + +def Expr_if(IF=None, THEN=None, ELSE=None): + """ + Function used to construct a conditional numeric expression. + """ + _pv = False + for _argname in ('ELSE', 'THEN', 'IF'): + _arg = locals()[_argname] + _type = _categorize_arg_type(_arg) + # Note that relational expressions get mapped to INVALID + while _type < ARG_TYPE.INVALID: + if _type is ARG_TYPE.MUTABLE: + _arg = _recast_mutable(_arg) + elif _type is ARG_TYPE.ASNUMERIC: + _arg = _arg.as_numeric() + else: + raise DeveloperError( + '_categorize_arg_type() returned unexpected ARG_TYPE' + ) + locals()[_argname] = _arg + _type = _categorize_arg_type(_arg) + if _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID: + _pv = True + # Notes: + # - side effect: IF is the last iteration, so _type == _categorize_arg_type(IF) + # - we do NO error checking as to the actual arg types. That is + # left to the writer (and as of writing [Jul 2023], the NL writer + # is the only writer that recognized Expr_if) + if _type is ARG_TYPE.NATIVE: + return THEN if IF else ELSE + elif _type is ARG_TYPE.PARAM and IF.is_constant(): + return THEN if IF.value else ELSE + elif _pv: + return Expr_ifExpression((IF, THEN, ELSE)) + else: + return NPV_Expr_ifExpression((IF, THEN, ELSE)) + + +# +# Misc (legacy) functions +# + + def _balanced_parens(arg): """Verify the string argument contains balanced parentheses. From e57a7fe19ff1aceddd83e1e2ad2bf562b131628e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 13 Jul 2023 21:48:21 -0600 Subject: [PATCH 08/15] Track removal of bool from native_numeric_types --- pyomo/repn/standard_repn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 5b3dedd4462..1ad99ff29a2 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -18,7 +18,7 @@ import logging import itertools -from pyomo.common.numeric_types import native_numeric_types +from pyomo.common.numeric_types import native_types, native_numeric_types from pyomo.core.base import Constraint, Objective, ComponentMap from pyomo.core.expr import current as EXPR @@ -970,7 +970,7 @@ def _collect_division(exp, multiplier, idMap, compute_values, verbose, quadratic def _collect_branching_expr(exp, multiplier, idMap, compute_values, verbose, quadratic): _if, _then, _else = exp.args - if _if.__class__ in native_numeric_types: # TODO: coverage? + if _if.__class__ in native_types: if_val = _if elif not _if.is_potentially_variable(): if compute_values: From c1bb342e7288e510b9146823bf462073d58a87c6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 13 Jul 2023 22:57:41 -0600 Subject: [PATCH 09/15] Move expression imports from expr.current to core.expr --- pyomo/core/expr/__init__.py | 166 +++++++++++++++++++++++++++------ pyomo/core/expr/current.py | 68 +++----------- pyomo/core/expr/expr_common.py | 29 ++++++ 3 files changed, 182 insertions(+), 81 deletions(-) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 0f0180b314f..d47fb3521f8 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -24,14 +24,13 @@ boolean_value, logical_expr, relational_expr, - current, ) # # FIXME: remove circular dependencies between relational_expr and numeric_expr # -# Initialize numvalue functions +# Initialize relational expression functions numeric_expr._generate_relational_expression = ( relational_expr._generate_relational_expression ) @@ -39,25 +38,79 @@ # Initialize logicalvalue functions boolean_value._generate_logical_proposition = logical_expr._generate_logical_proposition -from .numvalue import ( + +from pyomo.common.numeric_types import ( value, - is_constant, - is_fixed, - is_variable_type, - is_potentially_variable, - NumericValue, - ZeroConstant, native_numeric_types, native_types, - polynomial_degree, + nonpyomo_leaf_types, ) +from .base import ExpressionBase from .boolean_value import BooleanValue - +from .expr_common import ( + ExpressionType, + Mode, +) +from .logical_expr import ( + native_logical_types, + special_boolean_atom_types, + # + BooleanValue, + BooleanConstant, + BooleanExpressionBase, + # + UnaryBooleanExpression, + NotExpression, + BinaryBooleanExpression, + EquivalenceExpression, + XorExpression, + ImplicationExpression, + NaryBooleanExpression, + AndExpression, + OrExpression, + ExactlyExpression, + AtMostExpression, + AtLeastExpression, + # + land, + lnot, + lor, + xor, + equivalent, + exactly, + atleast, + atmost, + implies, +) from .numeric_expr import ( - linear_expression, - nonlinear_expression, - mutable_expression, + NumericValue, + NumericExpression, + # operators: + AbsExpression, + DivisionExpression, + Expr_ifExpression, + ExternalFunctionExpression, + LinearExpression, + MonomialTermExpression, + NegationExpression, + PowExpression, + ProductExpression, + SumExpressionBase, # TODO: deprecate / remove + SumExpression, + UnaryFunctionExpression, + # TBD: remove export of NPV classes here? + NPV_AbsExpression, + NPV_DivisionExpression, + NPV_Expr_ifExpression, + NPV_ExternalFunctionExpression, + NPV_NegationExpression, + NPV_PowExpression, + NPV_ProductExpression, + NPV_SumExpression, + NPV_UnaryFunctionExpression, + # functions to generate expressions + Expr_if, log, log10, sin, @@ -76,20 +129,77 @@ atanh, ceil, floor, - Expr_if, + # Lgacy utilities + NPV_expression_types, # TODO: remove + LinearDecompositionError, # TODO: move to common.errors + decompose_term, + linear_expression, + nonlinear_expression, + mutable_expression, ) -from .logical_expr import ( - land, - lnot, - lor, - xor, - equivalent, - exactly, - atleast, - atmost, - implies, +from .numvalue import ( + as_numeric, + is_constant, + is_fixed, + is_variable_type, + is_potentially_variable, + ZeroConstant, + polynomial_degree, +) +from .relational_expr import ( + RelationalExpression, + RangedExpression, + InequalityExpression, + EqualityExpression, + inequality, +) +from .symbol_map import SymbolMap +from .template_expr import ( + TemplateExpressionError, # TODO: move to common.errors + GetItemExpression, + Numeric_GetItemExpression, + Boolean_GetItemExpression, + Structural_GetItemExpression, + GetAttrExpression, + Numeric_GetAttrExpression, + Boolean_GetAttrExpression, + Structural_GetAttrExpression, + CallExpression, + TemplateSumExpression, + # + NPV_Numeric_GetItemExpression, + NPV_Boolean_GetItemExpression, + NPV_Structural_GetItemExpression, + NPV_Numeric_GetAttrExpression, + NPV_Boolean_GetAttrExpression, + NPV_Structural_GetAttrExpression, + # + IndexTemplate, + resolve_template, + ReplaceTemplateExpression, + substitute_template_expression, + substitute_getitem_with_param, + substitute_template_with_value, + templatize_rule, + templatize_constraint, +) +from .visitor import ( + StreamBasedExpressionVisitor, + SimpleExpressionVisitor, + ExpressionValueVisitor, + ExpressionReplacementVisitor, + FixedExpressionError, + NonConstantExpressionError, + identify_components, + identify_variables, + identify_mutable_parameters, + clone_expression, + evaluate_expression, + expression_to_string, + polynomial_degree, + replace_expressions, + sizeof_expression, ) -from .relational_expr import inequality -from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.expr.taylor_series import taylor_series_expansion +from .calculus.derivatives import differentiate +from .taylor_series import taylor_series_expansion diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 272d760c132..94bd1c0f433 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -15,54 +15,21 @@ # # Common intrinsic functions # -from pyomo.core.expr import expr_common as common - - -# -# Provide a global value that indicates which expression system is being used -# -class Mode(enum.IntEnum): - # coopr: Original Coopr/Pyomo expression system - coopr_trees = 1 - # coopr3: leverage reference counts to reduce the amount of required - # expression cloning to ensure independent expression trees. - coopr3_trees = 3 - # pyomo4: rework the expression system to remove reliance on - # reference counting. This enables pypy support (which doesn't have - # reference counting). This version never became the default. - pyomo4_trees = 4 - # pyomo5: refinement of pyomo4. Expressions are now immutable by - # contract, which tolerates "entangled" expression trees. Added - # specialized classes for NPV expressions and LinearExpressions. - pyomo5_trees = 5 - # pyomo6: refinement of pyomo5 expression generation to leverage - # multiple dispatch. Standardized expression storage and argument - # handling (significant rework of the LinearExpression structure). - pyomo6_trees = 6 - - -_mode = Mode.pyomo6_trees -# We no longer support concurrent expression systems. _mode is left -# primarily so we can support expression system-specific baselines -assert _mode == Mode.pyomo6_trees - -# -# Pull symbols from the appropriate expression system -# -from pyomo.core.expr import numvalue as _numvalue -from pyomo.core.expr import boolean_value as _logicalvalue -from pyomo.core.expr import numeric_expr as _numeric_expr - +import pyomo.core.expr.expr_common as common from pyomo.core.expr.expr_common import clone_counter -from pyomo.core.expr.base import ExpressionBase -from pyomo.core.expr.visitor import ( + +from pyomo.core.expr import ( + Mode, + _mode, + #from pyomo.core.expr.base + ExpressionBase, + # pyomo.core.expr.visitor evaluate_expression, expression_to_string, polynomial_degree, clone_expression, sizeof_expression, -) -from pyomo.core.expr.numeric_expr import ( + # pyomo.core.expr.numeric_expr NumericExpression, NumericValue, native_types, @@ -114,10 +81,9 @@ class Mode(enum.IntEnum): asinh, acosh, atanh, -) -from pyomo.core.expr.numvalue import as_numeric -from pyomo.core.expr import logical_expr as _logical_expr -from pyomo.core.expr.logical_expr import ( + #pyomo.core.expr.numvalue + as_numeric, + #pyomo.core.expr.logical_expr native_logical_types, BooleanValue, BooleanConstant, @@ -144,15 +110,13 @@ class Mode(enum.IntEnum): AtMostExpression, AtLeastExpression, special_boolean_atom_types, -) -from pyomo.core.expr.relational_expr import ( + # pyomo.core.expr.relational_expr RelationalExpression, RangedExpression, InequalityExpression, EqualityExpression, inequality, -) -from pyomo.core.expr.template_expr import ( + # pyomo.core.expr.template_expr TemplateExpressionError, GetItemExpression, Numeric_GetItemExpression, @@ -178,9 +142,7 @@ class Mode(enum.IntEnum): substitute_template_with_value, templatize_rule, templatize_constraint, -) -from pyomo.core.expr import visitor as _visitor -from pyomo.core.expr.visitor import ( + # pyomo.core.expr.visitor SymbolMap, StreamBasedExpressionVisitor, SimpleExpressionVisitor, diff --git a/pyomo/core/expr/expr_common.py b/pyomo/core/expr/expr_common.py index e5430158244..8e450ec35d4 100644 --- a/pyomo/core/expr/expr_common.py +++ b/pyomo/core/expr/expr_common.py @@ -28,6 +28,35 @@ _xor = 4 _impl = 5 +# +# Provide a global value that indicates which expression system is being used +# +class Mode(enum.IntEnum): + # coopr: Original Coopr/Pyomo expression system + coopr_trees = 1 + # coopr3: leverage reference counts to reduce the amount of required + # expression cloning to ensure independent expression trees. + coopr3_trees = 3 + # pyomo4: rework the expression system to remove reliance on + # reference counting. This enables pypy support (which doesn't have + # reference counting). This version never became the default. + pyomo4_trees = 4 + # pyomo5: refinement of pyomo4. Expressions are now immutable by + # contract, which tolerates "entangled" expression trees. Added + # specialized classes for NPV expressions and LinearExpressions. + pyomo5_trees = 5 + # pyomo6: refinement of pyomo5 expression generation to leverage + # multiple dispatch. Standardized expression storage and argument + # handling (significant rework of the LinearExpression structure). + pyomo6_trees = 6 + # + CURRENT = pyomo6_trees + +_mode = Mode.CURRENT +# We no longer support concurrent expression systems. _mode is left +# primarily so we can support expression system-specific baselines +assert _mode == Mode.pyomo6_trees + class OperatorAssociativity(enum.IntEnum): """Enum for indicating the associativity of an operator. From f96e90caee9262e471fcfdf1869569c5ea7e1c29 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 14 Jul 2023 11:28:56 -0600 Subject: [PATCH 10/15] Track change in expr.Mode --- pyomo/core/expr/__init__.py | 5 +---- pyomo/core/expr/current.py | 6 +++--- pyomo/core/expr/expr_common.py | 2 ++ pyomo/repn/tests/diffutils.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index d47fb3521f8..41d11366a9c 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -48,10 +48,7 @@ from .base import ExpressionBase from .boolean_value import BooleanValue -from .expr_common import ( - ExpressionType, - Mode, -) +from .expr_common import ExpressionType, Mode from .logical_expr import ( native_logical_types, special_boolean_atom_types, diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 94bd1c0f433..37d31f365c4 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -21,7 +21,7 @@ from pyomo.core.expr import ( Mode, _mode, - #from pyomo.core.expr.base + # from pyomo.core.expr.base ExpressionBase, # pyomo.core.expr.visitor evaluate_expression, @@ -81,9 +81,9 @@ asinh, acosh, atanh, - #pyomo.core.expr.numvalue + # pyomo.core.expr.numvalue as_numeric, - #pyomo.core.expr.logical_expr + # pyomo.core.expr.logical_expr native_logical_types, BooleanValue, BooleanConstant, diff --git a/pyomo/core/expr/expr_common.py b/pyomo/core/expr/expr_common.py index 8e450ec35d4..98c9a433994 100644 --- a/pyomo/core/expr/expr_common.py +++ b/pyomo/core/expr/expr_common.py @@ -28,6 +28,7 @@ _xor = 4 _impl = 5 + # # Provide a global value that indicates which expression system is being used # @@ -52,6 +53,7 @@ class Mode(enum.IntEnum): # CURRENT = pyomo6_trees + _mode = Mode.CURRENT # We no longer support concurrent expression systems. _mode is left # primarily so we can support expression system-specific baselines diff --git a/pyomo/repn/tests/diffutils.py b/pyomo/repn/tests/diffutils.py index d86f15aa711..d4edc026bd6 100644 --- a/pyomo/repn/tests/diffutils.py +++ b/pyomo/repn/tests/diffutils.py @@ -42,7 +42,7 @@ def load_baseline(baseline, testfile, extension, version): _tmp = [baseline[:-3]] else: _tmp = baseline.split(f'.{extension}.', 1) - _tmp.insert(1, f'expr{int(EXPR._mode)}') + _tmp.insert(1, f'expr{int(EXPR.Mode.CURRENT)}') _tmp.insert(2, version) if not os.path.exists('.'.join(_tmp)): _tmp.pop(1) From 2567886c273f33db857e0909081e88bd4ec172d5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 14 Jul 2023 11:36:10 -0600 Subject: [PATCH 11/15] Fix compatibility import --- pyomo/core/expr/current.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 37d31f365c4..b0405324580 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -16,11 +16,10 @@ # Common intrinsic functions # import pyomo.core.expr.expr_common as common -from pyomo.core.expr.expr_common import clone_counter +from pyomo.core.expr.expr_common import clone_counter, _mode from pyomo.core.expr import ( Mode, - _mode, # from pyomo.core.expr.base ExpressionBase, # pyomo.core.expr.visitor From 62591ce02d8010fe683275445f8e875222b676da Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 14 Jul 2023 13:52:34 -0600 Subject: [PATCH 12/15] Update Expr_if to support Cythonization --- pyomo/core/expr/numeric_expr.py | 36 ++++++++++++++++------ pyomo/core/tests/unit/test_numeric_expr.py | 4 ++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index e00199103e1..b231fc9e6a5 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -25,7 +25,7 @@ deprecation_warning, relocated_module_attribute, ) -from pyomo.common.errors import PyomoException +from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.formatting import tostr from pyomo.common.numeric_types import ( native_types, @@ -4040,13 +4040,26 @@ def atanh(arg): # -def Expr_if(IF=None, THEN=None, ELSE=None): +def Expr_if(IF_=None, THEN_=None, ELSE_=None, **kwargs): """ Function used to construct a conditional numeric expression. + + This function accepts either of the following signatures: + + - Expr_if(IF={expr}, THEN={expr}, ELSE={expr}) + - Expr_if(IF_={expr}, THEN_={expr}, ELSE_={expr}) + + (the former is historical, and the latter is required to support Cythonization) """ + L = locals() _pv = False - for _argname in ('ELSE', 'THEN', 'IF'): - _arg = locals()[_argname] + for _argname in ('ELSE_', 'THEN_', 'IF_'): + _arg = L[_argname] + _alt_arg = kwargs.pop(_argname[:-1], None) + if _alt_arg is not None: + if _arg is not None: + raise ValueError(f'Cannot specify both {_argname} and {_argname[:-1]}') + _arg = L[_argname] = _alt_arg _type = _categorize_arg_type(_arg) # Note that relational expressions get mapped to INVALID while _type < ARG_TYPE.INVALID: @@ -4058,23 +4071,26 @@ def Expr_if(IF=None, THEN=None, ELSE=None): raise DeveloperError( '_categorize_arg_type() returned unexpected ARG_TYPE' ) - locals()[_argname] = _arg + L[_argname] = _arg _type = _categorize_arg_type(_arg) if _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID: _pv = True + if kwargs: + raise ValueError('Unrecognized arguments: ' + ', '.join(kwargs)) # Notes: # - side effect: IF is the last iteration, so _type == _categorize_arg_type(IF) # - we do NO error checking as to the actual arg types. That is # left to the writer (and as of writing [Jul 2023], the NL writer # is the only writer that recognized Expr_if) + IF_ = L['IF_'] if _type is ARG_TYPE.NATIVE: - return THEN if IF else ELSE - elif _type is ARG_TYPE.PARAM and IF.is_constant(): - return THEN if IF.value else ELSE + return L['THEN_'] if IF_ else L['ELSE_'] + elif _type is ARG_TYPE.PARAM and IF_.is_constant(): + return L['THEN_'] if IF_.value else L['ELSE_'] elif _pv: - return Expr_ifExpression((IF, THEN, ELSE)) + return Expr_ifExpression((IF_, L['THEN_'], L['ELSE_'])) else: - return NPV_Expr_ifExpression((IF, THEN, ELSE)) + return NPV_Expr_ifExpression((IF_, L['THEN_'], L['ELSE_'])) # diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 808420e4ef6..330bffaacea 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -2452,7 +2452,7 @@ def test_expr_if(self): m = ConcreteModel() m.a = Var() m.b = Var() - expr = Expr_if(IF=m.a + m.b < 20, THEN=m.a, ELSE=m.b) + expr = Expr_if(IF_=m.a + m.b < 20, THEN_=m.a, ELSE_=m.b) self.assertEqual( "Expr_if( ( a + b < 20 ), then=( a ), else=( b ) )", str(expr) ) @@ -2460,6 +2460,8 @@ def test_expr_if(self): self.assertEqual( "Expr_if( ( a + b < 20 ), then=( 1 ), else=( b ) )", str(expr) ) + with self.assertRaisesRegex(ValueError, "Cannot specify both THEN_ and THEN"): + Expr_if(IF_=m.a + m.b < 20, THEN_=1, ELSE_=m.b, THEN=2) def test_getitem(self): m = ConcreteModel() From 5898c224c9ed1cfe6a26b1daf3394435bc5c6d6c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 14 Jul 2023 15:22:40 -0600 Subject: [PATCH 13/15] Avoid accessing locals() when processing Expr_if args --- pyomo/core/expr/numeric_expr.py | 57 +++++++++++----------- pyomo/core/tests/unit/test_numeric_expr.py | 2 + 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index b231fc9e6a5..b0f2cf380ff 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -4040,6 +4040,25 @@ def atanh(arg): # +def _process_expr_if_arg(arg, kwargs, name): + alt = kwargs.pop(name, None) + if alt is not None: + if arg is not None: + raise ValueError(f'Cannot specify both {name}_ and {name}') + arg = alt + _type = _categorize_arg_type(arg) + # Note that relational expressions get mapped to INVALID + while _type < ARG_TYPE.INVALID: + if _type is ARG_TYPE.MUTABLE: + arg = _recast_mutable(arg) + elif _type is ARG_TYPE.ASNUMERIC: + arg = arg.as_numeric() + else: + raise DeveloperError('_categorize_arg_type() returned unexpected ARG_TYPE') + _type = _categorize_arg_type(arg) + return arg, _type + + def Expr_if(IF_=None, THEN_=None, ELSE_=None, **kwargs): """ Function used to construct a conditional numeric expression. @@ -4051,30 +4070,13 @@ def Expr_if(IF_=None, THEN_=None, ELSE_=None, **kwargs): (the former is historical, and the latter is required to support Cythonization) """ - L = locals() _pv = False - for _argname in ('ELSE_', 'THEN_', 'IF_'): - _arg = L[_argname] - _alt_arg = kwargs.pop(_argname[:-1], None) - if _alt_arg is not None: - if _arg is not None: - raise ValueError(f'Cannot specify both {_argname} and {_argname[:-1]}') - _arg = L[_argname] = _alt_arg - _type = _categorize_arg_type(_arg) - # Note that relational expressions get mapped to INVALID - while _type < ARG_TYPE.INVALID: - if _type is ARG_TYPE.MUTABLE: - _arg = _recast_mutable(_arg) - elif _type is ARG_TYPE.ASNUMERIC: - _arg = _arg.as_numeric() - else: - raise DeveloperError( - '_categorize_arg_type() returned unexpected ARG_TYPE' - ) - L[_argname] = _arg - _type = _categorize_arg_type(_arg) - if _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID: - _pv = True + ELSE_, _type = _process_expr_if_arg(ELSE_, kwargs, 'ELSE') + _pv |= _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID + THEN_, _type = _process_expr_if_arg(THEN_, kwargs, 'THEN') + _pv |= _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID + IF_, _type = _process_expr_if_arg(IF_, kwargs, 'IF') + _pv |= _type >= ARG_TYPE.VAR or _type == ARG_TYPE.INVALID if kwargs: raise ValueError('Unrecognized arguments: ' + ', '.join(kwargs)) # Notes: @@ -4082,15 +4084,14 @@ def Expr_if(IF_=None, THEN_=None, ELSE_=None, **kwargs): # - we do NO error checking as to the actual arg types. That is # left to the writer (and as of writing [Jul 2023], the NL writer # is the only writer that recognized Expr_if) - IF_ = L['IF_'] if _type is ARG_TYPE.NATIVE: - return L['THEN_'] if IF_ else L['ELSE_'] + return THEN_ if IF_ else ELSE_ elif _type is ARG_TYPE.PARAM and IF_.is_constant(): - return L['THEN_'] if IF_.value else L['ELSE_'] + return THEN_ if IF_.value else ELSE_ elif _pv: - return Expr_ifExpression((IF_, L['THEN_'], L['ELSE_'])) + return Expr_ifExpression((IF_, THEN_, ELSE_)) else: - return NPV_Expr_ifExpression((IF_, L['THEN_'], L['ELSE_'])) + return NPV_Expr_ifExpression((IF_, THEN_, ELSE_)) # diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 330bffaacea..179ebed16ac 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -2462,6 +2462,8 @@ def test_expr_if(self): ) with self.assertRaisesRegex(ValueError, "Cannot specify both THEN_ and THEN"): Expr_if(IF_=m.a + m.b < 20, THEN_=1, ELSE_=m.b, THEN=2) + with self.assertRaisesRegex(ValueError, "Unrecognized arguments: _THEN_"): + Expr_if(IF_=m.a + m.b < 20, _THEN_=1, ELSE_=m.b) def test_getitem(self): m = ConcreteModel() From a64db1e5cc85d9f6496b78e6a9d3ec0fcfd84ecf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 21 Jul 2023 06:57:57 -0600 Subject: [PATCH 14/15] Update import location --- pyomo/core/expr/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 41d11366a9c..04ab1967175 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -45,6 +45,7 @@ native_types, nonpyomo_leaf_types, ) +from pyomo.common.errors import TemplateExpressionError from .base import ExpressionBase from .boolean_value import BooleanValue @@ -152,7 +153,6 @@ ) from .symbol_map import SymbolMap from .template_expr import ( - TemplateExpressionError, # TODO: move to common.errors GetItemExpression, Numeric_GetItemExpression, Boolean_GetItemExpression, From 5a3af38d0f830ec8eac5dd35f96e3243be3b1d3a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 21 Jul 2023 06:58:13 -0600 Subject: [PATCH 15/15] NFC: fix typo --- pyomo/common/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/errors.py b/pyomo/common/errors.py index 471fafc0036..050221c4a5e 100644 --- a/pyomo/common/errors.py +++ b/pyomo/common/errors.py @@ -224,7 +224,7 @@ class TemplateExpressionError(ValueError): This exception is triggered by the Pyomo expression system when attempting to get a member of an IndexedComponent using either a - TemplateIndex, or an expression vcontaining an TemplateIndex. + TemplateIndex, or an expression containing a TemplateIndex. Users should never see this exception.