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

Consolidate walker logic in LP/NL representations #3015

Merged
merged 33 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1073c3e
Add centralized beforeChild/exitNode dispatcher classes
jsiirola Sep 7, 2023
d4434bd
Standardize handling of named expression types
jsiirola Sep 7, 2023
1a3e34a
Add docs, improve method naming
jsiirola Sep 7, 2023
10a3b95
NFC: remove unreachable code; update comments
jsiirola Sep 7, 2023
ea1dc29
Fix typos
jsiirola Sep 7, 2023
8504c05
Split complex types out from native_numeric_types
jsiirola Sep 7, 2023
9143204
Update list of numpy types
jsiirola Sep 7, 2023
e0729db
Remove repeated constant resolution code
jsiirola Sep 7, 2023
e80bf23
NFC: comments
jsiirola Sep 7, 2023
1d89380
Remove redundant checks for int/float domains
jsiirola Sep 7, 2023
8876e25
Apply black
jsiirola Sep 7, 2023
0c52ea0
update tests to track dispatcher api changes
jsiirola Sep 7, 2023
596a45f
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Sep 18, 2023
0b60875
Remove unused import
jsiirola Sep 18, 2023
2640630
Apply black
jsiirola Sep 18, 2023
1c65805
Check for platform-specific float/complex types
jsiirola Sep 18, 2023
d122490
Leverage new native_complex_types set
jsiirola Sep 18, 2023
431e8bb
Fix base class for detecting named subexpressiosn
jsiirola Sep 18, 2023
14def43
relax kernel test to track differences in numpy builds
jsiirola Sep 20, 2023
f31e005
relax kernel test to track differences in numpy builds
jsiirola Sep 21, 2023
7a552a0
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Sep 21, 2023
6d3f77c
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Sep 27, 2023
000cd86
Add missing import
jsiirola Sep 27, 2023
09c9c2f
Improve exception/InvalidNumber messages
jsiirola Sep 27, 2023
b706fe1
Only cache type / unary, binary, and ternary dispatchers
jsiirola Sep 27, 2023
5a217cd
Test BeforeChildDispatcher / ExitNodeDispatcher
jsiirola Sep 27, 2023
9d3b38c
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Oct 6, 2023
706d27c
Clarify docstring / exception messages
jsiirola Oct 10, 2023
9171886
Slotize derived dispatchers
jsiirola Oct 10, 2023
2b6e111
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Oct 11, 2023
63aba93
Apply black
jsiirola Oct 12, 2023
9219573
NFC: fix comment typo
jsiirola Oct 23, 2023
db38e94
Merge branch 'main' into repn-dry-lp-nl-beforechild
jsiirola Oct 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion pyomo/common/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,8 @@
def _finalize_numpy(np, available):
if not available:
return
# Register ndarray as a native type to prevent 1-element ndarrays
# from accidentally registering ndarray as a native_numeric_type.
numeric_types.native_types.add(np.ndarray)
numeric_types.RegisterLogicalType(np.bool_)
for t in (
Expand All @@ -798,12 +800,30 @@
# registration here (to bypass the deprecation warning) until we
# finally remove all support for it
numeric_types._native_boolean_types.add(t)
for t in (np.float_, np.float16, np.float32, np.float64):
_floats = [np.float_, np.float16, np.float32, np.float64]
# float96 and float128 may or may not be defined in this particular
# numpy build (it depends on platform and version).
# Register them only if they are present
if hasattr(np, 'float96'):
_floats.append(np.float96)

Check warning on line 808 in pyomo/common/dependencies.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/dependencies.py#L808

Added line #L808 was not covered by tests
if hasattr(np, 'float128'):
_floats.append(np.float128)
for t in _floats:
numeric_types.RegisterNumericType(t)
# We have deprecated RegisterBooleanType, so we will mock up the
# registration here (to bypass the deprecation warning) until we
# finally remove all support for it
numeric_types._native_boolean_types.add(t)
_complex = [np.complex_, np.complex64, np.complex128]
# complex192 and complex256 may or may not be defined in this
# particular numpy build (it depends on platform and version).
# Register them only if they are present
if hasattr(np, 'complex192'):
_complex.append(np.complex192)

Check warning on line 822 in pyomo/common/dependencies.py

View check run for this annotation

Codecov / codecov/patch

pyomo/common/dependencies.py#L822

Added line #L822 was not covered by tests
if hasattr(np, 'complex256'):
_complex.append(np.complex256)
for t in _complex:
numeric_types.RegisterComplexType(t)


dill, dill_available = attempt_import('dill')
Expand Down
155 changes: 109 additions & 46 deletions pyomo/common/numeric_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@
#: 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.
native_numeric_types = {int, float, complex}
#:
#: Note that :data:`native_numeric_types` does NOT include
#: :py:`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}
pyomo_constant_types = set() # includes NumericConstant

_native_boolean_types = {int, bool, str, bytes}
Expand All @@ -64,34 +69,53 @@
#: like numpy.
#:
#: :data:`native_types` = :data:`native_numeric_types <pyomo.core.expr.numvalue.native_numeric_types>` + { str }
native_types = set([bool, str, type(None), slice, bytes])
native_types = {bool, str, type(None), slice, bytes}
native_types.update(native_numeric_types)
native_types.update(native_integer_types)
native_types.update(_native_boolean_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)


def RegisterNumericType(new_type):
"""
A utility function for updating the set of types that are
recognized to handle numeric values.
def RegisterNumericType(new_type: type):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't really bother me (as that would by hypocritical), but wouldn't it be PEP8-ier for these be snake case rather than camel case since they're functions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - they should be snake case. But as this has been an "official" API for a long time, we would need to rename it with a deprecation path (which I'm OK with, but maybe as a separate PR?).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. A separate PR makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to do that... We have these in several places.

"""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)

