Source code for pyomo.common.numeric_types

#  ___________________________________________________________________________
#
#  Pyomo: Python Optimization Modeling Objects
#  Copyright (c) 2008-2025
#  National Technology and Engineering Solutions of Sandia, LLC
#  Under the terms of Contract DE-NA0003525 with National Technology and
#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
#  rights in this software.
#  This software is distributed under the 3-clause BSD License.
#  ___________________________________________________________________________

import 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
#: :class:`NonNumericValue <pyomo.core.expr.numvalue.NonNumericValue>`,
#: which is commonly used in code that walks Pyomo expression trees.
#:
#: :data:`nonpyomo_leaf_types` = :data:`native_types <pyomo.core.expr.numvalue.native_types>` + { :data:`NonNumericValue <pyomo.core.expr.numvalue.NonNumericValue>` }
nonpyomo_leaf_types = set()

# It is *significantly* faster to build the list of types we want to
# test against as a "static" set, and not to regenerate it locally for
# every call.  Plus, this allows us to dynamically augment the set
# with new "native" types (e.g., from NumPy)
#
# Note: These type sets are used in set_types.py for domain validation
#       For backward compatibility reasons, we include str in the set
#       of valid types for bool. We also avoid updating the numeric
#       and integer type sets when a new boolean type is registered
#       because not all boolean types exhibit numeric properties
#       (e.g., numpy.bool_)
#

#: Python set used to identify numeric constants.  This set includes
#: native Python types as well as numeric types from Python packages
#: like numpy, which may be registered by users.
#:
#: Note that :data:`native_numeric_types` does NOT include
#: :py:class:`complex`, as that is not a valid constant in Pyomo numeric
#: expressions.
native_numeric_types = {int, float}
native_integer_types = {int}
native_logical_types = {bool}
native_complex_types = {complex}

_native_boolean_types = {int, bool, str, bytes}
relocated_module_attribute(
    'native_boolean_types',
    'pyomo.common.numeric_types._native_boolean_types',
    version='6.6.0',
    msg="The native_boolean_types set will be removed in the future: the set "
    "contains types that were convertible to bool, and not types that should "
    "be treated as if they were bool (as was the case for the other "
    "native_*_types sets).  Users likely should use native_logical_types.",
)
_pyomo_constant_types = set()  # includes NumericConstant, _PythonCallbackFunctionID
relocated_module_attribute(
    'pyomo_constant_types',
    'pyomo.common.numeric_types._pyomo_constant_types',
    version='6.7.2',
    msg="The pyomo_constant_types set will be removed in the future: the set "
    "contained only NumericConstant and _PythonCallbackFunctionID, and provided "
    "no meaningful value to clients or walkers.  Users should likely handle "
    "these types in the same manner as immutable Params.",
)


#: Python set used to identify numeric constants and related native
#: types.  This set includes
#: native Python types as well as numeric types from Python packages
#: like numpy.
#:
#: :data:`native_types` = :data:`native_numeric_types <pyomo.core.expr.numvalue.native_numeric_types>` + { str }
native_types = {bool, str, type(None), slice, bytes}
native_types.update(native_numeric_types)
native_types.update(native_integer_types)
native_types.update(native_complex_types)
native_types.update(native_logical_types)
native_types.update(_native_boolean_types)

nonpyomo_leaf_types.update(native_types)


