diff options
author | Hernan Grecco <hernan.grecco@gmail.com> | 2014-03-29 18:22:35 -0300 |
---|---|---|
committer | Hernan Grecco <hernan.grecco@gmail.com> | 2014-03-29 19:38:26 -0300 |
commit | 384ef9e90f3915d42a7ac8b413056d15d078b9de (patch) | |
tree | 56d3164634d22ee679c3ba9b61e6a511975903bb | |
parent | e215db7b28bc67e5cd126f8734cdcb8bddc2a4e0 (diff) | |
download | pint-384ef9e90f3915d42a7ac8b413056d15d078b9de.tar.gz |
Implemented better handling of Quantities with mutable magnitudes
Only if the magnitude is a `numpy.ndarray`, the in place math
operations take place. In any other case, another object Quantity
object is created and returned.
See #104
-rw-r--r-- | pint/quantity.py | 77 | ||||
-rw-r--r-- | pint/testsuite/__init__.py | 33 | ||||
-rw-r--r-- | pint/testsuite/test_issues.py | 20 | ||||
-rw-r--r-- | pint/testsuite/test_quantity.py | 309 |
4 files changed, 292 insertions, 147 deletions
diff --git a/pint/quantity.py b/pint/quantity.py index 5a9e3f3..97d3b33 100644 --- a/pint/quantity.py +++ b/pint/quantity.py @@ -336,7 +336,10 @@ class _Quantity(object): return self.__class__(magnitude, units) def __iadd__(self, other): - return self._iadd_sub(other, operator.iadd) + if not isinstance(self._magnitude, ndarray): + return self._add_sub(other, operator.add) + else: + return self._iadd_sub(other, operator.iadd) def __add__(self, other): return self._add_sub(other, operator.add) @@ -344,7 +347,10 @@ class _Quantity(object): __radd__ = __add__ def __isub__(self, other): - return self._iadd_sub(other, operator.isub) + if not isinstance(self._magnitude, ndarray): + return self._add_sub(other, operator.sub) + else: + return self._iadd_sub(other, operator.isub) def __sub__(self, other): return self._add_sub(other, operator.sub) @@ -387,11 +393,45 @@ class _Quantity(object): return self def _mul_div(self, other, magnitude_op, units_op=None): - ret = copy.copy(self) - return ret._imul_div(other, magnitude_op, units_op) + """Perform multiplication or division operation and return the result. + + :param other: object to be multiplied/divided with self + :type other: Quantity or any type accepted by :func:`_to_magnitude` + :param magnitude_op: operator function to perform on the magnitudes (e.g. operator.mul) + :type magnitude_op: function + :param units_op: operator function to perform on the units; if None, *magnitude_op* is used + :type units_op: function or None + """ + if units_op is None: + units_op = magnitude_op + + new_self = self + if self.__used: + if not _only_multiplicative_units(self): + new_self = self.to_base_units() + + if _check(self, other): + if not _only_multiplicative_units(other): + other = other.to_base_units() + magnitude = magnitude_op(new_self._magnitude, other._magnitude) + units = units_op(new_self._units, other._units) + else: + try: + other_magnitude = _to_magnitude(other, self.force_ndarray) + except TypeError: + return NotImplemented + magnitude = magnitude_op(new_self._magnitude, other_magnitude) + units = units_op(new_self._units, UnitsContainer()) + + ret = self.__class__(magnitude, units) + ret.__used = True + return ret def __imul__(self, other): - return self._imul_div(other, operator.imul) + if not isinstance(self._magnitude, ndarray): + return self._mul_div(other, operator.mul) + else: + return self._imul_div(other, operator.imul) def __mul__(self, other): return self._mul_div(other, operator.mul) @@ -399,13 +439,19 @@ class _Quantity(object): __rmul__ = __mul__ def __itruediv__(self, other): - return self._imul_div(other, operator.itruediv) + if not isinstance(self._magnitude, ndarray): + return self._mul_div(other, operator.truediv) + else: + return self._imul_div(other, operator.itruediv) def __truediv__(self, other): return self._mul_div(other, operator.truediv) def __ifloordiv__(self, other): - return self._imul_div(other, operator.ifloordiv, units_op=operator.itruediv) + if not isinstance(self._magnitude, ndarray): + return self._mul_div(other, operator.floordiv, units_op=operator.itruediv) + else: + return self._imul_div(other, operator.ifloordiv, units_op=operator.itruediv) def __floordiv__(self, other): return self._mul_div(other, operator.floordiv, units_op=operator.truediv) @@ -429,6 +475,9 @@ class _Quantity(object): __idiv__ = __itruediv__ def __ipow__(self, other): + if not isinstance(self._magnitude, ndarray): + return self.__pow__(other) + try: other_magnitude = _to_magnitude(other, self.force_ndarray) except TypeError: @@ -441,8 +490,18 @@ class _Quantity(object): return self def __pow__(self, other): - ret = copy.copy(self) - return operator.ipow(ret, other) + try: + other_magnitude = _to_magnitude(other, self.force_ndarray) + except TypeError: + return NotImplemented + else: + new_self = self + if not _only_multiplicative_units(self): + new_self = self.to_base_units() + + magnitude = new_self._magnitude ** _to_magnitude(other, self.force_ndarray) + units = new_self._units ** other + return self.__class__(magnitude, units) def __abs__(self): return self.__class__(abs(self._magnitude), self._units) diff --git a/pint/testsuite/__init__.py b/pint/testsuite/__init__.py index c29f7a7..98a506d 100644 --- a/pint/testsuite/__init__.py +++ b/pint/testsuite/__init__.py @@ -5,7 +5,7 @@ from __future__ import division, unicode_literals, print_function, absolute_impo import os import logging -from pint.compat import ndarray, unittest +from pint.compat import ndarray, unittest, np from pint import logger, UnitRegistry from pint.quantity import _Quantity @@ -37,6 +37,8 @@ class TestHandler(BufferingHandler): class TestCase(unittest.TestCase): + FORCE_NDARRAY = False + @classmethod def setUpClass(cls): cls.ureg = UnitRegistry(force_ndarray=cls.FORCE_NDARRAY) @@ -61,6 +63,35 @@ class TestCase(unittest.TestCase): seq2 = seq2.tolist() unittest.TestCase.assertSequenceEqual(self, seq1, seq2, msg, seq_type) + def assertQuantityAlmostEqual(self, first, second, places=None, msg=None, delta=None): + if msg is None: + msg = 'Comparing %r and %r. ' % (first, second) + + if isinstance(first, _Quantity) and isinstance(second, _Quantity): + second = second.to(first) + m1, m2 = first.magnitude, second.magnitude + self.assertEqual(first.units, second.units, msg=msg + 'Units are not equal.') + elif isinstance(first, _Quantity): + self.assertTrue(first.dimensionless, msg=msg + 'The first is not dimensionless.') + first = first.to('') + m1, m2 = first.magnitude, second + elif isinstance(second, _Quantity): + self.assertTrue(second.dimensionless, msg=msg + 'The second is not dimensionless.') + second = second.to('') + m1, m2 = first, second.magnitude + else: + m1, m2 = first, second + + if isinstance(m1, ndarray) or isinstance(m2, ndarray): + if delta is not None: + rtol, atol = 0, delta + else: + places = places or 7 + rtol, atol = 10 ** (-places), 0 + np.testing.assert_allclose(m1, m2, rtol=rtol, atol=atol, err_msg=msg) + else: + unittest.TestCase.assertAlmostEqual(self, m1, m2, places, msg, delta) + def assertAlmostEqual(self, first, second, places=None, msg=None, delta=None): if isinstance(first, _Quantity) and isinstance(second, _Quantity): second = second.to(first) diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index a2e7e7b..270bfaf 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -228,6 +228,26 @@ class TestIssues(TestCase): self.assertRaises(ValueError, func, 'METER') self.assertEqual(val, func('METER', False)) + def test_issue104(self): + ureg = UnitRegistry() + + x = [ureg('1 meter'), ureg('1 meter'), ureg('1 meter')] + y = [ureg('1 meter')] * 3 + + def summer(values): + if not values: + return 0 + total = values[0] + for v in values[1:]: + total += v + + return total + + self.assertQuantityAlmostEqual(summer(x), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(x[0], ureg.Quantity(1, 'meter')) + self.assertQuantityAlmostEqual(summer(y), ureg.Quantity(3, 'meter')) + self.assertQuantityAlmostEqual(y[0], ureg.Quantity(1, 'meter')) + @unittest.skipUnless(HAS_NUMPY, 'Numpy not present') class TestIssuesNP(TestCase): diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 0be2290..6ae0752 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -8,55 +8,14 @@ import operator as op from pint import DimensionalityError, UnitRegistry from pint.unit import UnitsContainer -from pint.compat import string_types, PYTHON3 -from pint.testsuite import TestCase +from pint.compat import string_types, PYTHON3, HAS_NUMPY, np +from pint.testsuite import TestCase, unittest class TestQuantity(TestCase): FORCE_NDARRAY = False - def _test_inplace(self, operator, value1, value2, expected_result): - if isinstance(value1, string_types): - value1 = self.Q_(value1) - if isinstance(value2, string_types): - value2 = self.Q_(value2) - if isinstance(expected_result, string_types): - expected_result = self.Q_(expected_result) - - value1 = copy.copy(value1) - value2 = copy.copy(value2) - id1 = id(value1) - id2 = id(value2) - value1 = operator(value1, value2) - value2_cpy = copy.copy(value2) - self.assertAlmostEqual(value1, expected_result) - self.assertEqual(id1, id(value1)) - self.assertAlmostEqual(value2, value2_cpy) - self.assertEqual(id2, id(value2)) - - def _test_not_inplace(self, operator, value1, value2, expected_result): - if isinstance(value1, string_types): - value1 = self.Q_(value1) - if isinstance(value2, string_types): - value2 = self.Q_(value2) - if isinstance(expected_result, string_types): - expected_result = self.Q_(expected_result) - - id1 = id(value1) - id2 = id(value2) - - value1_cpy = copy.copy(value1) - value2_cpy = copy.copy(value2) - - result = operator(value1, value2) - - self.assertAlmostEqual(expected_result, result) - self.assertAlmostEqual(value1, value1_cpy) - self.assertAlmostEqual(value2, value2_cpy) - self.assertNotEqual(id(result), id1) - self.assertNotEqual(id(result), id2) - def test_quantity_creation(self): for args in ((4.2, 'meter'), (4.2, UnitsContainer(meter=1)), @@ -162,100 +121,6 @@ class TestQuantity(TestCase): ureg.default_format = spec self.assertEqual('{0}'.format(x), result) - def test_quantity_add_sub(self): - x = self.Q_(1., 'centimeter') - y = self.Q_(1., 'inch') - z = self.Q_(1., 'second') - a = self.Q_(1., None) - - self._test_not_inplace(op.add, x, x, self.Q_(2., 'centimeter')) - self._test_not_inplace(op.add, x, y, self.Q_(1 + 2.54, 'centimeter')) - self._test_not_inplace(op.add, y, x, self.Q_(1 + 1 / 2.54, 'inch')) - self._test_not_inplace(op.add, a, 1, self.Q_(1 + 1, None)) - self.assertRaises(DimensionalityError, op.add, 10, x) - self.assertRaises(DimensionalityError, op.add, x, 10) - self.assertRaises(DimensionalityError, op.add, x, z) - - self._test_not_inplace(op.sub, x, x, self.Q_(0., 'centimeter')) - self._test_not_inplace(op.sub, x, y, self.Q_(1 - 2.54, 'centimeter')) - self._test_not_inplace(op.sub, y, x, self.Q_(1 - 1 / 2.54, 'inch')) - self._test_not_inplace(op.sub, a, 1, self.Q_(1 - 1, None)) - self.assertRaises(DimensionalityError, op.sub, 10, x) - self.assertRaises(DimensionalityError, op.sub, x, 10) - self.assertRaises(DimensionalityError, op.sub, x, z) - - def test_quantity_iadd_isub(self): - x = self.Q_(1., 'centimeter') - y = self.Q_(1., 'inch') - z = self.Q_(1., 'second') - a = self.Q_(1., None) - - self._test_inplace(op.iadd, x, x, self.Q_(2., 'centimeter')) - self._test_inplace(op.iadd, x, y, self.Q_(1 + 2.54, 'centimeter')) - self._test_inplace(op.iadd, y, x, self.Q_(1 + 1 / 2.54, 'inch')) - self._test_inplace(op.iadd, a, 1, self.Q_(1 + 1, None)) - self.assertRaises(DimensionalityError, op.iadd, 10, x) - self.assertRaises(DimensionalityError, op.iadd, x, 10) - self.assertRaises(DimensionalityError, op.iadd, x, z) - - self._test_inplace(op.isub, x, x, self.Q_(0., 'centimeter')) - self._test_inplace(op.isub, x, y, self.Q_(1 - 2.54, 'centimeter')) - self._test_inplace(op.isub, y, x, self.Q_(1 - 1 / 2.54, 'inch')) - self._test_inplace(op.isub, a, 1, self.Q_(1 - 1, None)) - self.assertRaises(DimensionalityError, op.sub, 10, x) - self.assertRaises(DimensionalityError, op.sub, x, 10) - self.assertRaises(DimensionalityError, op.sub, x, z) - - def test_quantity_mul_div(self): - self._test_not_inplace(op.mul, 10.0, '4.2*meter', '42*meter') - self._test_not_inplace(op.mul, '4.2*meter', 10.0, '42*meter') - self._test_not_inplace(op.mul, '4.2*meter', '10*inch', '42*meter*inch') - self._test_not_inplace(op.truediv, 42, '4.2*meter', '10/meter') - self._test_not_inplace(op.truediv, '4.2*meter', 10.0, '0.42*meter') - self._test_not_inplace(op.truediv, '4.2*meter', '10*inch', '0.42*meter/inch') - - def test_quantity_imul_idiv(self): - #self._test_inplace(op.imul, 10.0, '4.2*meter', '42*meter') - self._test_inplace(op.imul, '4.2*meter', 10.0, '42*meter') - self._test_inplace(op.imul, '4.2*meter', '10*inch', '42*meter*inch') - #self._test_not_inplace(op.truediv, 42, '4.2*meter', '10/meter') - self._test_inplace(op.itruediv, '4.2*meter', 10.0, '0.42*meter') - self._test_inplace(op.itruediv, '4.2*meter', '10*inch', '0.42*meter/inch') - - def test_quantity_floordiv(self): - self._test_not_inplace(op.floordiv, 10.0, '4.2*meter', '2/meter') - self._test_not_inplace(op.floordiv, '24*meter', 10.0, '2*meter') - self._test_not_inplace(op.floordiv, '10*meter', '4.2*inch', '2*meter/inch') - - #self._test_inplace(op.ifloordiv, 10.0, '4.2*meter', '2/meter') - self._test_inplace(op.ifloordiv, '24*meter', 10.0, '2*meter') - self._test_inplace(op.ifloordiv, '10*meter', '4.2*inch', '2*meter/inch') - - def test_quantity_abs_round(self): - - x = self.Q_(-4.2, 'meter') - y = self.Q_(4.2, 'meter') - # In Python 3+ round of x is delegated to x.__round__, instead of round(x.__float__) - # and therefore it can be properly implemented by Pint - for fun in (abs, op.pos, op.neg) + (round, ) if PYTHON3 else (): - zx = self.Q_(fun(x.magnitude), 'meter') - zy = self.Q_(fun(y.magnitude), 'meter') - rx = fun(x) - ry = fun(y) - self.assertEqual(rx, zx, 'while testing {0}'.format(fun)) - self.assertEqual(ry, zy, 'while testing {0}'.format(fun)) - self.assertIsNot(rx, zx, 'while testing {0}'.format(fun)) - self.assertIsNot(ry, zy, 'while testing {0}'.format(fun)) - - def test_quantity_float_complex(self): - x = self.Q_(-4.2, None) - y = self.Q_(4.2, None) - z = self.Q_(1, 'meter') - for fun in (float, complex): - self.assertEqual(fun(x), fun(x.magnitude)) - self.assertEqual(fun(y), fun(y.magnitude)) - self.assertRaises(DimensionalityError, fun, z) - def test_to_base_units(self): x = self.Q_('1*inch') self.assertAlmostEqual(x.to_base_units(), self.Q_(0.0254, 'meter')) @@ -351,6 +216,176 @@ class TestQuantity(TestCase): pickle_test(self.Q_(2.4, 'm/s')) +class TestQuantityBasicMath(TestCase): + + FORCE_NDARRAY = False + + def _test_inplace(self, operator, value1, value2, expected_result, unit=None): + if isinstance(value1, string_types): + value1 = self.Q_(value1) + if isinstance(value2, string_types): + value2 = self.Q_(value2) + if isinstance(expected_result, string_types): + expected_result = self.Q_(expected_result) + + if not unit is None: + value1 = value1 * unit + value2 = value2 * unit + expected_result = expected_result * unit + + value1 = copy.copy(value1) + value2 = copy.copy(value2) + id1 = id(value1) + id2 = id(value2) + value1 = operator(value1, value2) + value2_cpy = copy.copy(value2) + self.assertQuantityAlmostEqual(value1, expected_result) + self.assertEqual(id1, id(value1)) + self.assertQuantityAlmostEqual(value2, value2_cpy) + self.assertEqual(id2, id(value2)) + + def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None): + if isinstance(value1, string_types): + value1 = self.Q_(value1) + if isinstance(value2, string_types): + value2 = self.Q_(value2) + if isinstance(expected_result, string_types): + expected_result = self.Q_(expected_result) + + if not unit is None: + value1 = value1 * unit + value2 = value2 * unit + expected_result = expected_result * unit + + id1 = id(value1) + id2 = id(value2) + + value1_cpy = copy.copy(value1) + value2_cpy = copy.copy(value2) + + result = operator(value1, value2) + + self.assertQuantityAlmostEqual(expected_result, result) + self.assertQuantityAlmostEqual(value1, value1_cpy) + self.assertQuantityAlmostEqual(value2, value2_cpy) + self.assertNotEqual(id(result), id1) + self.assertNotEqual(id(result), id2) + + def _test_quantity_add_sub(self, unit, func): + x = self.Q_(unit, 'centimeter') + y = self.Q_(unit, 'inch') + z = self.Q_(unit, 'second') + a = self.Q_(unit, None) + + func(op.add, x, x, self.Q_(unit + unit, 'centimeter')) + func(op.add, x, y, self.Q_(unit + 2.54 * unit, 'centimeter')) + func(op.add, y, x, self.Q_(unit + unit / (2.54 * unit), 'inch')) + func(op.add, a, unit, self.Q_(unit + unit, None)) + self.assertRaises(DimensionalityError, op.add, 10, x) + self.assertRaises(DimensionalityError, op.add, x, 10) + self.assertRaises(DimensionalityError, op.add, x, z) + + func(op.sub, x, x, self.Q_(unit - unit, 'centimeter')) + func(op.sub, x, y, self.Q_(unit - 2.54 * unit, 'centimeter')) + func(op.sub, y, x, self.Q_(unit - unit / (2.54 * unit), 'inch')) + func(op.sub, a, unit, self.Q_(unit - unit, None)) + self.assertRaises(DimensionalityError, op.sub, 10, x) + self.assertRaises(DimensionalityError, op.sub, x, 10) + self.assertRaises(DimensionalityError, op.sub, x, z) + + def _test_quantity_iadd_isub(self, unit, func): + x = self.Q_(unit, 'centimeter') + y = self.Q_(unit, 'inch') + z = self.Q_(unit, 'second') + a = self.Q_(unit, None) + + func(op.iadd, x, x, self.Q_(unit + unit, 'centimeter')) + func(op.iadd, x, y, self.Q_(unit + 2.54 * unit, 'centimeter')) + func(op.iadd, y, x, self.Q_(unit + unit / 2.54, 'inch')) + func(op.iadd, a, unit, self.Q_(unit + unit, None)) + self.assertRaises(DimensionalityError, op.iadd, 10, x) + self.assertRaises(DimensionalityError, op.iadd, x, 10) + self.assertRaises(DimensionalityError, op.iadd, x, z) + + func(op.isub, x, x, self.Q_(unit - unit, 'centimeter')) + func(op.isub, x, y, self.Q_(unit - 2.54, 'centimeter')) + func(op.isub, y, x, self.Q_(unit - unit / 2.54, 'inch')) + func(op.isub, a, unit, self.Q_(unit - unit, None)) + self.assertRaises(DimensionalityError, op.sub, 10, x) + self.assertRaises(DimensionalityError, op.sub, x, 10) + self.assertRaises(DimensionalityError, op.sub, x, z) + + def _test_quantity_mul_div(self, unit, func): + func(op.mul, unit * 10.0, '4.2*meter', '42*meter', unit) + func(op.mul, '4.2*meter', unit * 10.0, '42*meter', unit) + func(op.mul, '4.2*meter', '10*inch', '42*meter*inch', unit) + func(op.truediv, unit * 42, '4.2*meter', '10/meter', unit) + func(op.truediv, '4.2*meter', unit * 10.0, '0.42*meter', unit) + func(op.truediv, '4.2*meter', '10*inch', '0.42*meter/inch', unit) + + def _test_quantity_imul_idiv(self, unit, func): + #func(op.imul, 10.0, '4.2*meter', '42*meter') + func(op.imul, '4.2*meter', 10.0, '42*meter', unit) + func(op.imul, '4.2*meter', '10*inch', '42*meter*inch', unit) + #func(op.truediv, 42, '4.2*meter', '10/meter') + func(op.itruediv, '4.2*meter', unit * 10.0, '0.42*meter', unit) + func(op.itruediv, '4.2*meter', '10*inch', '0.42*meter/inch', unit) + + def _test_quantity_floordiv(self, unit, func): + func(op.floordiv, unit * 10.0, '4.2*meter', '2/meter', unit) + func(op.floordiv, '24*meter', unit * 10.0, '2*meter', unit) + func(op.floordiv, '10*meter', '4.2*inch', '2*meter/inch', unit) + + def _test_quantity_ifloordiv(self, unit, func): + func(op.ifloordiv, 10.0, '4.2*meter', '2/meter', unit) + func(op.ifloordiv, '24*meter', 10.0, '2*meter', unit) + func(op.ifloordiv, '10*meter', '4.2*inch', '2*meter/inch', unit) + + def _test_numeric(self, unit, ifunc): + self._test_quantity_add_sub(unit, self._test_not_inplace) + self._test_quantity_iadd_isub(unit, ifunc) + self._test_quantity_mul_div(unit, self._test_not_inplace) + self._test_quantity_imul_idiv(unit, ifunc) + self._test_quantity_floordiv(unit, self._test_not_inplace) + #self._test_quantity_ifloordiv(unit, ifunc) + + def test_float(self): + self._test_numeric(1., self._test_not_inplace) + + def test_fraction(self): + import fractions + self._test_numeric(fractions.Fraction(1, 1), self._test_not_inplace) + + @unittest.skipUnless(HAS_NUMPY, 'Requires Numpy') + def test_nparray(self): + self._test_numeric(np.ones((1, 3)), self._test_inplace) + + def test_quantity_abs_round(self): + + x = self.Q_(-4.2, 'meter') + y = self.Q_(4.2, 'meter') + # In Python 3+ round of x is delegated to x.__round__, instead of round(x.__float__) + # and therefore it can be properly implemented by Pint + for fun in (abs, op.pos, op.neg) + (round, ) if PYTHON3 else (): + zx = self.Q_(fun(x.magnitude), 'meter') + zy = self.Q_(fun(y.magnitude), 'meter') + rx = fun(x) + ry = fun(y) + self.assertEqual(rx, zx, 'while testing {0}'.format(fun)) + self.assertEqual(ry, zy, 'while testing {0}'.format(fun)) + self.assertIsNot(rx, zx, 'while testing {0}'.format(fun)) + self.assertIsNot(ry, zy, 'while testing {0}'.format(fun)) + + def test_quantity_float_complex(self): + x = self.Q_(-4.2, None) + y = self.Q_(4.2, None) + z = self.Q_(1, 'meter') + for fun in (float, complex): + self.assertEqual(fun(x), fun(x.magnitude)) + self.assertEqual(fun(y), fun(y.magnitude)) + self.assertRaises(DimensionalityError, fun, z) + + class TestDimensions(TestCase): FORCE_NDARRAY = False |