Skip to content

Commit

Permalink
Merge #963
Browse files Browse the repository at this point in the history
963: Add tests and documentation with improvement of downcast type compatibility (part of #845) r=hgrecco a=jthielen

As a part of #845, this PR adds tests for downcast type compatibility with Sparse's `COO` and NumPy's `MaskedArray`, along with more careful handling of downcast types throughout the library. Also included is new documentation on array type compatibility, including the type casting hierarchy digraph by @shoyer and @crusaderky.

While this PR doesn't fully bring Pint's downcast type compatibility to a completed state, I think this gets it "good enough" for the upcoming release, and the remaining issues are fairly well defined:

- MaskedArray non-commutativity (#633 / numpy/numpy#15200)
- Dask compatibility (#883)
- Addition of CuPy tests (no issue on issue tracker yet)

Because of that, I think this can close #845, but if @hgrecco you want that kept open until the above items are resolved, let me know.

- [x] Closes #37; Closes #845
- [x] Executed ``black -t py36 . && isort -rc . && flake8`` with no errors
- [x] The change is fully covered by automated unit tests
- [x] Documented in docs/ as appropriate
- [x] Added an entry to the CHANGES file


Co-authored-by: Jon Thielen <[email protected]>
bors[bot] and jthielen authored Dec 30, 2019
2 parents f36cc74 + bd1897e commit 0d09f54
Showing 9 changed files with 1,027 additions and 244 deletions.
9 changes: 8 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -4,6 +4,13 @@ Pint Changelog
0.10 (unreleased)
-----------------

- Documentation on Pint's array type compatibility has been added to the NumPy support
page, including a graph of the duck array type casting hierarchy as understood by Pint
for N-dimensional arrays.
(Issue #963, Thanks Jon Thielen, Stephan Hoyer, and Guido Imperiale)
- Improved compatibility for downcast duck array types like Sparse and Masked Arrays. A
collection of basic tests has been added.
(Issue #963, Thanks Jon Thielen)
- Improvements to wraps and check:
- fail upon decoration (not execution) by checking wrapped function signature against
wraps/check arguments.
@@ -12,7 +19,7 @@ Pint Changelog
(might BREAK code not conforming to documentation)
- when strict=True, strings that can be parsed to quantities are accepted as arguments.
- Add revolutions per second (rps)
- Improved compatbility for upcast types like xarray's DataArray or Dataset, to which
- Improved compatibility for upcast types like xarray's DataArray or Dataset, to which
Pint Quantities now fully defer for arithmetic and NumPy operations. A collection of
basic tests for proper deferral has been added (for full integration tests, see
xarray's test suite). The list of upcast types is available at
763 changes: 763 additions & 0 deletions docs/numpy.ipynb

Large diffs are not rendered by default.

181 changes: 0 additions & 181 deletions docs/numpy.rst

This file was deleted.

40 changes: 33 additions & 7 deletions pint/compat.py
Original file line number Diff line number Diff line change
@@ -66,14 +66,16 @@ class BehaviorChangeWarning(UserWarning):
NUMPY_VER = np.__version__
NUMERIC_TYPES = (Number, Decimal, ndarray, np.number)

def _to_magnitude(value, force_ndarray=False):
def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False):
if isinstance(value, (dict, bool)) or value is None:
raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value))
elif isinstance(value, str) and value == "":
raise ValueError("Quantity magnitude cannot be an empty string.")
elif isinstance(value, (list, tuple)):
return np.asarray(value)
if force_ndarray:
if force_ndarray or (
force_ndarray_like and not is_duck_array_type(type(value))
):
return np.asarray(value)
return value

@@ -112,9 +114,11 @@ class ndarray:
NP_NO_VALUE = None
ARRAY_FALLBACK = False

def _to_magnitude(value, force_ndarray=False):
if force_ndarray:
raise ValueError("Cannot force to ndarray when NumPy is not present.")
def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False):
if force_ndarray or force_ndarray_like:
raise ValueError(
"Cannot force to ndarray or ndarray-like when NumPy is not present."
)
elif isinstance(value, (dict, bool)) or value is None:
raise TypeError("Invalid magnitude for Quantity: {0!r}".format(value))
elif isinstance(value, str) and value == "":
@@ -148,8 +152,8 @@ def _to_magnitude(value, force_ndarray=False):
if not HAS_BABEL:
babel_parse = babel_units = missing_dependency("Babel") # noqa: F811

# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast and
# downcast/wrappable types using guarded imports
# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast
# types using guarded imports
upcast_types = []

# pint-pandas (PintArray)
@@ -191,6 +195,28 @@ def is_upcast_type(other):
return other in upcast_types


def is_duck_array_type(other):
"""Check if the type object represents a (non-Quantity) duck array type.
Parameters
----------
other : object
Returns
-------
bool
"""
# TODO (NEP 30): replace duck array check with hasattr(other, "__duckarray__")
return other is ndarray or (
not hasattr(other, "_magnitude")
and not hasattr(other, "_units")
and HAS_NUMPY_ARRAY_FUNCTION
and hasattr(other, "__array_function__")
and hasattr(other, "ndim")
and hasattr(other, "dtype")
)


def eq(lhs, rhs, check_all):
"""Comparison of scalars and arrays.
3 changes: 2 additions & 1 deletion pint/numpy_func.py
Original file line number Diff line number Diff line change
@@ -424,7 +424,7 @@ def implementation(*args, **kwargs):
implement_func("ufunc", ufunc_str, input_units=None, output_unit=None)

for ufunc_str in matching_input_bare_output_ufuncs:
# Require all inputs to match units, but output base ndarray
# Require all inputs to match units, but output base ndarray/duck array
implement_func("ufunc", ufunc_str, input_units="all_consistent", output_unit=None)

for ufunc_str, out_unit in matching_input_set_units_output_ufuncs.items():
@@ -744,6 +744,7 @@ def implementation(*args, **kwargs):
("rot90", "m", True),
("insert", ["arr", "values"], True),
("resize", "a", True),
("reshape", "a", True),
]:
implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output)

123 changes: 80 additions & 43 deletions pint/quantity.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@
array_function_change_msg,
babel_parse,
eq,
is_duck_array_type,
is_upcast_type,
ndarray,
np,
@@ -145,6 +146,10 @@ class Quantity(PrettyIPython, SharedRegistryObject):
def force_ndarray(self):
return self._REGISTRY.force_ndarray

@property
def force_ndarray_like(self):
return self._REGISTRY.force_ndarray_like

def __reduce__(self):
"""Allow pickling quantities. Since UnitRegistries are not pickled, upon
unpickling the new object is always attached to the application registry.
@@ -173,15 +178,21 @@ def __new__(cls, value, units=None):
inst = copy.copy(value)
else:
inst = SharedRegistryObject.__new__(cls)
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
inst._magnitude = _to_magnitude(
value, inst.force_ndarray, inst.force_ndarray_like
)
inst._units = UnitsContainer()
elif isinstance(units, (UnitsContainer, UnitDefinition)):
inst = SharedRegistryObject.__new__(cls)
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
inst._magnitude = _to_magnitude(
value, inst.force_ndarray, inst.force_ndarray_like
)
inst._units = units
elif isinstance(units, str):
inst = SharedRegistryObject.__new__(cls)
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
inst._magnitude = _to_magnitude(
value, inst.force_ndarray, inst.force_ndarray_like
)
inst._units = inst._REGISTRY.parse_units(units)._units
elif isinstance(units, SharedRegistryObject):
if isinstance(units, Quantity) and units.magnitude != 1:
@@ -192,7 +203,9 @@ def __new__(cls, value, units=None):
else:
inst = SharedRegistryObject.__new__(cls)
inst._units = units._units
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
inst._magnitude = _to_magnitude(
value, inst.force_ndarray, inst.force_ndarray_like
)
else:
raise TypeError(
"units must be of type str, Quantity or "
@@ -502,7 +515,7 @@ def _convert_magnitude(self, other, *contexts, **ctx_kwargs):
self._magnitude,
self._units,
other,
inplace=isinstance(self._magnitude, ndarray),
inplace=is_duck_array_type(type(self._magnitude)),
)

def ito(self, other=None, *contexts, **ctx_kwargs):
@@ -729,7 +742,9 @@ def _iadd_sub(self, other, op):
if not self._check(other):
# other not from same Registry or not a Quantity
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
other_magnitude = _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
except PintTypeError:
raise
except TypeError:
@@ -848,12 +863,14 @@ def _add_sub(self, other, op):
# the operation.
units = self._units
magnitude = op(
self._magnitude, _to_magnitude(other, self.force_ndarray)
self._magnitude,
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like),
)
elif self.dimensionless:
units = UnitsContainer()
magnitude = op(
self.to(units)._magnitude, _to_magnitude(other, self.force_ndarray)
self.to(units)._magnitude,
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like),
)
else:
raise DimensionalityError(self._units, "dimensionless")
@@ -939,10 +956,10 @@ def _add_sub(self, other, op):
def __iadd__(self, other):
if isinstance(other, datetime.datetime):
return self.to_timedelta() + other
elif not isinstance(self._magnitude, ndarray):
return self._add_sub(other, operator.add)
else:
elif is_duck_array_type(type(self._magnitude)):
return self._iadd_sub(other, operator.iadd)
else:
return self._add_sub(other, operator.add)

