Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 18 additions & 49 deletions quantities/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,17 @@ def __array_prepare__(self, obj, context=None):
return obj

try:
res._dimensionality = p_dict[uf](*objs)
dimensionality = p_dict[uf](*objs)
except KeyError:
raise ValueError(
"""ufunc %r not supported by quantities
please file a bug report at https://github.com/python-quantities
""" % uf
)
if (isinstance(res.base, Quantity)
and res._dimensionality != dimensionality):
raise ValueError('can not modify units of a view of a Quantity')
res._dimensionality = dimensionality
return res

def __array_wrap__(self, obj, context=None, return_scalar=False):
Expand All @@ -328,12 +332,9 @@ def __array_wrap__(self, obj, context=None, return_scalar=False):
return self.__array_prepare__(obj, context)
else:
return obj
# For NumPy > 2.0 we either do the prepare or the wrap
# NumPy 2 no longer calls __array_prepare__, even for Quantity outputs.
else:
if not isinstance(obj, Quantity):
return self.__array_prepare__(obj, context)
else:
return super().__array_wrap__(obj, context, return_scalar)
return self.__array_prepare__(obj, context)


@with_doc(np.ndarray.__add__)
Expand Down Expand Up @@ -379,22 +380,7 @@ def __imod__(self, other):
@with_doc(np.ndarray.__imul__)
@protected_multiplication
def __imul__(self, other):
# the following is an inelegant fix for the removal of __array_prepare__ in NumPy 2.x
# the longer-term solution is probably to implement __array_ufunc__
# See:
# - https://numpy.org/devdocs/release/2.0.0-notes.html#array-prepare-is-removed
# - https://numpy.org/neps/nep-0013-ufunc-overrides.html
if _np_version < (2, 0, 0):
return super().__imul__(other)
else:
cself = self.copy()
if hasattr(other, "copy"):
cother = other.copy()
else:
cother = other
res = super().__imul__(other)
context = (np.multiply, (cself, cother, cself), 0)
return self.__array_prepare__(res, context=context)
return super().__imul__(other)

@with_doc(np.ndarray.__rmul__)
def __rmul__(self, other):
Expand All @@ -403,18 +389,7 @@ def __rmul__(self, other):
@with_doc(np.ndarray.__itruediv__)
@protected_multiplication
def __itruediv__(self, other):
# see comment above on __imul__
if _np_version < (2, 0, 0):
return super().__itruediv__(other)
else:
cself = self.copy()
if hasattr(other, "copy"):
cother = other.copy()
else:
cother = other
res = super().__itruediv__(other)
context = (np.true_divide, (cself, cother, cself), 0)
return self.__array_prepare__(res, context=context)
return super().__itruediv__(other)

@with_doc(np.ndarray.__rtruediv__)
def __rtruediv__(self, other):
Expand All @@ -429,18 +404,9 @@ def __pow__(self, other):
@check_uniform
@protected_power
def __ipow__(self, other):
# see comment above on __imul__
if _np_version < (2, 0, 0):
return super().__ipow__(other)
else:
cself = self.copy()
if hasattr(other, "copy"):
cother = other.copy()
else:
cother = other
res = super().__ipow__(other)
context = (np.power, (cself, cother, cself), 0)
return self.__array_prepare__(res, context=context)
# The exponent can share our magnitude buffer. Keep its original value
# for the dimensionality calculation performed after the ufunc.
return super().__ipow__(other.copy())

def __round__(self, decimals=0):
return np.around(self, decimals)
Expand Down Expand Up @@ -552,8 +518,11 @@ def _tolist(self, work_list):
# item, itemset, tofile, dump, byteswap

