oprattr 0.6.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.
- oprattr-0.6.0/.gitignore +13 -0
- oprattr-0.6.0/.python-version +1 -0
- oprattr-0.6.0/CHANGELOG.md +30 -0
- oprattr-0.6.0/DEVELOPERS.md +16 -0
- oprattr-0.6.0/LICENSE +32 -0
- oprattr-0.6.0/PKG-INFO +13 -0
- oprattr-0.6.0/README.md +1 -0
- oprattr-0.6.0/pyproject.toml +27 -0
- oprattr-0.6.0/src/oprattr/__init__.py +139 -0
- oprattr-0.6.0/src/oprattr/__init__.pyi +81 -0
- oprattr-0.6.0/src/oprattr/_operations.py +182 -0
- oprattr-0.6.0/src/oprattr/abstract.py +60 -0
- oprattr-0.6.0/src/oprattr/methods.py +107 -0
- oprattr-0.6.0/src/oprattr/mixins.py +489 -0
- oprattr-0.6.0/src/oprattr/py.typed +0 -0
- oprattr-0.6.0/src/oprattr/typeface.py +42 -0
- oprattr-0.6.0/src/oprattr/typeface.pyi +5 -0
- oprattr-0.6.0/tests/print-exceptions.py +32 -0
- oprattr-0.6.0/tests/test_object.py +527 -0
- oprattr-0.6.0/uv.lock +522 -0
oprattr-0.6.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## NEXT
|
|
2
|
+
|
|
3
|
+
## v0.6.0
|
|
4
|
+
|
|
5
|
+
- Add `methods` module
|
|
6
|
+
- Add `__init__.pyi` type file
|
|
7
|
+
- Update metadata error types
|
|
8
|
+
|
|
9
|
+
## v0.5.0
|
|
10
|
+
|
|
11
|
+
- Fix `MetadataError` bug
|
|
12
|
+
- Improve string representations of `Operand` instances
|
|
13
|
+
|
|
14
|
+
## v0.4.0
|
|
15
|
+
|
|
16
|
+
- Replace local `operators` module with equivalent module from `numerical` package
|
|
17
|
+
- Redefine equality to always return a single boolean value
|
|
18
|
+
|
|
19
|
+
## v0.3.0
|
|
20
|
+
|
|
21
|
+
- Incorporate `numerical` package
|
|
22
|
+
- Add `typeface` module
|
|
23
|
+
|
|
24
|
+
## v0.2.0
|
|
25
|
+
|
|
26
|
+
- Rename `_types` submodule to `abstract`
|
|
27
|
+
|
|
28
|
+
## v0.1.0
|
|
29
|
+
|
|
30
|
+
- Hello world!
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Developer Notes
|
|
2
|
+
|
|
3
|
+
This document contains notes for those who wish to contribute to `oprattr` by modifying the code base. In order to develop `oprattr`, you should fork the respository, edit your forked copy, and submit a pull request for integration with the original repository.
|
|
4
|
+
|
|
5
|
+
Note that this is a living document and is subject to change without notice.
|
|
6
|
+
|
|
7
|
+
## Version Numbers
|
|
8
|
+
|
|
9
|
+
When incrementing the version number to X.Y.Z, please do the following
|
|
10
|
+
* create a new subsection in `CHANGELOG.md`, below **NEXT**, with the title
|
|
11
|
+
formatted as vX.Y.Z (YYYY-MM-DD)
|
|
12
|
+
* update the version number in `pyproject.toml`
|
|
13
|
+
* commit with the message "Increment version to X.Y.Z"
|
|
14
|
+
* create a tag named "vX.Y.Z" with the message "version X.Y.Z"
|
|
15
|
+
* push and follow tags
|
|
16
|
+
|
oprattr-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025, Matt Young
|
|
6
|
+
All rights reserved.
|
|
7
|
+
|
|
8
|
+
Redistribution and use in source and binary forms, with or without
|
|
9
|
+
modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
12
|
+
list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
+
this list of conditions and the following disclaimer in the documentation
|
|
16
|
+
and/or other materials provided with the distribution.
|
|
17
|
+
|
|
18
|
+
* Neither the name of the copyright holder nor the names of its
|
|
19
|
+
contributors may be used to endorse or promote products derived from
|
|
20
|
+
this software without specific prior written permission.
|
|
21
|
+
|
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
26
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
27
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
28
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
29
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
30
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|
oprattr-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oprattr
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Matthew Young <myoung.space.science@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: numerical
|
|
9
|
+
Requires-Dist: numpy>=2.2.1
|
|
10
|
+
Requires-Dist: scipy>=1.15.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# oprattr: Self-Consistent Operations on Object Attributes
|
oprattr-0.6.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# oprattr: Self-Consistent Operations on Object Attributes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oprattr"
|
|
3
|
+
version = "0.6.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Matthew Young", email = "myoung.space.science@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"numerical",
|
|
12
|
+
"numpy>=2.2.1",
|
|
13
|
+
"scipy>=1.15.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"ipython>=8.31.0",
|
|
23
|
+
"pytest>=8.3.4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.uv.sources]
|
|
27
|
+
numerical = { git = "https://github.com/myoung-space-science/numerical", rev = "v0.2.0" }
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from numerical import operators
|
|
2
|
+
|
|
3
|
+
from .abstract import (
|
|
4
|
+
Quantity,
|
|
5
|
+
Object,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MetadataTypeError(TypeError):
|
|
10
|
+
"""A metadata-related TypeError occurred."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MetadataValueError(ValueError):
|
|
14
|
+
"""A metadata-related ValueError occurred."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_error_message(
|
|
18
|
+
f: operators.Operator,
|
|
19
|
+
*args,
|
|
20
|
+
error: str | None = None,
|
|
21
|
+
key: str | None = None,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Helper for metadata error messages.
|
|
24
|
+
|
|
25
|
+
This function should avoid raising an exception if at all possible, and
|
|
26
|
+
instead return the default error message, since it is already being called
|
|
27
|
+
as the result of an error elsewhere.
|
|
28
|
+
"""
|
|
29
|
+
errmsg = f"Cannot compute {f}"
|
|
30
|
+
errstr = error.lower() if isinstance(error, str) else ''
|
|
31
|
+
if errstr == 'unequal':
|
|
32
|
+
return f"{errmsg} between objects with unequal metadata"
|
|
33
|
+
types = [type(arg) for arg in args]
|
|
34
|
+
if errstr in {'non-empty', 'nonempty'}:
|
|
35
|
+
if len(types) == 2:
|
|
36
|
+
a, b = types
|
|
37
|
+
endstr = "because {} has metadata"
|
|
38
|
+
if issubclass(a, Object):
|
|
39
|
+
return f"{errmsg} between {a} and {b} {endstr.format(str(a))}"
|
|
40
|
+
if issubclass(b, Object):
|
|
41
|
+
return f"{errmsg} between {a} and {b} {endstr.format(str(b))}"
|
|
42
|
+
if errstr == 'type':
|
|
43
|
+
if key is None:
|
|
44
|
+
keystr = "a metadata attribute"
|
|
45
|
+
else:
|
|
46
|
+
keystr = f"metadata attribute {key!r}"
|
|
47
|
+
midstr = f"because {keystr}"
|
|
48
|
+
endstr = "does not support this operation"
|
|
49
|
+
if len(types) == 1:
|
|
50
|
+
return f"{errmsg} of {types[0]} {midstr} {endstr}"
|
|
51
|
+
if len(types) == 2:
|
|
52
|
+
a, b = types
|
|
53
|
+
return f"{errmsg} between {a} and {b} {midstr} {endstr}"
|
|
54
|
+
return errmsg
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def unary(f: operators.Operator, a):
|
|
58
|
+
"""Compute the unary operation f(a)."""
|
|
59
|
+
if isinstance(a, Quantity):
|
|
60
|
+
meta = {}
|
|
61
|
+
for key, value in a._meta.items():
|
|
62
|
+
try:
|
|
63
|
+
v = f(value)
|
|
64
|
+
except TypeError as exc:
|
|
65
|
+
errmsg = _build_error_message(f, a, error='type', key=key)
|
|
66
|
+
raise MetadataTypeError(errmsg) from exc
|
|
67
|
+
else:
|
|
68
|
+
meta[key] = v
|
|
69
|
+
return type(a)(f(a._data), **meta)
|
|
70
|
+
return f(a)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def equality(f: operators.Operator, a, b):
|
|
74
|
+
"""Compute the equality operation f(a, b)."""
|
|
75
|
+
x = a._data if isinstance(a, Quantity) else a
|
|
76
|
+
y = b._data if isinstance(b, Quantity) else b
|
|
77
|
+
fxy = f(x, y)
|
|
78
|
+
try:
|
|
79
|
+
iter(fxy)
|
|
80
|
+
except TypeError:
|
|
81
|
+
r = bool(fxy)
|
|
82
|
+
else:
|
|
83
|
+
r = all(fxy)
|
|
84
|
+
isne = f(1, 2)
|
|
85
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
86
|
+
if a._meta != b._meta:
|
|
87
|
+
return isne
|
|
88
|
+
return r
|
|
89
|
+
if isinstance(a, Quantity):
|
|
90
|
+
if not a._meta:
|
|
91
|
+
return r
|
|
92
|
+
return isne
|
|
93
|
+
if isinstance(b, Quantity):
|
|
94
|
+
if not b._meta:
|
|
95
|
+
return r
|
|
96
|
+
return isne
|
|
97
|
+
return r
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def ordering(f: operators.Operator, a, b):
|
|
101
|
+
"""Compute the ordering operation f(a, b)."""
|
|
102
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
103
|
+
if a._meta == b._meta:
|
|
104
|
+
return f(a._data, b._data)
|
|
105
|
+
errmsg = _build_error_message(f, a, b, error='unequal')
|
|
106
|
+
raise MetadataValueError(errmsg) from None
|
|
107
|
+
errmsg = _build_error_message(f, a, b, error='non-empty')
|
|
108
|
+
if isinstance(a, Quantity):
|
|
109
|
+
if not a._meta:
|
|
110
|
+
return f(a._data, b)
|
|
111
|
+
raise MetadataTypeError(errmsg) from None
|
|
112
|
+
if isinstance(b, Quantity):
|
|
113
|
+
if not b._meta:
|
|
114
|
+
return f(a, b._data)
|
|
115
|
+
raise MetadataTypeError(errmsg) from None
|
|
116
|
+
return f(a, b)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def additive(f: operators.Operator, a, b):
|
|
120
|
+
"""Compute the additive operation f(a, b)."""
|
|
121
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
122
|
+
if a._meta == b._meta:
|
|
123
|
+
return type(a)(f(a._data, b._data), **a._meta)
|
|
124
|
+
errmsg = _build_error_message(f, a, b, error='unequal')
|
|
125
|
+
raise MetadataValueError(errmsg) from None
|
|
126
|
+
errmsg = _build_error_message(f, a, b, error='non-empty')
|
|
127
|
+
if isinstance(a, Quantity):
|
|
128
|
+
if not a._meta:
|
|
129
|
+
return type(a)(f(a._data, b))
|
|
130
|
+
raise MetadataTypeError(errmsg) from None
|
|
131
|
+
if isinstance(b, Quantity):
|
|
132
|
+
if not b._meta:
|
|
133
|
+
return type(b)(f(a, b._data))
|
|
134
|
+
raise MetadataTypeError(errmsg) from None
|
|
135
|
+
return f(a, b)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def multiplicative(f: operators.Operator, a, b):
|
|
139
|
+
"""Compute the multiplicative operation f(a, b)."""
|
|
140
|
+
if isinstance(a, Quantity) and isinstance(b, Quantity):
|
|
141
|
+
keys = set(a._meta) & set(b._meta)
|
|
142
|
+
meta = {}
|
|
143
|
+
for key in keys:
|
|
144
|
+
try:
|
|
145
|
+
v = f(a._meta[key], b._meta[key])
|
|
146
|
+
except TypeError as exc:
|
|
147
|
+
errmsg = _build_error_message(f, a, b, error='type', key=key)
|
|
148
|
+
raise MetadataTypeError(errmsg) from exc
|
|
149
|
+
else:
|
|
150
|
+
meta[key] = v
|
|
151
|
+
for key, value in a._meta.items():
|
|
152
|
+
if key not in keys:
|
|
153
|
+
meta[key] = value
|
|
154
|
+
for key, value in b._meta.items():
|
|
155
|
+
if key not in keys:
|
|
156
|
+
meta[key] = value
|
|
157
|
+
return type(a)(f(a._data, b._data), **meta)
|
|
158
|
+
if isinstance(a, Quantity):
|
|
159
|
+
meta = {}
|
|
160
|
+
for key, value in a._meta.items():
|
|
161
|
+
try:
|
|
162
|
+
v = f(value, b)
|
|
163
|
+
except TypeError as exc:
|
|
164
|
+
errmsg = _build_error_message(f, a, b, error='type', key=key)
|
|
165
|
+
raise MetadataTypeError(errmsg) from exc
|
|
166
|
+
else:
|
|
167
|
+
meta[key] = v
|
|
168
|
+
return type(a)(f(a._data, b), **meta)
|
|
169
|
+
if isinstance(b, Quantity):
|
|
170
|
+
meta = {}
|
|
171
|
+
for key, value in b._meta.items():
|
|
172
|
+
try:
|
|
173
|
+
v = f(a, value)
|
|
174
|
+
except TypeError as exc:
|
|
175
|
+
errmsg = _build_error_message(f, a, b, error='type', key=key)
|
|
176
|
+
raise MetadataTypeError(errmsg) from exc
|
|
177
|
+
else:
|
|
178
|
+
meta[key] = v
|
|
179
|
+
return type(b)(f(a, b._data), **meta)
|
|
180
|
+
return f(a, b)
|
|
181
|
+
|
|
182
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import collections.abc
|
|
2
|
+
import numbers
|
|
3
|
+
|
|
4
|
+
import numerical
|
|
5
|
+
import numpy.typing
|
|
6
|
+
|
|
7
|
+
from . import typeface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DataType = typeface.TypeVar(
|
|
11
|
+
'DataType',
|
|
12
|
+
int,
|
|
13
|
+
float,
|
|
14
|
+
numbers.Number,
|
|
15
|
+
numpy.number,
|
|
16
|
+
numpy.typing.ArrayLike,
|
|
17
|
+
numpy.typing.NDArray,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@typeface.runtime_checkable
|
|
22
|
+
class Quantity(numerical.Quantity[DataType], typeface.Protocol):
|
|
23
|
+
"""Protocol for numerical objects with metadata."""
|
|
24
|
+
|
|
25
|
+
_meta: collections.abc.Mapping[str, typeface.Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Object(numerical.Real, typeface.Generic[DataType]):
|
|
29
|
+
"""A real-valued object with metadata attributes."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
__data: DataType,
|
|
34
|
+
**metadata,
|
|
35
|
+
) -> None:
|
|
36
|
+
if not isinstance(__data, numerical.Real):
|
|
37
|
+
raise TypeError("Data input to Object must be real-valued")
|
|
38
|
+
self._data = __data
|
|
39
|
+
self._meta = metadata
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
"""Called for str(self)."""
|
|
43
|
+
try:
|
|
44
|
+
datastr = numpy.array2string(
|
|
45
|
+
self._data,
|
|
46
|
+
separator=", ",
|
|
47
|
+
threshold=6,
|
|
48
|
+
edgeitems=2,
|
|
49
|
+
prefix=f"{self.__class__.__qualname__}(",
|
|
50
|
+
suffix=")"
|
|
51
|
+
)
|
|
52
|
+
except Exception:
|
|
53
|
+
datastr = str(self._data)
|
|
54
|
+
metastr = ", ".join(f"{k}={str(v)!r}" for k, v in self._meta.items())
|
|
55
|
+
return f"{datastr}, {metastr}"
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
"""Called for repr(self)."""
|
|
59
|
+
return f"{self.__class__.__qualname__}({self})"
|
|
60
|
+
|