def __add__(self, other):
if isinstance(other, datetime.datetime):
@@ -953,10 +970,10 @@ def __add__(self, other):
__radd__ = __add__

def __isub__(self, other):
if not isinstance(self._magnitude, ndarray):
return self._add_sub(other, operator.sub)
else:
if is_duck_array_type(type(self._magnitude)):
return self._iadd_sub(other, operator.isub)
else:
return self._add_sub(other, operator.sub)

def __sub__(self, other):
return self._add_sub(other, operator.sub)
@@ -1007,7 +1024,9 @@ def _imul_div(self, other, magnitude_op, units_op=None):
self._units, getattr(other, "units", "")
)
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
other_magnitude = _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
except PintTypeError:
raise
except TypeError:
@@ -1075,7 +1094,9 @@ def _mul_div(self, other, magnitude_op, units_op=None):
self._units, getattr(other, "units", "")
)
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
other_magnitude = _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
except PintTypeError:
raise
except TypeError:
@@ -1109,10 +1130,10 @@ def _mul_div(self, other, magnitude_op, units_op=None):
return self.__class__(magnitude, units)

def __imul__(self, other):
if not isinstance(self._magnitude, ndarray):
return self._mul_div(other, operator.mul)
else:
if is_duck_array_type(type(self._magnitude)):
return self._imul_div(other, operator.imul)
else:
return self._mul_div(other, operator.mul)