[docs] def RegisterNumericType(new_type: type): """Register the specified type as a "numeric type". A utility function for registering new types as "native numeric types" that can be leaf nodes in Pyomo numeric expressions. The type should be compatible with :py:class:`float` (that is, store a scalar and be castable to a Python float). Parameters ---------- new_type : type The new numeric type (e.g, `numpy.float64`) """ native_numeric_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type)
[docs] def RegisterIntegerType(new_type: type): """Register the specified type as an "integer type". A utility function for registering new types as "native integer types". Integer types can be leaf nodes in Pyomo numeric expressions. The type should be compatible with :py:class:`float` (that is, store a scalar and be castable to a Python float). Registering a type as an integer type implies :py:func:`RegisterNumericType`. Note that integer types are NOT registered as logical / Boolean types. Parameters ---------- new_type : type The new integer type (e.g, `numpy.int64`) """ native_numeric_types.add(new_type) native_integer_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type)
[docs] @deprecated( "The native_boolean_types set (and hence RegisterBooleanType) " "is deprecated. Users likely should use RegisterLogicalType.", version='6.6.0', ) def RegisterBooleanType(new_type: type): """Register the specified type as a "logical type". A utility function for registering new types as "native logical types". Logical types can be leaf nodes in Pyomo logical expressions. The type should be compatible with :py:class:`bool` (that is, store a scalar and be castable to a Python bool). Note that logical types are NOT registered as numeric types. Parameters ---------- new_type : type The new logical type (e.g, `numpy.bool_`) """ _native_boolean_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type)
[docs] def RegisterComplexType(new_type: type): """Register the specified type as an "complex type". A utility function for registering new types as "native complex types". Complex types can NOT be leaf nodes in Pyomo numeric expressions. The type should be compatible with :py:class:`complex` (that is, store a scalar complex value and be castable to a Python complex). Note that complex types are NOT registered as logical or numeric types. Parameters ---------- new_type : type The new complex type (e.g, `numpy.complex128`) """ native_types.add(new_type) native_complex_types.add(new_type) nonpyomo_leaf_types.add(new_type)
[docs] def RegisterLogicalType(new_type: type): """Register the specified type as a "logical type". A utility function for registering new types as "native logical types". Logical types can be leaf nodes in Pyomo logical expressions. The type should be compatible with :py:class:`bool` (that is, store a scalar and be castable to a Python bool). Note that logical types are NOT registered as numeric types. Parameters ---------- new_type : type The new logical type (e.g, `numpy.bool_`) """ _native_boolean_types.add(new_type) native_logical_types.add(new_type) native_types.add(new_type) nonpyomo_leaf_types.add(new_type)
[docs] def check_if_native_type(obj): if isinstance(obj, (str, bytes)): native_types.add(obj.__class__) return True if check_if_logical_type(obj): return True if check_if_numeric_type(obj): return True return False
[docs] def check_if_logical_type(obj): """Test if the argument behaves like a logical type. We check for "logical types" by checking if the type returns sane results for Boolean operators (``^``, ``|``, ``&``) and if it maps ``1`` and ``2`` both to the same equivalent instance. If that works, then we register the type in :py:attr:`native_logical_types`. """ obj_class = obj.__class__ # Do not re-evaluate known native types if obj_class in native_types: return obj_class in native_logical_types try: # It is not an error if you can't initialize the type from an # int, but if you can, it should map !0 to True if obj_class(1) != obj_class(2): return False except: pass try: # Native logical types *must* be hashable hash(obj) # Native logical types must honor standard Boolean operators if all( ( obj_class(False) != obj_class(True), obj_class(False) ^ obj_class(False) == obj_class(False), obj_class(False) ^ obj_class(True) == obj_class(True), obj_class(True) ^ obj_class(False) == obj_class(True), obj_class(True) ^ obj_class(True) == obj_class(False), obj_class(False) | obj_class(False) == obj_class(False), obj_class(False) | obj_class(True) == obj_class(True), obj_class(True) | obj_class(False) == obj_class(True), obj_class(True) | obj_class(True) == obj_class(True), obj_class(False) & obj_class(False) == obj_class(False), obj_class(False) & obj_class(True) == obj_class(False), obj_class(True) & obj_class(False) == obj_class(False), obj_class(True) & obj_class(True) == obj_class(True), ) ): RegisterLogicalType(obj_class) return True except: pass return False
[docs] 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, and that the object compares to 0 in a meaningful way. If that works, then we register the type in :py:attr:`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__ # Native numeric types *must* be hashable hash(obj) except: return False if obj_p0_class is not obj_class and obj_p0_class not in native_numeric_types: return False # # Check if the numeric type behaves like a complex type # try: if 1.41 < abs(obj_class(1j + 1)) < 1.42: RegisterComplexType(obj_class) return False except: pass # # Ensure that the object is comparable to 0 in a meaningful way # try: if not ((obj < 0) ^ (obj >= 0)): return False except: return False # # 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) try: if obj_class(0.4) == obj_class(0): RegisterIntegerType(obj_class) except: pass # # 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
[docs] 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 # # Test if we have a duck typed Pyomo expression # if not hasattr(obj, 'is_numeric_type'): # # 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__ ) # # 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 %s object %s" % (type(obj).__name__, 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)