@with_doc(np.ndarray.sum)
def sum(self, axis=None, dtype=None, out=None):
ret = self.magnitude.sum(axis, dtype, None if out is None else out.magnitude)
def sum(self, axis=None, dtype=None, out=None, keepdims=False):
ret = self.magnitude.sum(
axis, dtype, None if out is None else out.magnitude,
keepdims=keepdims
)
dim = self.dimensionality
if out is None:
return Quantity(ret, dim)
Expand Down Expand Up @@ -692,7 +661,7 @@ def clip(self, min=None, max=None, out=None):
clipped = self.magnitude.clip(
min.rescale(self._dimensionality).magnitude,
max.rescale(self._dimensionality).magnitude,
out
None if out is None else out.magnitude
)
dim = self.dimensionality
if out is None:
Expand Down
1 change: 1 addition & 0 deletions quantities/tests/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def test_sum(self):
self.assertQuantityEqual(self.q.sum(), 10*pq.m)
self.assertQuantityEqual(self.q.sum(0), [4, 6]*pq.m)
self.assertQuantityEqual(self.q.sum(1), [3, 7]*pq.m)
self.assertQuantityEqual(self.q.sum(1, keepdims=True), [[3], [7]]*pq.m)

def test_nansum(self):
import numpy as np
Expand Down
68 changes: 68 additions & 0 deletions quantities/tests/test_umath.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,74 @@ def test_nansum(self):
c = [1,2,3, np.nan] * pq.m
self.assertQuantityEqual(np.nansum(c), 6 * pq.m)

def test_nan_variance_and_standard_deviation(self):
values = np.array([[1., np.nan, 5.], [2., 4., 8.]])
for data in (values, values + 1j * values):
q = data * pq.ms
for func, units in ((np.nanvar, pq.ms**2), (np.nanstd, pq.ms)):
for axis in (None, 0, 1):
for keepdims in (False, True):
with self.subTest(func=func, axis=axis, keepdims=keepdims,
dtype=data.dtype):
expected = func(data, axis=axis, keepdims=keepdims)
result = func(q, axis=axis, keepdims=keepdims)
self.assertQuantityEqual(result, expected * units)
out = np.zeros_like(expected) * pq.kg
self.assertIs(func(q, axis=axis, keepdims=keepdims,
out=out), out)
self.assertQuantityEqual(out, expected * units)
self.assertQuantityEqual(np.nanstd(q, ddof=1),
np.nanstd(data, ddof=1) * pq.ms)
np.testing.assert_array_equal(q.magnitude, data)
self.assertEqual(q.units, pq.ms)

def test_ufunc_output_units(self):
q = [2., 4.] * pq.m
out = [0., 0.] * pq.s
self.assertIs(np.multiply(q, q, out=out), out)
self.assertQuantityEqual(out, [4., 16.] * pq.m**2)
self.assertIs(np.sqrt(out, out=out), out)
self.assertQuantityEqual(out, q)
self.assertIs(np.divide(out, q, out=out), out)
self.assertQuantityEqual(out, [1., 1.] * pq.dimensionless)
q **= 2
self.assertQuantityEqual(q, [4., 16.] * pq.m**2)
scalar = 2. * pq.m
scalar **= scalar.magnitude
self.assertQuantityEqual(scalar, 4. * pq.m**2)

def test_ufunc_output_view_units(self):
for operation in (
lambda v: np.multiply(v, v, out=v),
lambda v: np.divide(v, v, out=v),
lambda v: np.square(v, out=v),
lambda v: np.sqrt(v, out=v)):
with self.subTest(operation=operation):
q = [1., 4., 9.] * pq.m
view = q[:2]
with self.assertRaisesRegex(ValueError, 'modify units of a view'):
operation(view)
self.assertEqual(view.units, pq.m)
self.assertEqual(q.units, pq.m)

q = [1., 4., 9.] * pq.m
view = q[:2]
self.assertIs(np.multiply(view, 2, out=view), view)
self.assertQuantityEqual(q, [2., 8., 9.] * pq.m)
self.assertIs(np.add(view, view, out=view), view)
self.assertQuantityEqual(q, [4., 16., 9.] * pq.m)
self.assertQuantityEqual(np.square(view), [16., 256.] * pq.m**2)

source = [2., 3.] * pq.m
destination = [0., 0., 0.] * pq.m**2
output = destination[:2]
self.assertIs(np.multiply(source, source, out=output), output)
self.assertQuantityEqual(destination, [4., 9., 0.] * pq.m**2)
with self.assertRaisesRegex(ValueError, 'modify units of a view'):
np.sqrt(source, out=output)
self.assertEqual(destination.units, pq.m**2)
self.assertEqual(output.units, pq.m**2)

def test_cumprod(self):
self.assertRaises(ValueError, np.cumprod, self.q)

Expand Down
Loading