def __mul__(self, other):
return self._mul_div(other, operator.mul)
@@ -1129,17 +1150,19 @@ def __matmul__(self, other):
__rmatmul__ = __matmul__

def __itruediv__(self, other):
if not isinstance(self._magnitude, ndarray):
return self._mul_div(other, operator.truediv)
else:
if is_duck_array_type(type(self._magnitude)):
return self._imul_div(other, operator.itruediv)
else:
return self._mul_div(other, operator.truediv)

def __truediv__(self, other):
return self._mul_div(other, operator.truediv)

def __rtruediv__(self, other):
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
other_magnitude = _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
except PintTypeError:
raise
except TypeError:
@@ -1233,11 +1256,11 @@ def __rdivmod__(self, other):

@check_implemented
def __ipow__(self, other):
if not isinstance(self._magnitude, ndarray):
if not is_duck_array_type(type(self._magnitude)):
return self.__pow__(other)

try:
_to_magnitude(other, self.force_ndarray)
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like)
except PintTypeError:
raise
except TypeError:
@@ -1246,7 +1269,7 @@ def __ipow__(self, other):
if not self._ok_for_muldiv:
raise OffsetUnitCalculusError(self._units)

if isinstance(getattr(other, "_magnitude", other), ndarray):
if is_duck_array_type(type(getattr(other, "_magnitude", other))):
# arrays are refused as exponent, because they would create
# len(array) quantities of len(set(array)) different units
# unless the base is dimensionless.
@@ -1286,13 +1309,15 @@ def __ipow__(self, other):
else:
self._units **= other