The argument should be a class (e.g, numpy.float64).
"""
native_numeric_types.add(new_type)
native_types.add(new_type)
nonpyomo_leaf_types.add(new_type)


def RegisterIntegerType(new_type):
"""
A utility function for updating the set of types that are
recognized to handle integer values. This also registers the type
as numeric but does not register it as boolean.
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).
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a copy-paste typo? Should they be castable to int rather than float? (I mean, Python is happy to cast floats to ints but...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Believe it or not, it is not a typo. The assumption in the Numeric expression system is that the value of any expression should be float-like.

This set is actually not used anywhere in Pyomo. I think it was added in the dawn of time on speculation that it might be useful. Maybe the right thing is to deprecate it entirely?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be in favor of deprecating it before someone starts using it and we get really confused. :P


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)

The argument should be a class (e.g., numpy.int64).
"""
native_numeric_types.add(new_type)
native_integer_types.add(new_type)
Expand All @@ -104,26 +128,64 @@ def RegisterIntegerType(new_type):
"is deprecated. Users likely should use RegisterLogicalType.",
version='6.6.0',
)
def RegisterBooleanType(new_type):
"""
A utility function for updating the set of types that are
recognized as handling boolean values. This function does not
register the type of integer or numeric.
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_)

The argument should be a class (e.g., numpy.bool_).
"""
_native_boolean_types.add(new_type)
native_types.add(new_type)
nonpyomo_leaf_types.add(new_type)


def RegisterLogicalType(new_type):
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)

"""
A utility function for updating the set of types that are
recognized as handling boolean values. This function does not
register the type of integer or numeric.
native_types.add(new_type)
native_complex_types.add(new_type)
nonpyomo_leaf_types.add(new_type)


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).
Comment on lines +174 to +180
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What goes in native_logical_types that doesn't go in native_boolean_types? Or is this a backwards compatibility thing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a backwards compatibility thing. native_boolean_types is the deprecated set and was replaced by native_logical_types. I toyed with making them explicit aliases for each other, but elected to not continue mucking with a deprecated API.


Note that logical types are NOT registered as numeric types.

Parameters
----------
new_type: type
The new logical type (e.g, numpy.bool_)

The argument should be a class (e.g., numpy.bool_).
"""
_native_boolean_types.add(new_type)
native_logical_types.add(new_type)
Expand All @@ -135,8 +197,9 @@ 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.
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__
Expand Down Expand Up @@ -181,25 +244,25 @@ def check_if_numeric_type(obj):

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.
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
Expand Down
22 changes: 11 additions & 11 deletions pyomo/core/kernel/register_numpy_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
version='6.1',
)

from pyomo.core.expr.numvalue import (
from pyomo.common.numeric_types import (
RegisterNumericType,
RegisterIntegerType,
RegisterBooleanType,
native_complex_types,
native_numeric_types,
native_integer_types,
native_boolean_types,
Expand All @@ -37,13 +38,17 @@
numpy_float = []
numpy_bool_names = []
numpy_bool = []
numpy_complex_names = []
numpy_complex = []

if _has_numpy:
# Historically, the lists included several numpy aliases
numpy_int_names.extend(('int_', 'intc', 'intp'))
numpy_int.extend((numpy.int_, numpy.intc, numpy.intp))
numpy_float_names.append('float_')
numpy_float.append(numpy.float_)
numpy_complex_names.append('complex_')
numpy_complex.append(numpy.complex_)

# Re-build the old numpy_* lists
for t in native_boolean_types:
Expand All @@ -63,13 +68,8 @@


# Complex
numpy_complex_names = []
numpy_complex = []
if _has_numpy:
numpy_complex_names.extend(('complex_', 'complex64', 'complex128'))
for _type_name in numpy_complex_names:
try:
_type = getattr(numpy, _type_name)
numpy_complex.append(_type)
except: # pragma:nocover
pass
for t in native_complex_types:
if t.__module__ == 'numpy':
if t.__name__ not in numpy_complex_names:
numpy_complex.append(t)
numpy_complex_names.append(t.__name__)
12 changes: 11 additions & 1 deletion pyomo/core/tests/unit/test_kernel_register_numpy_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# ___________________________________________________________________________

import pyomo.common.unittest as unittest
from pyomo.common.dependencies import numpy_available
from pyomo.common.dependencies import numpy, numpy_available
from pyomo.common.log import LoggingIntercept

# Boolean
Expand Down Expand Up @@ -38,12 +38,22 @@
numpy_float_names.append('float16')
numpy_float_names.append('float32')
numpy_float_names.append('float64')
if hasattr(numpy, 'float96'):
numpy_float_names.append('float96')
if hasattr(numpy, 'float128'):
# On some numpy builds, the name of float128 is longdouble
numpy_float_names.append(numpy.float128.__name__)
# Complex
numpy_complex_names = []
if numpy_available:
numpy_complex_names.append('complex_')
numpy_complex_names.append('complex64')
numpy_complex_names.append('complex128')
if hasattr(numpy, 'complex192'):
numpy_complex_names.append('complex192')
if hasattr(numpy, 'complex256'):
# On some numpy builds, the name of complex256 is clongdouble
numpy_complex_names.append(numpy.complex256.__name__)


class TestNumpyRegistration(unittest.TestCase):
Expand Down
Loading