Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix type registrations for ExternalFunction arguments #3168

Merged
merged 11 commits into from
Mar 5, 2024
130 changes: 93 additions & 37 deletions pyomo/common/numeric_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
native_integer_types = {int}
native_logical_types = {bool}
native_complex_types = {complex}
pyomo_constant_types = set() # includes NumericConstant

_native_boolean_types = {int, bool, str, bytes}
relocated_module_attribute(
Expand All @@ -62,6 +61,16 @@
"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.dev0',
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
Expand Down Expand Up @@ -194,6 +203,48 @@
nonpyomo_leaf_types.add(new_type)


def check_if_native_type(obj):
if isinstance(obj, (str, bytes)):
native_types.add(obj.__class__)
return True

Check warning on line 209 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L208-L209

Added lines #L208 - L209 were not covered by tests
if check_if_logical_type(obj):
return True

Check warning on line 211 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L211

Added line #L211 was not covered by tests
if check_if_numeric_type(obj):
return True

Check warning on line 213 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L213

Added line #L213 was not covered by tests
return False


def check_if_logical_type(obj):
"""Test if the argument behaves like a logical 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`.
blnicho marked this conversation as resolved.
Show resolved Hide resolved

"""
obj_class = obj.__class__
# Do not re-evaluate known native types
if obj_class in native_types:
return obj_class in native_logical_types

Check warning on line 229 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L229

Added line #L229 was not covered by tests

try:
if all(
(
obj_class(1) == obj_class(2),
obj_class(False) != obj_class(True),
obj_class(False) ^ obj_class(True) == obj_class(True),
obj_class(False) | obj_class(True) == obj_class(True),
obj_class(False) & obj_class(True) == obj_class(False),
)
):
RegisterLogicalType(obj_class)
return True

Check warning on line 242 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L241-L242

Added lines #L241 - L242 were not covered by tests
except:
pass
return False


def check_if_numeric_type(obj):
"""Test if the argument behaves like a numeric type.

Expand All @@ -218,36 +269,53 @@
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)
# 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 warning on line 277 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L277

Added line #L277 was not covered by tests
#
# 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

Check warning on line 284 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L283-L284

Added lines #L283 - L284 were not covered by tests
except:
pass
#
# 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)
try:
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:
#
# 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

Check warning on line 306 in pyomo/common/numeric_types.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/numeric_types.py#L305-L306

Added lines #L305 - L306 were not covered by tests
#
# 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
)
return True


def value(obj, exception=True):
Expand All @@ -274,22 +342,10 @@
"""
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:
if not hasattr(obj, 'is_numeric_type'):
#
# TODO: Historically we checked for new *numeric* types and
# raised exceptions for anything else. That is inconsistent
Expand All @@ -304,7 +360,7 @@
return None
raise TypeError(
"Cannot evaluate object with unknown type: %s" % obj.__class__.__name__
) from None
)
#
# Evaluate the expression object
#
Expand Down
17 changes: 9 additions & 8 deletions pyomo/core/base/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@

from pyomo.common.autoslots import AutoSlots
from pyomo.common.fileutils import find_library
from pyomo.core.expr.numvalue import (
from pyomo.common.numeric_types import (
check_if_native_type,
native_types,
native_numeric_types,
pyomo_constant_types,
NonNumericValue,
NumericConstant,
value,
_pyomo_constant_types,
)
from pyomo.core.expr.numvalue import NonNumericValue, NumericConstant
import pyomo.core.expr as EXPR
from pyomo.core.base.component import Component
from pyomo.core.base.units_container import units
Expand Down Expand Up @@ -197,14 +197,15 @@
pv = False
for i, arg in enumerate(args_):
try:
# Q: Is there a better way to test if a value is an object
# not in native_types and not a standard expression type?
if arg.__class__ in native_types:
continue
if arg.is_potentially_variable():
pv = True
continue
except AttributeError:
args_[i] = NonNumericValue(arg)
if check_if_native_type(arg):
continue

Check warning on line 207 in pyomo/core/base/external.py

View check run for this annotation

Codecov / codecov/patch

pyomo/core/base/external.py#L207

Added line #L207 was not covered by tests
args_[i] = NonNumericValue(arg)
#
if pv:
return EXPR.ExternalFunctionExpression(args_, self)
Expand Down Expand Up @@ -491,7 +492,7 @@
return False


pyomo_constant_types.add(_PythonCallbackFunctionID)
_pyomo_constant_types.add(_PythonCallbackFunctionID)


class PythonCallbackFunction(ExternalFunction):
Expand Down
5 changes: 2 additions & 3 deletions pyomo/core/base/units_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
value,
native_types,
native_numeric_types,
pyomo_constant_types,
)
from pyomo.core.expr.template_expr import IndexTemplate
from pyomo.core.expr.visitor import ExpressionValueVisitor
Expand Down Expand Up @@ -902,7 +901,7 @@ def initializeWalker(self, expr):

def beforeChild(self, node, child, child_idx):
ctype = child.__class__
if ctype in native_types or ctype in pyomo_constant_types:
if ctype in native_types:
return False, self._pint_dimensionless

if child.is_expression_type():
Expand All @@ -917,7 +916,7 @@ def beforeChild(self, node, child, child_idx):
pint_unit = self._pyomo_units_container._get_pint_units(pyomo_unit)
return False, pint_unit

return True, None
return False, self._pint_dimensionless

def exitNode(self, node, data):
"""Visitor callback when moving up the expression tree.
Expand Down
9 changes: 6 additions & 3 deletions pyomo/core/expr/numvalue.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
native_numeric_types,
native_integer_types,
native_logical_types,
pyomo_constant_types,
_pyomo_constant_types,
check_if_numeric_type,
value,
)
Expand Down Expand Up @@ -85,7 +85,7 @@
##------------------------------------------------------------------------


class NonNumericValue(object):
class NonNumericValue(PyomoObject):
"""An object that contains a non-numeric value

Constructor Arguments:
Expand All @@ -100,6 +100,9 @@
def __str__(self):
return str(self.value)

def __call__(self, exception=None):
return self.value

Check warning on line 104 in pyomo/core/expr/numvalue.py

View check run for this annotation

Codecov / codecov/patch

pyomo/core/expr/numvalue.py#L104

Added line #L104 was not covered by tests


nonpyomo_leaf_types.add(NonNumericValue)

Expand Down Expand Up @@ -410,7 +413,7 @@
ostream.write(str(self))


pyomo_constant_types.add(NumericConstant)
_pyomo_constant_types.add(NumericConstant)

# We use as_numeric() so that the constant is also in the cache
ZeroConstant = as_numeric(0)