self._magnitude **= _to_magnitude(other, self.force_ndarray)
self._magnitude **= _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
return self

@check_implemented
def __pow__(self, other):
try:
_to_magnitude(other, self.force_ndarray)
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like)
except PintTypeError:
raise
except TypeError:
@@ -1301,7 +1326,7 @@ def __pow__(self, other):
if not self._ok_for_muldiv:
raise OffsetUnitCalculusError(self._units)

if isinstance(getattr(other, "_magnitude", other), ndarray):
if is_duck_array_type(type(getattr(other, "_magnitude", other))):
# arrays are refused as exponent, because they would create
# len(array) quantities of len(set(array)) different units
# unless the base is dimensionless.
@@ -1339,7 +1364,9 @@ def __pow__(self, other):
elif not getattr(other, "dimensionless", True):
raise DimensionalityError(other._units, "dimensionless")
else:
exponent = _to_magnitude(other, self.force_ndarray)
exponent = _to_magnitude(
other, self.force_ndarray, self.force_ndarray_like
)
units = new_self._units ** exponent

magnitude = new_self._magnitude ** exponent
@@ -1348,15 +1375,15 @@ def __pow__(self, other):
@check_implemented
def __rpow__(self, other):
try:
_to_magnitude(other, self.force_ndarray)
_to_magnitude(other, self.force_ndarray, self.force_ndarray_like)
except PintTypeError:
raise
except TypeError:
return NotImplemented
else:
if not self.dimensionless:
raise DimensionalityError(self._units, "dimensionless")
if isinstance(self._magnitude, ndarray):
if is_duck_array_type(type(self._magnitude)):
if np.size(self._magnitude) > 1:
raise DimensionalityError(self._units, "dimensionless")
new_self = self.to_root_units()
@@ -1415,7 +1442,7 @@ def __eq__(self, other):
@check_implemented
def __ne__(self, other):
out = self.__eq__(other)
if isinstance(out, ndarray):
if is_duck_array_type(type(out)):
return np.logical_not(out)
return not out

@@ -1637,12 +1664,12 @@ def __getattr__(self, item):
stacklevel=2,
)

if isinstance(self._magnitude, ndarray):
if is_duck_array_type(type(self._magnitude)):
# Defer to magnitude, and don't catch any AttributeErrors
return getattr(self._magnitude, item)
else:
# If an `__array_` attributes is requested but the magnitude is not an ndarray,
# we convert the magnitude to a numpy ndarray.
# TODO (#905 follow-up): Potentially problematic, investigate for duck arrays
# If an `__array_` attribute is requested but the magnitude is not
# a duck array, we convert the magnitude to a numpy ndarray.
magnitude_as_array = _to_magnitude(
self._magnitude, force_ndarray=True
)
@@ -1651,13 +1678,23 @@ def __getattr__(self, item):
# TODO (next minor version): ARRAY_FALLBACK is removed and this becomes the standard behavior
raise AttributeError(f"Array protocol attribute {item} not available.")
elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods:
# TODO (#905 follow-up): Potentially problematic, investigate for duck arrays/scalars
magnitude_as_array = _to_magnitude(self._magnitude, True)
attr = getattr(magnitude_as_array, item)
if callable(attr):
magnitude_as_duck_array = _to_magnitude(
self._magnitude, force_ndarray_like=True
)
try:
attr = getattr(magnitude_as_duck_array, item)
return functools.partial(self._numpy_method_wrap, attr)
else:
raise AttributeError("NumPy method {} was not callable.".format(item))
except AttributeError:
raise AttributeError(
f"NumPy method {item} not available on {type(magnitude_as_duck_array)}"
)
except TypeError as exc:
if "not callable" in str(exc):
raise AttributeError(
f"NumPy method {item} not callable on {type(magnitude_as_duck_array)}"
)
else:
raise exc

