oprattr 0.6.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of oprattr might be problematic. Click here for more details.
- {oprattr-0.6.0 → oprattr-0.8.0}/CHANGELOG.md +14 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/PKG-INFO +1 -1
- {oprattr-0.6.0 → oprattr-0.8.0}/pyproject.toml +2 -2
- oprattr-0.8.0/src/oprattr/__init__.py +128 -0
- oprattr-0.8.0/src/oprattr/__init__.pyi +123 -0
- oprattr-0.6.0/src/oprattr/abstract.py → oprattr-0.8.0/src/oprattr/_abstract.py +7 -7
- oprattr-0.8.0/src/oprattr/_exceptions.py +17 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/src/oprattr/_operations.py +5 -9
- oprattr-0.8.0/src/oprattr/mixins.py +148 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/tests/test_object.py +11 -12
- {oprattr-0.6.0 → oprattr-0.8.0}/uv.lock +4 -4
- oprattr-0.6.0/src/oprattr/__init__.py +0 -139
- oprattr-0.6.0/src/oprattr/__init__.pyi +0 -81
- oprattr-0.6.0/src/oprattr/mixins.py +0 -489
- {oprattr-0.6.0 → oprattr-0.8.0}/.gitignore +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/.python-version +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/DEVELOPERS.md +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/LICENSE +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/README.md +0 -0
- /oprattr-0.6.0/src/oprattr/typeface.py → /oprattr-0.8.0/src/oprattr/_typeface.py +0 -0
- /oprattr-0.6.0/src/oprattr/typeface.pyi → /oprattr-0.8.0/src/oprattr/_typeface.pyi +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/src/oprattr/methods.py +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/src/oprattr/py.typed +0 -0
- {oprattr-0.6.0 → oprattr-0.8.0}/tests/print-exceptions.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## NEXT
|
|
2
2
|
|
|
3
|
+
## v0.8.0
|
|
4
|
+
|
|
5
|
+
- Redefine `mixins.NumpyMixin` to extend `numerical.NumpyMixin`
|
|
6
|
+
- Redefine `Object` to extend `numerical.Object`
|
|
7
|
+
- Improve flexibility of `numpy` functions in `Operand`
|
|
8
|
+
- Update types in `__init__.pyi`
|
|
9
|
+
|
|
10
|
+
## v0.7.0
|
|
11
|
+
|
|
12
|
+
- Add `oprattr.Object` and `oprattr.Quantity` to the public namespace
|
|
13
|
+
- Rename modules that should not be part of the public namespace
|
|
14
|
+
- Define the `OperationError` exception class
|
|
15
|
+
- Change default behavior when a metadata attribute does not implement an operation
|
|
16
|
+
|
|
3
17
|
## v0.6.0
|
|
4
18
|
|
|
5
19
|
- Add `methods` module
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oprattr"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Add your description here"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -24,4 +24,4 @@ dev = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[tool.uv.sources]
|
|
27
|
-
numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.
|
|
27
|
+
numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.3.0" }
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
|
|
6
|
+
from ._abstract import (
|
|
7
|
+
Quantity,
|
|
8
|
+
Object,
|
|
9
|
+
)
|
|
10
|
+
from ._exceptions import (
|
|
11
|
+
MetadataTypeError,
|
|
12
|
+
MetadataValueError,
|
|
13
|
+
OperationError,
|
|
14
|
+
)
|
|
15
|
+
from . import methods
|
|
16
|
+
from . import mixins
|
|
17
|
+
from . import _typeface
|
|
18
|
+
from ._operations import (
|
|
19
|
+
unary,
|
|
20
|
+
equality,
|
|
21
|
+
ordering,
|
|
22
|
+
additive,
|
|
23
|
+
multiplicative,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
T = _typeface.TypeVar('T')
|
|
28
|
+
|
|
29
|
+
class Operand(Object[T], mixins.NumpyMixin):
|
|
30
|
+
"""A concrete implementation of a real-valued object."""
|
|
31
|
+
|
|
32
|
+
__abs__ = methods.__abs__
|
|
33
|
+
__pos__ = methods.__pos__
|
|
34
|
+
__neg__ = methods.__neg__
|
|
35
|
+
|
|
36
|
+
__eq__ = methods.__eq__
|
|
37
|
+
__ne__ = methods.__ne__
|
|
38
|
+
__lt__ = methods.__lt__
|
|
39
|
+
__le__ = methods.__le__
|
|
40
|
+
__gt__ = methods.__gt__
|
|
41
|
+
__ge__ = methods.__ge__
|
|
42
|
+
|
|
43
|
+
__add__ = methods.__add__
|
|
44
|
+
__radd__ = methods.__radd__
|
|
45
|
+
__sub__ = methods.__sub__
|
|
46
|
+
__rsub__ = methods.__rsub__
|
|
47
|
+
__mul__ = methods.__mul__
|
|
48
|
+
__rmul__ = methods.__rmul__
|
|
49
|
+
__truediv__ = methods.__truediv__
|
|
50
|
+
__rtruediv__ = methods.__rtruediv__
|
|
51
|
+
__floordiv__ = methods.__floordiv__
|
|
52
|
+
__rfloordiv__ = methods.__rfloordiv__
|
|
53
|
+
__mod__ = methods.__mod__
|
|
54
|
+
__rmod__ = methods.__rmod__
|
|
55
|
+
__pow__ = methods.__pow__
|
|
56
|
+
__rpow__ = methods.__rpow__
|
|
57
|
+
|
|
58
|
+
def __array__(self, *args, **kwargs):
|
|
59
|
+
"""Called for numpy.array(self)."""
|
|
60
|
+
return numpy.array(self._data, *args, **kwargs)
|
|
61
|
+
|
|
62
|
+
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
63
|
+
if ufunc in (numpy.equal, numpy.not_equal):
|
|
64
|
+
# NOTE: We are probably here because the left operand is a
|
|
65
|
+
# `numpy.ndarray`, which would otherwise take control and return the
|
|
66
|
+
# pure `numpy` result.
|
|
67
|
+
f = getattr(ufunc, method)
|
|
68
|
+
return equality(f, *args)
|
|
69
|
+
data, meta = super()._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
70
|
+
return self._from_numpy(data, **meta)
|
|
71
|
+
|
|
72
|
+
def _apply_function(self, func, types, args, kwargs):
|
|
73
|
+
data, meta = super()._apply_function(func, types, args, kwargs)
|
|
74
|
+
if data is NotImplemented:
|
|
75
|
+
return data
|
|
76
|
+
return self._from_numpy(data, **meta)
|
|
77
|
+
|
|
78
|
+
def _get_numpy_array(self):
|
|
79
|
+
return numpy.array(self._data)
|
|
80
|
+
|
|
81
|
+
def _from_numpy(self, data, **meta):
|
|
82
|
+
"""Create a new instance after applying a numpy function."""
|
|
83
|
+
if isinstance(data, (list, tuple)):
|
|
84
|
+
r = [self._factory(array, **meta) for array in data]
|
|
85
|
+
if isinstance(data, tuple):
|
|
86
|
+
return tuple(r)
|
|
87
|
+
return r
|
|
88
|
+
return self._factory(data, **meta)
|
|
89
|
+
|
|
90
|
+
def _factory(self, data, **meta):
|
|
91
|
+
"""Create a new instance from data and metadata.
|
|
92
|
+
|
|
93
|
+
The default implementation uses the standard `__new__` constructor.
|
|
94
|
+
Subclasses may overload this method to use a different constructor
|
|
95
|
+
(e.g., a module-defined factory function).
|
|
96
|
+
"""
|
|
97
|
+
return type(self)(data, **meta)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@Operand.implementation(numpy.array_equal)
|
|
101
|
+
def array_equal(
|
|
102
|
+
x: numpy.typing.ArrayLike,
|
|
103
|
+
y: numpy.typing.ArrayLike,
|
|
104
|
+
**kwargs
|
|
105
|
+
) -> bool:
|
|
106
|
+
"""Called for numpy.array_equal(x, y)"""
|
|
107
|
+
return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
# Modules
|
|
112
|
+
methods,
|
|
113
|
+
mixins,
|
|
114
|
+
# Object classes
|
|
115
|
+
Quantity,
|
|
116
|
+
Object,
|
|
117
|
+
# Error classes
|
|
118
|
+
MetadataTypeError,
|
|
119
|
+
MetadataValueError,
|
|
120
|
+
OperationError,
|
|
121
|
+
# Functions
|
|
122
|
+
additive,
|
|
123
|
+
equality,
|
|
124
|
+
multiplicative,
|
|
125
|
+
ordering,
|
|
126
|
+
unary,
|
|
127
|
+
]
|
|
128
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
|
|
3
|
+
import numpy.typing
|
|
4
|
+
|
|
5
|
+
from . import _abstract
|
|
6
|
+
from . import methods
|
|
7
|
+
from . import mixins
|
|
8
|
+
from . import _typeface
|
|
9
|
+
from ._abstract import (
|
|
10
|
+
Quantity,
|
|
11
|
+
Object,
|
|
12
|
+
)
|
|
13
|
+
from ._exceptions import (
|
|
14
|
+
MetadataTypeError,
|
|
15
|
+
MetadataValueError,
|
|
16
|
+
OperationError,
|
|
17
|
+
)
|
|
18
|
+
from ._operations import (
|
|
19
|
+
unary,
|
|
20
|
+
equality,
|
|
21
|
+
ordering,
|
|
22
|
+
additive,
|
|
23
|
+
multiplicative,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
T = _typeface.TypeVar('T')
|
|
28
|
+
|
|
29
|
+
class Operand(_abstract.Object[T], mixins.Numpy):
|
|
30
|
+
"""A concrete implementation of a real-valued object."""
|
|
31
|
+
|
|
32
|
+
def __abs__(self) -> _typeface.Self:
|
|
33
|
+
"""Called for abs(self)."""
|
|
34
|
+
|
|
35
|
+
def __pos__(self) -> _typeface.Self:
|
|
36
|
+
"""Called for +self."""
|
|
37
|
+
|
|
38
|
+
def __neg__(self) -> _typeface.Self:
|
|
39
|
+
"""Called for -self."""
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other) -> bool:
|
|
42
|
+
"""Called for self == other."""
|
|
43
|
+
|
|
44
|
+
def __ne__(self, other) -> bool:
|
|
45
|
+
"""Called for self != other."""
|
|
46
|
+
|
|
47
|
+
def __lt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
48
|
+
"""Called for self < other."""
|
|
49
|
+
|
|
50
|
+
def __le__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
51
|
+
"""Called for self <= other."""
|
|
52
|
+
|
|
53
|
+
def __gt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
54
|
+
"""Called for self > other."""
|
|
55
|
+
|
|
56
|
+
def __ge__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
57
|
+
"""Called for self >= other."""
|
|
58
|
+
|
|
59
|
+
def __add__(self, other) -> _typeface.Self:
|
|
60
|
+
"""Called for self + other."""
|
|
61
|
+
|
|
62
|
+
def __radd__(self, other) -> _typeface.Self:
|
|
63
|
+
"""Called for other + self."""
|
|
64
|
+
|
|
65
|
+
def __sub__(self, other) -> _typeface.Self:
|
|
66
|
+
"""Called for self - other."""
|
|
67
|
+
|
|
68
|
+
def __rsub__(self, other) -> _typeface.Self:
|
|
69
|
+
"""Called for other - self."""
|
|
70
|
+
|
|
71
|
+
def __mul__(self, other) -> _typeface.Self:
|
|
72
|
+
"""Called for self * other."""
|
|
73
|
+
|
|
74
|
+
def __rmul__(self, other) -> _typeface.Self:
|
|
75
|
+
"""Called for other * self."""
|
|
76
|
+
|
|
77
|
+
def __truediv__(self, other) -> _typeface.Self:
|
|
78
|
+
"""Called for self / other."""
|
|
79
|
+
|
|
80
|
+
def __rtruediv__(self, other) -> _typeface.Self:
|
|
81
|
+
"""Called for other / self."""
|
|
82
|
+
|
|
83
|
+
def __floordiv__(self, other) -> _typeface.Self:
|
|
84
|
+
"""Called for self // other."""
|
|
85
|
+
|
|
86
|
+
def __rfloordiv__(self, other) -> _typeface.Self:
|
|
87
|
+
"""Called for other // self."""
|
|
88
|
+
|
|
89
|
+
def __mod__(self, other) -> _typeface.Self:
|
|
90
|
+
"""Called for self % other."""
|
|
91
|
+
|
|
92
|
+
def __rmod__(self, other) -> _typeface.Self:
|
|
93
|
+
"""Called for other % self."""
|
|
94
|
+
|
|
95
|
+
def __pow__(self, other) -> _typeface.Self:
|
|
96
|
+
"""Called for self ** other."""
|
|
97
|
+
|
|
98
|
+
def __rpow__(self, other):
|
|
99
|
+
"""Called for other ** self."""
|
|
100
|
+
|
|
101
|
+
def __array__(self, *args, **kwargs) -> numpy.typing.NDArray:
|
|
102
|
+
"""Called for numpy.array(self)."""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
# Modules
|
|
107
|
+
methods,
|
|
108
|
+
mixins,
|
|
109
|
+
# Object classes
|
|
110
|
+
Quantity,
|
|
111
|
+
Object,
|
|
112
|
+
# Functions
|
|
113
|
+
unary,
|
|
114
|
+
equality,
|
|
115
|
+
ordering,
|
|
116
|
+
additive,
|
|
117
|
+
multiplicative,
|
|
118
|
+
# Error classes
|
|
119
|
+
MetadataTypeError,
|
|
120
|
+
MetadataValueError,
|
|
121
|
+
OperationError,
|
|
122
|
+
]
|
|
123
|
+
|
|
@@ -4,10 +4,10 @@ import numbers
|
|
|
4
4
|
import numerical
|
|
5
5
|
import numpy.typing
|
|
6
6
|
|
|
7
|
-
from . import
|
|
7
|
+
from . import _typeface
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
DataType =
|
|
10
|
+
DataType = _typeface.TypeVar(
|
|
11
11
|
'DataType',
|
|
12
12
|
int,
|
|
13
13
|
float,
|
|
@@ -18,14 +18,14 @@ DataType = typeface.TypeVar(
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
@
|
|
22
|
-
class Quantity(numerical.Quantity[DataType],
|
|
21
|
+
@_typeface.runtime_checkable
|
|
22
|
+
class Quantity(numerical.Quantity[DataType], _typeface.Protocol):
|
|
23
23
|
"""Protocol for numerical objects with metadata."""
|
|
24
24
|
|
|
25
|
-
_meta: collections.abc.Mapping[str,
|
|
25
|
+
_meta: collections.abc.Mapping[str, _typeface.Any]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
class Object(numerical.
|
|
28
|
+
class Object(numerical.Object[DataType], numerical.Real):
|
|
29
29
|
"""A real-valued object with metadata attributes."""
|
|
30
30
|
|
|
31
31
|
def __init__(
|
|
@@ -35,7 +35,7 @@ class Object(numerical.Real, typeface.Generic[DataType]):
|
|
|
35
35
|
) -> None:
|
|
36
36
|
if not isinstance(__data, numerical.Real):
|
|
37
37
|
raise TypeError("Data input to Object must be real-valued")
|
|
38
|
-
|
|
38
|
+
super().__init__(__data)
|
|
39
39
|
self._meta = metadata
|
|
40
40
|
|
|
41
41
|
def __str__(self):
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class MetadataTypeError(TypeError):
|
|
2
|
+
"""A metadata-related TypeError occurred."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MetadataValueError(ValueError):
|
|
6
|
+
"""A metadata-related ValueError occurred."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OperationError(NotImplementedError):
|
|
10
|
+
"""A metadata attribute does not support this operation.
|
|
11
|
+
|
|
12
|
+
The default behavior when applying an operator to a metadata attribute of
|
|
13
|
+
`~Operand` is to copy the current value if the attribute does not define the
|
|
14
|
+
operation. Custom metadata attributes may raise this exception to indicate
|
|
15
|
+
that attempting to apply the operator is truly an error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from numerical import operators
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from ._abstract import (
|
|
4
4
|
Quantity,
|
|
5
5
|
Object,
|
|
6
6
|
)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MetadataValueError(ValueError):
|
|
14
|
-
"""A metadata-related ValueError occurred."""
|
|
7
|
+
from ._exceptions import (
|
|
8
|
+
MetadataTypeError,
|
|
9
|
+
MetadataValueError,
|
|
10
|
+
)
|
|
15
11
|
|
|
16
12
|
|
|
17
13
|
def _build_error_message(
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
|
|
3
|
+
import numerical
|
|
4
|
+
|
|
5
|
+
from ._abstract import Quantity
|
|
6
|
+
from ._exceptions import OperationError
|
|
7
|
+
from . import _typeface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
T = _typeface.TypeVar('T')
|
|
11
|
+
|
|
12
|
+
class Real:
|
|
13
|
+
"""Mixin for adding basic real-valued operator support."""
|
|
14
|
+
|
|
15
|
+
def __abs__(self):
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def __pos__(self):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __neg__(self):
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
def __ne__(self, other):
|
|
28
|
+
return not (self == other)
|
|
29
|
+
|
|
30
|
+
def __lt__(self, other):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def __le__(self, other):
|
|
34
|
+
return (self < other) and (self == other)
|
|
35
|
+
|
|
36
|
+
def __gt__(self, other):
|
|
37
|
+
return not (self <= other)
|
|
38
|
+
|
|
39
|
+
def __ge__(self, other):
|
|
40
|
+
return not (self < other)
|
|
41
|
+
|
|
42
|
+
def __add__(self, other):
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def __radd__(self, other):
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __sub__(self, other):
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __rsub__(self, other):
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __mul__(self, other):
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def __rmul__(self, other):
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __truediv__(self, other):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __rtruediv__(self, other):
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def __floordiv__(self, other):
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __rfloordiv__(self, other):
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __mod__(self, other):
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __rmod__(self, other):
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __pow__(self, other):
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def __rpow__(self, other):
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
UserFunction = collections.abc.Callable[..., T]
|
|
86
|
+
|
|
87
|
+
class NumpyMixin(numerical.mixins.NumpyMixin):
|
|
88
|
+
"""Mixin for adding `numpy` support to objects with metadata.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
- This class extends `numerical.mixins.NumpyMixin`. See that class for
|
|
93
|
+
further documentation.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
97
|
+
data = super()._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
98
|
+
f = getattr(ufunc, method)
|
|
99
|
+
types = [type(arg) for arg in args]
|
|
100
|
+
try:
|
|
101
|
+
meta = self._apply_operator_to_metadata(f, *args, **kwargs)
|
|
102
|
+
except OperationError as err:
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"Cannot apply numpy.{ufunc} to arguments with type(s) {types}"
|
|
105
|
+
) from err
|
|
106
|
+
if method != 'at':
|
|
107
|
+
return data, meta
|
|
108
|
+
|
|
109
|
+
def _apply_function(self, func, types, args, kwargs):
|
|
110
|
+
data = super()._apply_function(func, types, args, kwargs)
|
|
111
|
+
try:
|
|
112
|
+
meta = self._apply_operator_to_metadata(func, *args, **kwargs)
|
|
113
|
+
except OperationError as err:
|
|
114
|
+
raise TypeError(
|
|
115
|
+
f"Cannot apply numpy.{func} to arguments with type(s) {types}"
|
|
116
|
+
) from err
|
|
117
|
+
return data, meta
|
|
118
|
+
|
|
119
|
+
def _apply_operator_to_metadata(self, f, *args, **kwargs):
|
|
120
|
+
"""Apply a numpy universal or public function to arguments."""
|
|
121
|
+
keys = {
|
|
122
|
+
k
|
|
123
|
+
for x in args
|
|
124
|
+
if isinstance(x, Quantity)
|
|
125
|
+
for k in x._meta.keys()
|
|
126
|
+
}
|
|
127
|
+
meta = {}
|
|
128
|
+
for key in keys:
|
|
129
|
+
values = [
|
|
130
|
+
x._meta[key]
|
|
131
|
+
if isinstance(x, Quantity) else x
|
|
132
|
+
for x in args
|
|
133
|
+
]
|
|
134
|
+
try:
|
|
135
|
+
result = f(*values, **kwargs)
|
|
136
|
+
except TypeError as err:
|
|
137
|
+
if len(values) > 1 and any(v != values[0] for v in values):
|
|
138
|
+
raise err # TODO: More descriptive error message
|
|
139
|
+
else:
|
|
140
|
+
meta[key] = values[0]
|
|
141
|
+
except OperationError as err:
|
|
142
|
+
raise TypeError(
|
|
143
|
+
f"Attribute {key!r} does not support this operation"
|
|
144
|
+
) from err
|
|
145
|
+
else:
|
|
146
|
+
meta[key] = result
|
|
147
|
+
return meta.copy()
|
|
148
|
+
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import numbers
|
|
3
3
|
|
|
4
|
+
import numerical
|
|
4
5
|
import numpy
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
import oprattr
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class Symbol(
|
|
11
|
+
class Symbol(numerical.mixins.NumpyMixin):
|
|
11
12
|
"""A symbolic test attribute."""
|
|
12
13
|
|
|
13
14
|
def __init__(self, __x: str):
|
|
@@ -157,11 +158,6 @@ def symbol_gradient(x: Symbol, **kwargs):
|
|
|
157
158
|
return f"numpy.gradient({x})"
|
|
158
159
|
|
|
159
160
|
|
|
160
|
-
@Symbol.implementation(numpy.trapezoid)
|
|
161
|
-
def symbol_trapezoid(x: Symbol, **kwargs):
|
|
162
|
-
return f"numpy.trapezoid({x})"
|
|
163
|
-
|
|
164
|
-
|
|
165
161
|
def symbolic_binary(a, op, b):
|
|
166
162
|
if isinstance(a, (Symbol, str)) and isinstance(b, (Symbol, str)):
|
|
167
163
|
return f"{a} {op} {b}"
|
|
@@ -502,26 +498,29 @@ def test_gradient():
|
|
|
502
498
|
grad = numpy.gradient(v)
|
|
503
499
|
for t, g in zip(that, grad):
|
|
504
500
|
assert isinstance(t, oprattr.Operand)
|
|
505
|
-
assert numpy.array_equal(t,
|
|
501
|
+
assert numpy.array_equal(t, g)
|
|
502
|
+
assert t._meta['name'] == nR
|
|
506
503
|
for axis in range(v.ndim):
|
|
507
504
|
that = numpy.gradient(this, axis=axis)
|
|
508
505
|
assert isinstance(that, oprattr.Operand)
|
|
509
506
|
grad = numpy.gradient(v, axis=axis)
|
|
510
|
-
assert numpy.array_equal(that,
|
|
507
|
+
assert numpy.array_equal(that, grad)
|
|
508
|
+
assert t._meta['name'] == nR
|
|
511
509
|
|
|
512
510
|
|
|
513
511
|
def test_trapezoid():
|
|
514
|
-
"""Test `numpy.trapezoid
|
|
512
|
+
"""Test `numpy.trapezoid`, which `Symbol` does not implement."""
|
|
515
513
|
nA = Symbol('A')
|
|
516
|
-
nR = Symbol('numpy.trapezoid(A)')
|
|
517
514
|
v = numpy.arange(3*4*5).reshape(3, 4, 5)
|
|
518
515
|
this = x(v, name=nA)
|
|
519
516
|
that = numpy.trapezoid(this)
|
|
520
517
|
assert isinstance(that, oprattr.Operand)
|
|
521
|
-
assert numpy.array_equal(that,
|
|
518
|
+
assert numpy.array_equal(that, numpy.trapezoid(v))
|
|
519
|
+
assert that._meta['name'] == nA
|
|
522
520
|
for axis in range(v.ndim):
|
|
523
521
|
that = numpy.trapezoid(this, axis=axis)
|
|
524
522
|
assert isinstance(that, oprattr.Operand)
|
|
525
523
|
trap = numpy.trapezoid(v, axis=axis)
|
|
526
|
-
assert numpy.array_equal(that,
|
|
524
|
+
assert numpy.array_equal(that, trap)
|
|
525
|
+
assert that._meta['name'] == nA
|
|
527
526
|
|
|
@@ -108,8 +108,8 @@ wheels = [
|
|
|
108
108
|
|
|
109
109
|
[[package]]
|
|
110
110
|
name = "numerical"
|
|
111
|
-
version = "0.
|
|
112
|
-
source = { git = "https://github.com/myoung-space-science/numerical?rev=v0.
|
|
111
|
+
version = "0.3.0"
|
|
112
|
+
source = { git = "https://github.com/myoung-space-science/numerical?rev=v0.3.0#b4a46e5e93e8d256c1198aec22d5556ea041673b" }
|
|
113
113
|
dependencies = [
|
|
114
114
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
115
115
|
{ name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
@@ -267,7 +267,7 @@ wheels = [
|
|
|
267
267
|
|
|
268
268
|
[[package]]
|
|
269
269
|
name = "oprattr"
|
|
270
|
-
version = "0.
|
|
270
|
+
version = "0.7.0"
|
|
271
271
|
source = { editable = "." }
|
|
272
272
|
dependencies = [
|
|
273
273
|
{ name = "numerical" },
|
|
@@ -284,7 +284,7 @@ dev = [
|
|
|
284
284
|
|
|
285
285
|
[package.metadata]
|
|
286
286
|
requires-dist = [
|
|
287
|
-
{ name = "numerical", git = "https://github.com/myoung-space-science/numerical?rev=v0.
|
|
287
|
+
{ name = "numerical", git = "https://github.com/myoung-space-science/numerical?rev=v0.3.0" },
|
|
288
288
|
{ name = "numpy", specifier = ">=2.2.1" },
|
|
289
289
|
{ name = "scipy", specifier = ">=1.15.0" },
|
|
290
290
|
]
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import collections.abc
|
|
2
|
-
import functools
|
|
3
|
-
|
|
4
|
-
import numpy
|
|
5
|
-
|
|
6
|
-
from . import abstract
|
|
7
|
-
from . import methods
|
|
8
|
-
from . import mixins
|
|
9
|
-
from . import typeface
|
|
10
|
-
from ._operations import equality
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
T = typeface.TypeVar('T')
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Operand(abstract.Object[T], mixins.Numpy):
|
|
17
|
-
"""A concrete implementation of a real-valued object."""
|
|
18
|
-
|
|
19
|
-
__abs__ = methods.__abs__
|
|
20
|
-
__pos__ = methods.__pos__
|
|
21
|
-
__neg__ = methods.__neg__
|
|
22
|
-
|
|
23
|
-
__eq__ = methods.__eq__
|
|
24
|
-
__ne__ = methods.__ne__
|
|
25
|
-
__lt__ = methods.__lt__
|
|
26
|
-
__le__ = methods.__le__
|
|
27
|
-
__gt__ = methods.__gt__
|
|
28
|
-
__ge__ = methods.__ge__
|
|
29
|
-
|
|
30
|
-
__add__ = methods.__add__
|
|
31
|
-
__radd__ = methods.__radd__
|
|
32
|
-
__sub__ = methods.__sub__
|
|
33
|
-
__rsub__ = methods.__rsub__
|
|
34
|
-
__mul__ = methods.__mul__
|
|
35
|
-
__rmul__ = methods.__rmul__
|
|
36
|
-
__truediv__ = methods.__truediv__
|
|
37
|
-
__rtruediv__ = methods.__rtruediv__
|
|
38
|
-
__floordiv__ = methods.__floordiv__
|
|
39
|
-
__rfloordiv__ = methods.__rfloordiv__
|
|
40
|
-
__mod__ = methods.__mod__
|
|
41
|
-
__rmod__ = methods.__rmod__
|
|
42
|
-
__pow__ = methods.__pow__
|
|
43
|
-
__rpow__ = methods.__rpow__
|
|
44
|
-
|
|
45
|
-
def __array__(self, *args, **kwargs):
|
|
46
|
-
"""Called for numpy.array(self)."""
|
|
47
|
-
return numpy.array(self._data, *args, **kwargs)
|
|
48
|
-
|
|
49
|
-
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
50
|
-
if ufunc in (numpy.equal, numpy.not_equal):
|
|
51
|
-
# NOTE: We are probably here because the left operand is a
|
|
52
|
-
# `numpy.ndarray`, which would otherwise take control and return the
|
|
53
|
-
# pure `numpy` result.
|
|
54
|
-
f = getattr(ufunc, method)
|
|
55
|
-
return equality(f, *args)
|
|
56
|
-
return super()._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@Operand.implementation(numpy.array_equal)
|
|
60
|
-
def array_equal(
|
|
61
|
-
x: numpy.typing.ArrayLike,
|
|
62
|
-
y: numpy.typing.ArrayLike,
|
|
63
|
-
**kwargs
|
|
64
|
-
) -> bool:
|
|
65
|
-
"""Called for numpy.array_equal(x, y)"""
|
|
66
|
-
return numpy.array_equal(numpy.array(x), numpy.array(y), **kwargs)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@Operand.implementation(numpy.gradient)
|
|
70
|
-
def gradient(x: Operand[T], *args, **kwargs):
|
|
71
|
-
"""Called for numpy.gradient(x)."""
|
|
72
|
-
data = numpy.gradient(x._data, *args, **kwargs)
|
|
73
|
-
meta = {}
|
|
74
|
-
for key, value in x._meta.items():
|
|
75
|
-
try:
|
|
76
|
-
v = numpy.gradient(value, **kwargs)
|
|
77
|
-
except TypeError as exc:
|
|
78
|
-
raise TypeError(
|
|
79
|
-
"Cannot compute numpy.gradient(x)"
|
|
80
|
-
f" because metadata attribute {key!r}"
|
|
81
|
-
" does not support this operation"
|
|
82
|
-
) from exc
|
|
83
|
-
else:
|
|
84
|
-
meta[key] = v
|
|
85
|
-
if isinstance(data, (list, tuple)):
|
|
86
|
-
r = [type(x)(array, **meta) for array in data]
|
|
87
|
-
if isinstance(data, tuple):
|
|
88
|
-
return tuple(r)
|
|
89
|
-
return r
|
|
90
|
-
return type(x)(data, **meta)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def wrapnumpy(f: collections.abc.Callable):
|
|
94
|
-
"""Implement a numpy function for objects with metadata."""
|
|
95
|
-
@functools.wraps(f)
|
|
96
|
-
def method(x: Operand[T], **kwargs):
|
|
97
|
-
"""Apply a numpy function to x."""
|
|
98
|
-
data = f(x._data, **kwargs)
|
|
99
|
-
meta = {}
|
|
100
|
-
for key, value in x._meta.items():
|
|
101
|
-
try:
|
|
102
|
-
v = f(value, **kwargs)
|
|
103
|
-
except TypeError as exc:
|
|
104
|
-
raise TypeError(
|
|
105
|
-
f"Cannot compute numpy.{f.__qualname__}(x)"
|
|
106
|
-
f" because metadata attribute {key!r}"
|
|
107
|
-
" does not support this operation"
|
|
108
|
-
) from exc
|
|
109
|
-
else:
|
|
110
|
-
meta[key] = v
|
|
111
|
-
return type(x)(data, **meta)
|
|
112
|
-
return method
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
_OPERAND_UFUNCS = (
|
|
116
|
-
numpy.sqrt,
|
|
117
|
-
numpy.sin,
|
|
118
|
-
numpy.cos,
|
|
119
|
-
numpy.tan,
|
|
120
|
-
numpy.log,
|
|
121
|
-
numpy.log2,
|
|
122
|
-
numpy.log10,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
_OPERAND_FUNCTIONS = (
|
|
127
|
-
numpy.squeeze,
|
|
128
|
-
numpy.mean,
|
|
129
|
-
numpy.sum,
|
|
130
|
-
numpy.cumsum,
|
|
131
|
-
numpy.transpose,
|
|
132
|
-
numpy.trapezoid,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for f in _OPERAND_UFUNCS + _OPERAND_FUNCTIONS:
|
|
137
|
-
Operand.implement(f, wrapnumpy(f))
|
|
138
|
-
|
|
139
|
-
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import numpy.typing
|
|
2
|
-
|
|
3
|
-
from . import abstract
|
|
4
|
-
from . import mixins
|
|
5
|
-
from . import typeface
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
T = typeface.TypeVar('T')
|
|
9
|
-
|
|
10
|
-
class Operand(abstract.Object[T], mixins.Numpy):
|
|
11
|
-
"""A concrete implementation of a real-valued object."""
|
|
12
|
-
|
|
13
|
-
def __abs__(self) -> typeface.Self:
|
|
14
|
-
"""Called for abs(self)."""
|
|
15
|
-
|
|
16
|
-
def __pos__(self) -> typeface.Self:
|
|
17
|
-
"""Called for +self."""
|
|
18
|
-
|
|
19
|
-
def __neg__(self) -> typeface.Self:
|
|
20
|
-
"""Called for -self."""
|
|
21
|
-
|
|
22
|
-
def __eq__(self, other) -> bool:
|
|
23
|
-
"""Called for self == other."""
|
|
24
|
-
|
|
25
|
-
def __ne__(self, other) -> bool:
|
|
26
|
-
"""Called for self != other."""
|
|
27
|
-
|
|
28
|
-
def __lt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
29
|
-
"""Called for self < other."""
|
|
30
|
-
|
|
31
|
-
def __le__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
32
|
-
"""Called for self <= other."""
|
|
33
|
-
|
|
34
|
-
def __gt__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
35
|
-
"""Called for self > other."""
|
|
36
|
-
|
|
37
|
-
def __ge__(self, other) -> bool | numpy.typing.NDArray[numpy.bool]:
|
|
38
|
-
"""Called for self >= other."""
|
|
39
|
-
|
|
40
|
-
def __add__(self, other) -> typeface.Self:
|
|
41
|
-
"""Called for self + other."""
|
|
42
|
-
|
|
43
|
-
def __radd__(self, other) -> typeface.Self:
|
|
44
|
-
"""Called for other + self."""
|
|
45
|
-
|
|
46
|
-
def __sub__(self, other) -> typeface.Self:
|
|
47
|
-
"""Called for self - other."""
|
|
48
|
-
|
|
49
|
-
def __rsub__(self, other) -> typeface.Self:
|
|
50
|
-
"""Called for other - self."""
|
|
51
|
-
|
|
52
|
-
def __mul__(self, other) -> typeface.Self:
|
|
53
|
-
"""Called for self * other."""
|
|
54
|
-
|
|
55
|
-
def __rmul__(self, other) -> typeface.Self:
|
|
56
|
-
"""Called for other * self."""
|
|
57
|
-
|
|
58
|
-
def __truediv__(self, other) -> typeface.Self:
|
|
59
|
-
"""Called for self / other."""
|
|
60
|
-
|
|
61
|
-
def __rtruediv__(self, other) -> typeface.Self:
|
|
62
|
-
"""Called for other / self."""
|
|
63
|
-
|
|
64
|
-
def __floordiv__(self, other) -> typeface.Self:
|
|
65
|
-
"""Called for self // other."""
|
|
66
|
-
|
|
67
|
-
def __rfloordiv__(self, other) -> typeface.Self:
|
|
68
|
-
"""Called for other // self."""
|
|
69
|
-
|
|
70
|
-
def __mod__(self, other) -> typeface.Self:
|
|
71
|
-
"""Called for self % other."""
|
|
72
|
-
|
|
73
|
-
def __rmod__(self, other) -> typeface.Self:
|
|
74
|
-
"""Called for other % self."""
|
|
75
|
-
|
|
76
|
-
def __pow__(self, other) -> typeface.Self:
|
|
77
|
-
"""Called for self ** other."""
|
|
78
|
-
|
|
79
|
-
def __rpow__(self, other):
|
|
80
|
-
"""Called for other ** self."""
|
|
81
|
-
|
|
@@ -1,489 +0,0 @@
|
|
|
1
|
-
import collections.abc
|
|
2
|
-
import numbers
|
|
3
|
-
|
|
4
|
-
import numpy
|
|
5
|
-
|
|
6
|
-
from . import abstract
|
|
7
|
-
from . import typeface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
T = typeface.TypeVar('T')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Real:
|
|
14
|
-
"""Mixin for adding basic real-valued operator support."""
|
|
15
|
-
|
|
16
|
-
def __abs__(self):
|
|
17
|
-
return self
|
|
18
|
-
|
|
19
|
-
def __pos__(self):
|
|
20
|
-
return self
|
|
21
|
-
|
|
22
|
-
def __neg__(self):
|
|
23
|
-
return self
|
|
24
|
-
|
|
25
|
-
def __eq__(self, other):
|
|
26
|
-
return False
|
|
27
|
-
|
|
28
|
-
def __ne__(self, other):
|
|
29
|
-
return not (self == other)
|
|
30
|
-
|
|
31
|
-
def __lt__(self, other):
|
|
32
|
-
return False
|
|
33
|
-
|
|
34
|
-
def __le__(self, other):
|
|
35
|
-
return (self < other) and (self == other)
|
|
36
|
-
|
|
37
|
-
def __gt__(self, other):
|
|
38
|
-
return not (self <= other)
|
|
39
|
-
|
|
40
|
-
def __ge__(self, other):
|
|
41
|
-
return not (self < other)
|
|
42
|
-
|
|
43
|
-
def __add__(self, other):
|
|
44
|
-
return self
|
|
45
|
-
|
|
46
|
-
def __radd__(self, other):
|
|
47
|
-
return self
|
|
48
|
-
|
|
49
|
-
def __sub__(self, other):
|
|
50
|
-
return self
|
|
51
|
-
|
|
52
|
-
def __rsub__(self, other):
|
|
53
|
-
return self
|
|
54
|
-
|
|
55
|
-
def __mul__(self, other):
|
|
56
|
-
return self
|
|
57
|
-
|
|
58
|
-
def __rmul__(self, other):
|
|
59
|
-
return self
|
|
60
|
-
|
|
61
|
-
def __truediv__(self, other):
|
|
62
|
-
return self
|
|
63
|
-
|
|
64
|
-
def __rtruediv__(self, other):
|
|
65
|
-
return self
|
|
66
|
-
|
|
67
|
-
def __floordiv__(self, other):
|
|
68
|
-
return self
|
|
69
|
-
|
|
70
|
-
def __rfloordiv__(self, other):
|
|
71
|
-
return self
|
|
72
|
-
|
|
73
|
-
def __mod__(self, other):
|
|
74
|
-
return self
|
|
75
|
-
|
|
76
|
-
def __rmod__(self, other):
|
|
77
|
-
return self
|
|
78
|
-
|
|
79
|
-
def __pow__(self, other):
|
|
80
|
-
return self
|
|
81
|
-
|
|
82
|
-
def __rpow__(self, other):
|
|
83
|
-
return self
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
UserFunction = collections.abc.Callable[..., T]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class Numpy:
|
|
90
|
-
"""Mixin for adding support for `numpy` functions to numeric objects.
|
|
91
|
-
|
|
92
|
-
Classes that inherit from this class may implement support for `numpy`
|
|
93
|
-
universal functions ("ufuncs"; e.g., `numpy.sqrt`) by overloading
|
|
94
|
-
`_apply_ufunc`, and may implement support for `numpy` public functions
|
|
95
|
-
(e.g., `numpy.squeeze`) by overloading `_apply_function` and registering
|
|
96
|
-
individual function implementations via `implementation`.
|
|
97
|
-
|
|
98
|
-
It is important to note that the use cases of this class extend beyond
|
|
99
|
-
array-like objects. Both single- and multi-valued objects can benefit from
|
|
100
|
-
implementing support for `numpy` universal and public functions. For
|
|
101
|
-
example, it is possible to apply `numpy.sqrt` to both a real number and an
|
|
102
|
-
array
|
|
103
|
-
|
|
104
|
-
>>> numpy.sqrt(4)
|
|
105
|
-
2.0
|
|
106
|
-
>>> numpy.sqrt([4, 9])
|
|
107
|
-
array([2., 3.])
|
|
108
|
-
|
|
109
|
-
Even the trivial application of `numpy.mean` to a real number is defined:
|
|
110
|
-
|
|
111
|
-
>>> numpy.mean(2.5)
|
|
112
|
-
2.5
|
|
113
|
-
|
|
114
|
-
Notes
|
|
115
|
-
-----
|
|
116
|
-
- This class does not inherit from `numpy.lib.mixins.NDArrayOperatorsMixin`,
|
|
117
|
-
which implements most of the built-in Python numeric operators via
|
|
118
|
-
`__array_ufunc__`, because it assumes that subclasses independently
|
|
119
|
-
implement those methods.
|
|
120
|
-
"""
|
|
121
|
-
|
|
122
|
-
def __init_subclass__(cls):
|
|
123
|
-
cls._UFUNC_TYPES |= {cls}
|
|
124
|
-
cls._FUNCTION_TYPES |= {cls}
|
|
125
|
-
cls._FUNCTIONS = {}
|
|
126
|
-
|
|
127
|
-
_UFUNC_TYPES = {
|
|
128
|
-
numpy.ndarray,
|
|
129
|
-
numbers.Number,
|
|
130
|
-
list,
|
|
131
|
-
abstract.Quantity,
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
def __array_ufunc__(self, ufunc, method, *args, **kwargs):
|
|
135
|
-
"""Provide support for `numpy` universal functions.
|
|
136
|
-
|
|
137
|
-
See https://numpy.org/doc/stable/reference/arrays.classes.html for more
|
|
138
|
-
information on use of this special method.
|
|
139
|
-
|
|
140
|
-
Notes
|
|
141
|
-
-----
|
|
142
|
-
- This method first ensures that the input types (as well as the type of
|
|
143
|
-
`out`, if passed via keyword) are supported types. It then checks for
|
|
144
|
-
a custom implementation of `ufunc`. If there is a custom
|
|
145
|
-
implementation, this method applies it and returns the result. If
|
|
146
|
-
there is no custom implementation, this method passes control to
|
|
147
|
-
`_apply_ufunc`, to allow subclass customization.
|
|
148
|
-
- See `implementation` for additional guidance on custom
|
|
149
|
-
implementations.
|
|
150
|
-
|
|
151
|
-
See Also
|
|
152
|
-
--------
|
|
153
|
-
`implementation`
|
|
154
|
-
Class method for registering custom ufunc implementations.
|
|
155
|
-
|
|
156
|
-
`_apply_ufunc`
|
|
157
|
-
Instance method that allows custom handling of ufuncs corresponding
|
|
158
|
-
to standard Python numerical operators.
|
|
159
|
-
"""
|
|
160
|
-
out = kwargs.get('out', ())
|
|
161
|
-
accepted = tuple(self._UFUNC_TYPES)
|
|
162
|
-
if not all(isinstance(x, accepted) for x in args + out):
|
|
163
|
-
return NotImplemented
|
|
164
|
-
if out:
|
|
165
|
-
kwargs['out'] = tuple(
|
|
166
|
-
x._data if isinstance(x, abstract.Quantity)
|
|
167
|
-
else x for x in out
|
|
168
|
-
)
|
|
169
|
-
if self._implements(ufunc):
|
|
170
|
-
operator = self._FUNCTIONS[ufunc]
|
|
171
|
-
return operator(*args, **kwargs)
|
|
172
|
-
return self._apply_ufunc(ufunc, method, *args, **kwargs)
|
|
173
|
-
|
|
174
|
-
def _apply_ufunc(self, ufunc, method, *args, **kwargs):
|
|
175
|
-
"""Apply a `numpy` universal function (a.k.a "ufunc") to data.
|
|
176
|
-
|
|
177
|
-
Notes
|
|
178
|
-
-----
|
|
179
|
-
- Subclasses that wish to customize support for ufuncs should overload
|
|
180
|
-
this method instead of `__array_ufunc__`.
|
|
181
|
-
- Subclasses should prefer to define custom implementations of specific
|
|
182
|
-
universal functions and register each via `implementation`, rather
|
|
183
|
-
than implementing function-specific logic in this method, since
|
|
184
|
-
`__array_ufunc__` will check for a custom implementation of a given
|
|
185
|
-
function before calling this method.
|
|
186
|
-
- The default implementation of this method applies the given ufunc to
|
|
187
|
-
real-valued data and directly returns the `numpy` result, without
|
|
188
|
-
attempting to create a new instance of the custom subclass.
|
|
189
|
-
|
|
190
|
-
See Also
|
|
191
|
-
--------
|
|
192
|
-
`implementation`
|
|
193
|
-
Class method for registering custom ufunc implementations.
|
|
194
|
-
|
|
195
|
-
`__array_ufunc__`
|
|
196
|
-
The entry point for `numpy` universal functions.
|
|
197
|
-
"""
|
|
198
|
-
operator = getattr(ufunc, method)
|
|
199
|
-
values = self._get_numpy_args(args)
|
|
200
|
-
try:
|
|
201
|
-
data = operator(*values, **kwargs)
|
|
202
|
-
except TypeError as err:
|
|
203
|
-
raise TypeError(
|
|
204
|
-
f"Unable to apply {ufunc} to {args}"
|
|
205
|
-
) from err
|
|
206
|
-
if method != 'at':
|
|
207
|
-
return data
|
|
208
|
-
|
|
209
|
-
_FUNCTION_TYPES = {
|
|
210
|
-
numpy.ndarray,
|
|
211
|
-
abstract.Object,
|
|
212
|
-
} | set(numpy.ScalarType)
|
|
213
|
-
|
|
214
|
-
def __array_function__(self, func, types, args, kwargs):
|
|
215
|
-
"""Provide support for functions in the `numpy` public API.
|
|
216
|
-
|
|
217
|
-
See https://numpy.org/doc/stable/reference/arrays.classes.html for more
|
|
218
|
-
information of use of this special method. The implementation shown here
|
|
219
|
-
is a combination of the example on that page and code from the
|
|
220
|
-
definition of `EncapsulateNDArray.__array_function__` in
|
|
221
|
-
https://github.com/dask/dask/blob/main/dask/array/tests/test_dispatch.py
|
|
222
|
-
|
|
223
|
-
Notes
|
|
224
|
-
-----
|
|
225
|
-
- This method first checks that all `types` are in
|
|
226
|
-
`self._FUNCTION_TYPES`, thereby allowing subclasses that don't
|
|
227
|
-
override `__array_function__` to handle objects of this type. It then
|
|
228
|
-
checks for a custom implementation of `func`. If there is a custom
|
|
229
|
-
implementation, this method applies it and returns the result. If
|
|
230
|
-
there is no custom implementation, this method passes control to
|
|
231
|
-
`_apply_function`, to allow subclass customization.
|
|
232
|
-
- See `implementation` for additional guidance on custom
|
|
233
|
-
implementations.
|
|
234
|
-
|
|
235
|
-
See Also
|
|
236
|
-
--------
|
|
237
|
-
`implementation`
|
|
238
|
-
Class method for registering custom function implementations.
|
|
239
|
-
|
|
240
|
-
`_apply_function`
|
|
241
|
-
Instance method that allows custom handling of `numpy` public
|
|
242
|
-
functions when there is no registered custom implementation.
|
|
243
|
-
"""
|
|
244
|
-
accepted = tuple(self._FUNCTION_TYPES)
|
|
245
|
-
if not all(issubclass(ti, accepted) for ti in types):
|
|
246
|
-
return NotImplemented
|
|
247
|
-
if self._implements(func):
|
|
248
|
-
return self._FUNCTIONS[func](*args, **kwargs)
|
|
249
|
-
return self._apply_function(func, types, args, kwargs)
|
|
250
|
-
|
|
251
|
-
def _apply_function(self, func, types, args, kwargs):
|
|
252
|
-
"""Apply a function in the `numpy` public API.
|
|
253
|
-
|
|
254
|
-
Notes
|
|
255
|
-
-----
|
|
256
|
-
- Subclasses that wish to customize support for public functions should
|
|
257
|
-
overload this method instead of `__array_function__`.
|
|
258
|
-
- Subclasses should prefer to define custom implementations of specific
|
|
259
|
-
public functions and register each via `implementation`, rather than
|
|
260
|
-
implementing function-specific logic in this method, since
|
|
261
|
-
`__array_function__` will check for a custom implementation of a given
|
|
262
|
-
function before calling this method.
|
|
263
|
-
- The default implementation calls `_get_numpy_array` for access to
|
|
264
|
-
real-valued data via an instance of `numpy.ndarray`, `_get_numpy_args`
|
|
265
|
-
to convert `args` to appropriate operands, and `_get_numpy_types` to
|
|
266
|
-
extract appropriate operand types. Subclasses may choose to overload
|
|
267
|
-
any of those individual methods instead of overloading this method.
|
|
268
|
-
|
|
269
|
-
See Also
|
|
270
|
-
--------
|
|
271
|
-
`implementation`
|
|
272
|
-
Class method for registering custom ufunc implementations.
|
|
273
|
-
|
|
274
|
-
`__array_function__`
|
|
275
|
-
The entry point for `numpy` public functions.
|
|
276
|
-
"""
|
|
277
|
-
array = self._get_numpy_array()
|
|
278
|
-
if array is None:
|
|
279
|
-
return NotImplemented
|
|
280
|
-
if not isinstance(array, numpy.ndarray):
|
|
281
|
-
raise TypeError(
|
|
282
|
-
f"{self.__class__.__qualname__}._get_numpy_array"
|
|
283
|
-
" did not return a numpy.ndarray"
|
|
284
|
-
) from None
|
|
285
|
-
args = self._get_numpy_args(args)
|
|
286
|
-
types = self._get_numpy_types(types)
|
|
287
|
-
return array.__array_function__(func, types, args, kwargs)
|
|
288
|
-
|
|
289
|
-
def _get_numpy_array(self) -> numpy.typing.NDArray | None:
|
|
290
|
-
"""Convert the data interface to an array for `numpy` mixin methods.
|
|
291
|
-
|
|
292
|
-
Notes
|
|
293
|
-
-----
|
|
294
|
-
- This method allows subclass implementations to control how they
|
|
295
|
-
convert their data interface to a `numpy.ndarray` for use with `numpy`
|
|
296
|
-
public functions.
|
|
297
|
-
- Returning `None` from this method will cause `_apply_function` to
|
|
298
|
-
return `NotImplemented`.
|
|
299
|
-
- The default implementation unconditionally returns `None`.
|
|
300
|
-
"""
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
def _get_numpy_args(self, args):
|
|
304
|
-
"""Convert `args` to operands of a `numpy` function.
|
|
305
|
-
|
|
306
|
-
This method will call `~_get_arg_data` on each member of `args` in order
|
|
307
|
-
to build a `tuple` of suitable operands. Subclasses may overload
|
|
308
|
-
`~_get_arg_data` to customize access to their data attribute.
|
|
309
|
-
"""
|
|
310
|
-
return tuple(self._get_arg_data(arg) for arg in args)
|
|
311
|
-
|
|
312
|
-
def _get_arg_data(self, arg):
|
|
313
|
-
"""Convert `arg` to an operand of a `numpy` function.
|
|
314
|
-
|
|
315
|
-
See Also
|
|
316
|
-
--------
|
|
317
|
-
`~_get_numpy_args`
|
|
318
|
-
The method that calls this method in a loop.
|
|
319
|
-
|
|
320
|
-
Notes
|
|
321
|
-
-----
|
|
322
|
-
- This method allows a subclass to customize how `numpy` functions
|
|
323
|
-
access its data attribute.
|
|
324
|
-
- The default implementation will return the `data` attribute of a of
|
|
325
|
-
`arg` if `arg` is an instance of the base object class; otherwise, it
|
|
326
|
-
will return the unmodified argument.
|
|
327
|
-
"""
|
|
328
|
-
if isinstance(arg, abstract.Quantity):
|
|
329
|
-
return arg._data
|
|
330
|
-
return arg
|
|
331
|
-
|
|
332
|
-
def _get_numpy_types(self, types):
|
|
333
|
-
"""Extract appropriate types for a `numpy` function.
|
|
334
|
-
|
|
335
|
-
Notes
|
|
336
|
-
-----
|
|
337
|
-
- This method allows subclasses to restrict the object types that they
|
|
338
|
-
pass to `numpy` public functions via `_apply_function`.
|
|
339
|
-
- The default implementation returns a tuple that contains all types
|
|
340
|
-
except for subtypes of `~_types.Quantity`.
|
|
341
|
-
"""
|
|
342
|
-
return tuple(
|
|
343
|
-
ti for ti in types
|
|
344
|
-
if not issubclass(ti, abstract.Object)
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
@classmethod
|
|
348
|
-
def _implements(cls, operation: collections.abc.Callable):
|
|
349
|
-
"""True if this class defines a custom implementation for `operation`.
|
|
350
|
-
|
|
351
|
-
This is a helper methods that gracefully handles the case in which a
|
|
352
|
-
subclass does not support custom operator implementations.
|
|
353
|
-
"""
|
|
354
|
-
try:
|
|
355
|
-
result = operation in cls._FUNCTIONS
|
|
356
|
-
except TypeError:
|
|
357
|
-
return False
|
|
358
|
-
return result
|
|
359
|
-
|
|
360
|
-
_FUNCTIONS: dict[str, collections.abc.Callable]=None
|
|
361
|
-
"""Internal collection of custom `numpy` function implementations."""
|
|
362
|
-
|
|
363
|
-
@classmethod
|
|
364
|
-
def implementation(cls, numpy_function: collections.abc.Callable, /):
|
|
365
|
-
"""Register a custom implementation of this `numpy` function.
|
|
366
|
-
|
|
367
|
-
Parameters
|
|
368
|
-
----------
|
|
369
|
-
numpy_function : callable
|
|
370
|
-
The `numpy` universal or public function to implement.
|
|
371
|
-
|
|
372
|
-
Notes
|
|
373
|
-
-----
|
|
374
|
-
- Users may register `numpy` universal functions (a.k.a. ufuncs;
|
|
375
|
-
https://numpy.org/doc/stable/reference/ufuncs.html) as well as
|
|
376
|
-
functions in the public `numpy` API (e.g., `numpy.mean`). This may be
|
|
377
|
-
important if, for example, a class needs to implement a custom version
|
|
378
|
-
of `numpy.sqrt`, which is a ufunc.
|
|
379
|
-
- See https://numpy.org/doc/stable/reference/arrays.classes.html for the
|
|
380
|
-
suggestion on which this method is based.
|
|
381
|
-
|
|
382
|
-
Examples
|
|
383
|
-
--------
|
|
384
|
-
Overload `numpy.mean` for an existing class called `Array` with a
|
|
385
|
-
version that accepts no keyword arguments:
|
|
386
|
-
|
|
387
|
-
```
|
|
388
|
-
@Array.implementation(numpy.mean)
|
|
389
|
-
def mean(a: Array, **kwargs) -> Array:
|
|
390
|
-
if kwargs:
|
|
391
|
-
msg = "Cannot pass keywords to numpy.mean with Array" raise
|
|
392
|
-
TypeError(msg)
|
|
393
|
-
return numpy.sum(a) / len(a)
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
This will compute the mean of the underlying data when called with no
|
|
397
|
-
arguments, but will raise an exception when called with arguments:
|
|
398
|
-
|
|
399
|
-
>>> v = Array([[1, 2], [3, 4]])
|
|
400
|
-
>>> numpy.mean(v)
|
|
401
|
-
5.0
|
|
402
|
-
>>> numpy.mean(v, axis=0)
|
|
403
|
-
...
|
|
404
|
-
TypeError: Cannot pass keywords to numpy.mean with Array
|
|
405
|
-
|
|
406
|
-
See Also
|
|
407
|
-
--------
|
|
408
|
-
`~implements`
|
|
409
|
-
"""
|
|
410
|
-
if not callable(numpy_function):
|
|
411
|
-
raise TypeError(
|
|
412
|
-
"The target operation of a custom numpy implementation"
|
|
413
|
-
" must be callable"
|
|
414
|
-
) from None
|
|
415
|
-
def decorator(user_function: UserFunction):
|
|
416
|
-
if cls._FUNCTIONS is None:
|
|
417
|
-
raise NotImplementedError(
|
|
418
|
-
f"Type {cls} does not support custom implementations"
|
|
419
|
-
" of numpy functions"
|
|
420
|
-
) from None
|
|
421
|
-
cls._FUNCTIONS[numpy_function] = user_function
|
|
422
|
-
return user_function
|
|
423
|
-
return decorator
|
|
424
|
-
|
|
425
|
-
@classmethod
|
|
426
|
-
def implement(
|
|
427
|
-
cls,
|
|
428
|
-
numpy_function: collections.abc.Callable,
|
|
429
|
-
user_function: UserFunction,
|
|
430
|
-
/,
|
|
431
|
-
) -> None:
|
|
432
|
-
"""Implement a `numpy` function via a given user function.
|
|
433
|
-
|
|
434
|
-
This method serves as an alternative to the class method
|
|
435
|
-
`implementation`, which is primarily meant to be used as a decorator.
|
|
436
|
-
This method allows the user to directly associate a custom
|
|
437
|
-
implementation with the target `numpy` function.
|
|
438
|
-
|
|
439
|
-
Parameters
|
|
440
|
-
----------
|
|
441
|
-
numpy_function : callable
|
|
442
|
-
The `numpy` universal or public function to implement.
|
|
443
|
-
|
|
444
|
-
user_function: callable
|
|
445
|
-
The custom implementation to associate with `numpy_function`.
|
|
446
|
-
|
|
447
|
-
Examples
|
|
448
|
-
--------
|
|
449
|
-
Here is an alternative to the `~implementation` example usage:
|
|
450
|
-
|
|
451
|
-
```
|
|
452
|
-
def mean(a: Array, **kwargs) -> Array:
|
|
453
|
-
if kwargs:
|
|
454
|
-
msg = "Cannot pass keywords to numpy.mean with Array" raise
|
|
455
|
-
TypeError(msg)
|
|
456
|
-
return numpy.sum(a) / len(a)
|
|
457
|
-
|
|
458
|
-
Array.implement(numpy.mean, mean)
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
However, a more useful application may be to associate multiple `numpy`
|
|
462
|
-
functions with a single custom implementation:
|
|
463
|
-
|
|
464
|
-
```
|
|
465
|
-
def trig(f: numpy.ufunc):
|
|
466
|
-
def method(a: Array):
|
|
467
|
-
... # custom implementation
|
|
468
|
-
return method
|
|
469
|
-
|
|
470
|
-
for f in {numpy.sin, numpy.cos, numpy.tan}:
|
|
471
|
-
Array.implement(f, trig(f))
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
See Also
|
|
475
|
-
--------
|
|
476
|
-
`~implementation`
|
|
477
|
-
"""
|
|
478
|
-
if not callable(numpy_function):
|
|
479
|
-
raise TypeError(
|
|
480
|
-
"The target operation of a custom numpy implementation"
|
|
481
|
-
" must be callable"
|
|
482
|
-
) from None
|
|
483
|
-
if cls._FUNCTIONS is None:
|
|
484
|
-
raise NotImplementedError(
|
|
485
|
-
f"Type {cls} does not support custom implementations"
|
|
486
|
-
" of numpy functions"
|
|
487
|
-
) from None
|
|
488
|
-
cls._FUNCTIONS[numpy_function] = user_function
|
|
489
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|