try:
return getattr(self._magnitude, item)
10 changes: 9 additions & 1 deletion pint/registry.py
Original file line number Diff line number Diff line change
@@ -140,6 +140,8 @@ class BaseRegistry(metaclass=RegistryMeta):
the default definition file. None to leave the UnitRegistry empty.
force_ndarray : bool
convert any input, scalar or not to a numpy.ndarray.
force_ndarray_like : bool
convert all inputs other than duck arrays to a numpy.ndarray.
on_redefinition : str
action to take in case a unit is redefined: 'warn', 'raise', 'ignore'
auto_reduce_dimensions :
@@ -179,6 +181,7 @@ def __init__(
self,
filename="",
force_ndarray=False,
force_ndarray_like=False,
on_redefinition="warn",
auto_reduce_dimensions=False,
preprocessors=None,
@@ -189,6 +192,7 @@ def __init__(

self._filename = filename
self.force_ndarray = force_ndarray
self.force_ndarray_like = force_ndarray_like
self.preprocessors = preprocessors or []

#: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore'
@@ -1950,8 +1954,10 @@ class UnitRegistry(SystemRegistry, ContextRegistry, NonMultiplicativeRegistry):
path of the units definition file to load or line-iterable object.
Empty to load the default definition file.
None to leave the UnitRegistry empty.
force_ndarray :
force_ndarray : bool
convert any input, scalar or not to a numpy.ndarray.
force_ndarray_like : bool
convert all inputs other than duck arrays to a numpy.ndarray.
default_as_delta :
In the context of a multiplication of units, interpret
non-multiplicative units as their *delta* counterparts.
@@ -1975,6 +1981,7 @@ def __init__(
self,
filename="",
force_ndarray=False,
force_ndarray_like=False,
default_as_delta=True,
autoconvert_offset_to_baseunit=False,
on_redefinition="warn",
@@ -1987,6 +1994,7 @@ def __init__(
super().__init__(
filename=filename,
force_ndarray=force_ndarray,
force_ndarray_like=force_ndarray_like,
on_redefinition=on_redefinition,
default_as_delta=default_as_delta,
autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit,
108 changes: 108 additions & 0 deletions pint/testsuite/test_compat_downcast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import pytest

from pint import UnitRegistry

# Conditionally import NumPy and any upcast type libraries
np = pytest.importorskip("numpy", reason="NumPy is not available")
sparse = pytest.importorskip("sparse", reason="sparse is not available")

# Set up unit registry and sample
ureg = UnitRegistry(force_ndarray_like=True)
q_base = (np.arange(25).reshape(5, 5).T + 1) * ureg.kg


# Define identity function for use in tests
def identity(x):
return x


@pytest.fixture(params=["sparse", "masked_array"])
def array(request):
"""Generate 5x5 arrays of given type for tests."""
if request.param == "sparse":
# Create sample sparse COO as a permutation matrix.
coords = [[0, 1, 2, 3, 4], [1, 3, 0, 2, 4]]
data = [1.0] * 5
return sparse.COO(coords, data, shape=(5, 5))
elif request.param == "masked_array":
# Create sample masked array as an upper triangular matrix.
return np.ma.masked_array(
np.arange(25, dtype=np.float).reshape((5, 5)),
mask=np.logical_not(np.triu(np.ones((5, 5)))),
)


@pytest.mark.parametrize(
"op, magnitude_op, unit_op",
[
pytest.param(identity, identity, identity, id="identity"),
pytest.param(
lambda x: x + 1 * ureg.m, lambda x: x + 1, identity, id="addition"
),
pytest.param(
lambda x: x - 20 * ureg.cm, lambda x: x - 0.2, identity, id="subtraction"
),
pytest.param(
lambda x: x * (2 * ureg.s),
lambda x: 2 * x,
lambda u: u * ureg.s,
id="multiplication",
),
pytest.param(
lambda x: x / (1 * ureg.s), identity, lambda u: u / ureg.s, id="division"
),
pytest.param(lambda x: x ** 2, lambda x: x ** 2, lambda u: u ** 2, id="square"),
pytest.param(lambda x: x.T, lambda x: x.T, identity, id="transpose"),
pytest.param(np.mean, np.mean, identity, id="mean ufunc"),
pytest.param(np.sum, np.sum, identity, id="sum ufunc"),
pytest.param(np.sqrt, np.sqrt, lambda u: u ** 0.5, id="sqrt ufunc"),
pytest.param(
lambda x: np.reshape(x, 25),
lambda x: np.reshape(x, 25),
identity,
id="reshape function",
),
pytest.param(np.amax, np.amax, identity, id="amax function"),
],
)
def test_univariate_op_consistency(op, magnitude_op, unit_op, array):
q = ureg.Quantity(array, "meter")
res = op(q)
assert np.all(res.magnitude == magnitude_op(array)) # Magnitude check
assert res.units == unit_op(q.units) # Unit check
assert q.magnitude is array # Immutability check


@pytest.mark.parametrize(
"op, unit",
[
pytest.param(lambda x, y: x * y, ureg("kg m"), id="multiplication"),
pytest.param(lambda x, y: x / y, ureg("m / kg"), id="division"),
pytest.param(np.multiply, ureg("kg m"), id="multiply ufunc"),
],
)
def test_bivariate_op_consistency(op, unit, array):
q = ureg.Quantity(array, "meter")
res = op(q, q_base)
assert np.all(res.magnitude == op(array, q_base.magnitude)) # Magnitude check
assert res.units == unit # Unit check
assert q.magnitude is array # Immutability check


@pytest.mark.parametrize(
"op",
[
pytest.param(
lambda a, u: a * u,
id="array-first",
marks=pytest.mark.xfail(reason="upstream issue numpy/numpy#15200"),
),
pytest.param(lambda a, u: u * a, id="unit-first"),
],
)
@pytest.mark.parametrize(
"unit",
[pytest.param(ureg.m, id="Unit"), pytest.param(ureg("meter"), id="Quantity")],
)
def test_array_quantity_creation_by_multiplication(op, unit, array):
assert type(op(array, unit)) == ureg.Quantity
34 changes: 24 additions & 10 deletions pint/unit.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
import operator
from numbers import Number

from .compat import NUMERIC_TYPES
from .compat import NUMERIC_TYPES, is_upcast_type
from .definitions import UnitDefinition
from .formatting import siunitx_format_unit
from .util import PrettyIPython, SharedRegistryObject, UnitsContainer
@@ -230,18 +230,32 @@ def __complex__(self):

__array_priority__ = 17

def __array_prepare__(self, array, context=None):
return 1
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
if method != "__call__":
# Only handle ufuncs as callables
return NotImplemented

def __array_wrap__(self, array, context=None):
uf, objs, huh = context
# Check types and return NotImplemented when upcast type encountered
types = set(
type(arg)
for arg in list(inputs) + list(kwargs.values())
if hasattr(arg, "__array_ufunc__")
)
if any(is_upcast_type(other) for other in types):
return NotImplemented

if uf.__name__ in ("true_divide", "divide", "floor_divide"):
return self._REGISTRY.Quantity(array, 1 / self._units)
elif uf.__name__ in ("multiply",):
return self._REGISTRY.Quantity(array, self._units)
# Act on limited implementations by conversion to multiplicative identity
# Quantity
if ufunc.__name__ in ("true_divide", "divide", "floor_divide", "multiply"):
return ufunc(
*tuple(
self._REGISTRY.Quantity(1, self._units) if arg is self else arg
for arg in inputs
),
**kwargs,
)
else:
raise ValueError("Unsupproted operation for Unit")
return NotImplemented

@property
def systems(self):

0 comments on commit 0d09f54

Please sign in to comment.