1250 lines
50 KiB
Python
1250 lines
50 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
pint.quantity
|
|
~~~~~~~~~~~~~
|
|
|
|
:copyright: 2013 by Pint Authors, see AUTHORS for more details.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
|
|
from __future__ import division, unicode_literals, print_function, absolute_import
|
|
|
|
import copy
|
|
import math
|
|
import operator
|
|
import functools
|
|
|
|
from .formatting import remove_custom_flags
|
|
from .unit import (DimensionalityError, OffsetUnitCalculusError,
|
|
UnitsContainer, UnitDefinition, UndefinedUnitError)
|
|
from .compat import string_types, ndarray, np, _to_magnitude, long_type
|
|
from .util import logger
|
|
|
|
|
|
def _eq(first, second, check_all):
|
|
"""Comparison of scalars and arrays
|
|
"""
|
|
out = first == second
|
|
if check_all and isinstance(out, ndarray):
|
|
return np.all(out)
|
|
return out
|
|
|
|
|
|
class _Exception(Exception): # pragma: no cover
|
|
|
|
def __init__(self, internal):
|
|
self.internal = internal
|
|
|
|
|
|
def _check(q1, other):
|
|
"""Check Quantities before math operations.
|
|
|
|
Return True if q1 and other are from the same class.
|
|
Raise a ValueError if other has a different _REGISTRY than q1.
|
|
|
|
In other case, return False.
|
|
"""
|
|
|
|
if isinstance(other, q1.__class__):
|
|
# Both quantities are the same class and therefore from the same registry.
|
|
# (Each registry has its own Quantity class)
|
|
return True
|
|
elif q1._REGISTRY is getattr(other, '_REGISTRY', None):
|
|
return True
|
|
elif isinstance(other, _Quantity):
|
|
# The other object is a Quantity but from another registry.
|
|
raise ValueError('Cannot operate between quantities of different registries')
|
|
|
|
return False
|
|
|
|
|
|
class _Quantity(object):
|
|
"""Implements a class to describe a physical quantity:
|
|
the product of a numerical value and a unit of measurement.
|
|
|
|
:param value: value of the physical quantity to be created.
|
|
:type value: str, Quantity or any numeric type.
|
|
:param units: units of the physical quantity to be created.
|
|
:type units: UnitsContainer, str or Quantity.
|
|
"""
|
|
|
|
#: Default formatting string.
|
|
default_format = ''
|
|
|
|
def __reduce__(self):
|
|
from . import _build_quantity
|
|
return _build_quantity, (self.magnitude, self.units)
|
|
|
|
def __new__(cls, value, units=None):
|
|
if units is None:
|
|
if isinstance(value, string_types):
|
|
if value == '':
|
|
raise ValueError('Expression to parse as Quantity cannot be an empty string.')
|
|
inst = cls._REGISTRY.parse_expression(value)
|
|
return cls.__new__(cls, inst)
|
|
elif isinstance(value, cls):
|
|
inst = copy.copy(value)
|
|
else:
|
|
inst = object.__new__(cls)
|
|
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
|
|
inst._units = UnitsContainer()
|
|
elif isinstance(units, (UnitsContainer, UnitDefinition)):
|
|
inst = object.__new__(cls)
|
|
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
|
|
inst._units = units
|
|
elif isinstance(units, string_types):
|
|
inst = object.__new__(cls)
|
|
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
|
|
inst._units = inst._REGISTRY.parse_units(units)
|
|
elif isinstance(units, cls):
|
|
if units.magnitude != 1:
|
|
logger.warning('Creating new Quantity using a non unity Quantity as units.')
|
|
inst = copy.copy(units)
|
|
inst._magnitude = _to_magnitude(value, inst.force_ndarray)
|
|
else:
|
|
raise TypeError('units must be of type str, Quantity or '
|
|
'UnitsContainer; not {0}.'.format(type(units)))
|
|
|
|
inst.__used = False
|
|
inst.__handling = None
|
|
return inst
|
|
|
|
@property
|
|
def debug_used(self):
|
|
return self.__used
|
|
|
|
def __copy__(self):
|
|
ret = self.__class__(copy.copy(self._magnitude), copy.copy(self._units))
|
|
ret.__used = self.__used
|
|
return ret
|
|
|
|
def __str__(self):
|
|
return format(self)
|
|
|
|
def __repr__(self):
|
|
return "<Quantity({0}, '{1}')>".format(self._magnitude, self._units)
|
|
|
|
def __format__(self, spec):
|
|
spec = spec or self.default_format
|
|
|
|
if '~' in spec:
|
|
units = UnitsContainer(dict((self._REGISTRY._get_symbol(key), value)
|
|
for key, value in self.units.items()))
|
|
spec = spec.replace('~', '')
|
|
else:
|
|
units = self.units
|
|
|
|
return '%s %s' % (format(self.magnitude, remove_custom_flags(spec)),
|
|
format(units, spec))
|
|
|
|
# IPython related code
|
|
def _repr_html_(self):
|
|
return self.__format__('H')
|
|
|
|
def _repr_latex_(self):
|
|
return "$" + self.__format__('L') + "$"
|
|
|
|
@property
|
|
def magnitude(self):
|
|
"""Quantity's magnitude.
|
|
"""
|
|
return self._magnitude
|
|
|
|
@property
|
|
def units(self):
|
|
"""Quantity's units.
|
|
|
|
:rtype: UnitContainer
|
|
"""
|
|
return self._units
|
|
|
|
@property
|
|
def unitless(self):
|
|
"""Return true if the quantity does not have units.
|
|
"""
|
|
return not bool(self.to_base_units().units)
|
|
|
|
@property
|
|
def dimensionless(self):
|
|
"""Return true if the quantity is dimensionless.
|
|
"""
|
|
tmp = copy.copy(self).to_base_units()
|
|
|
|
return not bool(tmp.dimensionality)
|
|
|
|
@property
|
|
def dimensionality(self):
|
|
"""Quantity's dimensionality (e.g. {length: 1, time: -1})
|
|
"""
|
|
try:
|
|
return self._dimensionality
|
|
except AttributeError:
|
|
self._dimensionality = self._REGISTRY.get_dimensionality(self.units)
|
|
|
|
return self._dimensionality
|
|
|
|
def compatible_units(self, *contexts):
|
|
if contexts:
|
|
with self._REGISTRY.context(*contexts):
|
|
return self._REGISTRY.get_compatible_units(self._units)
|
|
|
|
return self._REGISTRY.get_compatible_units(self._units)
|
|
|
|
def _convert_magnitude_not_inplace(self, other, *contexts, **ctx_kwargs):
|
|
if contexts:
|
|
with self._REGISTRY.context(*contexts, **ctx_kwargs):
|
|
return self._REGISTRY.convert(self._magnitude, self._units, other)
|
|
|
|
return self._REGISTRY.convert(self._magnitude, self._units, other)
|
|
|
|
def _convert_magnitude(self, other, *contexts, **ctx_kwargs):
|
|
if contexts:
|
|
with self._REGISTRY.context(*contexts, **ctx_kwargs):
|
|
return self._REGISTRY.convert(self._magnitude, self._units, other)
|
|
|
|
return self._REGISTRY.convert(self._magnitude, self._units, other,
|
|
inplace=isinstance(self._magnitude, ndarray))
|
|
|
|
def ito(self, other=None, *contexts, **ctx_kwargs):
|
|
"""Inplace rescale to different units.
|
|
|
|
:param other: destination units.
|
|
:type other: Quantity, str or dict
|
|
"""
|
|
if isinstance(other, string_types):
|
|
other = self._REGISTRY.parse_units(other)
|
|
elif isinstance(other, self.__class__):
|
|
other = copy.copy(other.units)
|
|
elif isinstance(other, UnitsContainer):
|
|
pass
|
|
else:
|
|
other = UnitsContainer(other)
|
|
|
|
self._magnitude = self._convert_magnitude(other, *contexts, **ctx_kwargs)
|
|
self._units = other
|
|
|
|
return None
|
|
|
|
def to(self, other=None, *contexts, **ctx_kwargs):
|
|
"""Return Quantity rescaled to different units.
|
|
|
|
:param other: destination units.
|
|
:type other: Quantity, str or dict
|
|
"""
|
|
if isinstance(other, string_types):
|
|
other = self._REGISTRY.parse_units(other)
|
|
elif isinstance(other, self.__class__):
|
|
other = copy.copy(other.units)
|
|
elif isinstance(other, UnitsContainer):
|
|
pass
|
|
else:
|
|
other = UnitsContainer(other)
|
|
|
|
magnitude = self._convert_magnitude_not_inplace(other, *contexts, **ctx_kwargs)
|
|
|
|
return self.__class__(magnitude, other)
|
|
|
|
def ito_base_units(self):
|
|
"""Return Quantity rescaled to base units
|
|
"""
|
|
|
|
_, other = self._REGISTRY.get_base_units(self.units)
|
|
|
|
self._magnitude = self._convert_magnitude(other)
|
|
self._units = other
|
|
|
|
return None
|
|
|
|
def to_base_units(self):
|
|
"""Return Quantity rescaled to base units
|
|
"""
|
|
|
|
_, other = self._REGISTRY.get_base_units(self.units)
|
|
|
|
magnitude = self._convert_magnitude_not_inplace(other)
|
|
|
|
return self.__class__(magnitude, other)
|
|
|
|
# Mathematical operations
|
|
def __int__(self):
|
|
if self.dimensionless:
|
|
return int(self._convert_magnitude_not_inplace(UnitsContainer()))
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
def __long__(self):
|
|
if self.dimensionless:
|
|
return long_type(self._convert_magnitude_not_inplace(UnitsContainer()))
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
def __float__(self):
|
|
if self.dimensionless:
|
|
return float(self._convert_magnitude_not_inplace(UnitsContainer()))
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
def __complex__(self):
|
|
if self.dimensionless:
|
|
return complex(self._convert_magnitude_not_inplace(UnitsContainer()))
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
def _iadd_sub(self, other, op):
|
|
"""Perform addition or subtraction operation in-place and return the result.
|
|
|
|
:param other: object to be added to / subtracted from self
|
|
:type other: Quantity or any type accepted by :func:`_to_magnitude`
|
|
:param op: operator function (e.g. operator.add, operator.isub)
|
|
:type op: function
|
|
"""
|
|
if not _check(self, other):
|
|
# other not from same Registry or not a Quantity
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
if _eq(other, 0, True):
|
|
# If the other value is 0 (but not Quantity 0)
|
|
# do the operation without checking units.
|
|
# We do the calculation instead of just returning the same value to
|
|
# enforce any shape checking and type casting due to the operation.
|
|
self._magnitude = op(self._magnitude, other_magnitude)
|
|
elif self.dimensionless:
|
|
self.ito(UnitsContainer())
|
|
self._magnitude = op(self._magnitude, other_magnitude)
|
|
else:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
return self
|
|
|
|
if not self.dimensionality == other.dimensionality:
|
|
raise DimensionalityError(self.units, other.units,
|
|
self.dimensionality,
|
|
other.dimensionality)
|
|
|
|
# Next we define some variables to make if-clauses more readable.
|
|
self_non_mul_units = self._get_non_multiplicative_units()
|
|
is_self_multiplicative = len(self_non_mul_units) == 0
|
|
if len(self_non_mul_units) == 1:
|
|
self_non_mul_unit = self_non_mul_units[0]
|
|
other_non_mul_units = other._get_non_multiplicative_units()
|
|
is_other_multiplicative = len(other_non_mul_units) == 0
|
|
if len(other_non_mul_units) == 1:
|
|
other_non_mul_unit = other_non_mul_units[0]
|
|
|
|
# Presence of non-multiplicative units gives rise to several cases.
|
|
if is_self_multiplicative and is_other_multiplicative:
|
|
if self._units == other._units:
|
|
self._magnitude = op(self._magnitude, other._magnitude)
|
|
# If only self has a delta unit, other determines unit of result.
|
|
elif self._get_delta_units() and not other._get_delta_units():
|
|
self._magnitude = op(self._convert_magnitude(other.units),
|
|
other._magnitude)
|
|
self._units = copy.copy(other.units)
|
|
else:
|
|
self._magnitude = op(self._magnitude,
|
|
other.to(self.units)._magnitude)
|
|
|
|
elif (op == operator.isub and len(self_non_mul_units) == 1
|
|
and self.units[self_non_mul_unit] == 1
|
|
and not other._has_compatible_delta(self_non_mul_unit)):
|
|
if self.units == other.units:
|
|
self._magnitude = op(self._magnitude, other._magnitude)
|
|
else:
|
|
self._magnitude = op(self._magnitude,
|
|
other.to(self.units)._magnitude)
|
|
self.units['delta_' + self_non_mul_unit
|
|
] = self.units.pop(self_non_mul_unit)
|
|
|
|
elif (op == operator.isub and len(other_non_mul_units) == 1
|
|
and other.units[other_non_mul_unit] == 1
|
|
and not self._has_compatible_delta(other_non_mul_unit)):
|
|
# we convert to self directly since it is multiplicative
|
|
self._magnitude = op(self._magnitude,
|
|
other.to(self.units)._magnitude)
|
|
|
|
elif (len(self_non_mul_units) == 1
|
|
# order of the dimension of offset unit == 1 ?
|
|
and self._units[self_non_mul_unit] == 1
|
|
and other._has_compatible_delta(self_non_mul_unit)):
|
|
tu = copy.copy(self.units)
|
|
# Replace offset unit in self by the corresponding delta unit.
|
|
# This is done to prevent a shift by offset in the to()-call.
|
|
tu['delta_' + self_non_mul_unit] = tu.pop(self_non_mul_unit)
|
|
self._magnitude = op(self._magnitude, other.to(tu)._magnitude)
|
|
elif (len(other_non_mul_units) == 1
|
|
# order of the dimension of offset unit == 1 ?
|
|
and other._units[other_non_mul_unit] == 1
|
|
and self._has_compatible_delta(other_non_mul_unit)):
|
|
tu = copy.copy(other.units)
|
|
# Replace offset unit in other by the corresponding delta unit.
|
|
# This is done to prevent a shift by offset in the to()-call.
|
|
tu['delta_' + other_non_mul_unit] = tu.pop(other_non_mul_unit)
|
|
self._magnitude = op(self._convert_magnitude(tu), other._magnitude)
|
|
self._units = copy.copy(other.units)
|
|
else:
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
|
|
return self
|
|
|
|
def _add_sub(self, other, op):
|
|
"""Perform addition or subtraction operation and return the result.
|
|
|
|
:param other: object to be added to / subtracted from self
|
|
:type other: Quantity or any type accepted by :func:`_to_magnitude`
|
|
:param op: operator function (e.g. operator.add, operator.isub)
|
|
:type op: function
|
|
"""
|
|
if not _check(self, other):
|
|
# other not from same Registry or not a Quantity
|
|
if _eq(other, 0, True):
|
|
# If the other value is 0 (but not Quantity 0)
|
|
# do the operation without checking units.
|
|
# We do the calculation instead of just returning the same value to
|
|
# enforce any shape checking and type casting due to the operation.
|
|
units = self.units
|
|
magnitude = op(self._magnitude,
|
|
_to_magnitude(other, self.force_ndarray))
|
|
elif self.dimensionless:
|
|
units = UnitsContainer()
|
|
magnitude = op(self.to(units)._magnitude,
|
|
_to_magnitude(other, self.force_ndarray))
|
|
else:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
return self.__class__(magnitude, units)
|
|
|
|
if not self.dimensionality == other.dimensionality:
|
|
raise DimensionalityError(self.units, other.units,
|
|
self.dimensionality,
|
|
other.dimensionality)
|
|
|
|
# Next we define some variables to make if-clauses more readable.
|
|
self_non_mul_units = self._get_non_multiplicative_units()
|
|
is_self_multiplicative = len(self_non_mul_units) == 0
|
|
if len(self_non_mul_units) == 1:
|
|
self_non_mul_unit = self_non_mul_units[0]
|
|
other_non_mul_units = other._get_non_multiplicative_units()
|
|
is_other_multiplicative = len(other_non_mul_units) == 0
|
|
if len(other_non_mul_units) == 1:
|
|
other_non_mul_unit = other_non_mul_units[0]
|
|
|
|
# Presence of non-multiplicative units gives rise to several cases.
|
|
if is_self_multiplicative and is_other_multiplicative:
|
|
if self._units == other._units:
|
|
magnitude = op(self._magnitude, other._magnitude)
|
|
units = copy.copy(self.units)
|
|
# If only self has a delta unit, other determines unit of result.
|
|
elif self._get_delta_units() and not other._get_delta_units():
|
|
magnitude = op(self._convert_magnitude(other.units),
|
|
other._magnitude)
|
|
units = copy.copy(other.units)
|
|
else:
|
|
units = copy.copy(self.units)
|
|
magnitude = op(self._magnitude,
|
|
other.to(self.units).magnitude)
|
|
|
|
elif (op == operator.sub and len(self_non_mul_units) == 1
|
|
and self.units[self_non_mul_unit] == 1
|
|
and not other._has_compatible_delta(self_non_mul_unit)):
|
|
if self.units == other.units:
|
|
magnitude = op(self._magnitude, other._magnitude)
|
|
else:
|
|
magnitude = op(self._magnitude,
|
|
other.to(self.units)._magnitude)
|
|
units = copy.copy(self.units)
|
|
units['delta_' + self_non_mul_unit] = units.pop(self_non_mul_unit)
|
|
|
|
elif (op == operator.sub and len(other_non_mul_units) == 1
|
|
and other.units[other_non_mul_unit] == 1
|
|
and not self._has_compatible_delta(other_non_mul_unit)):
|
|
# we convert to self directly since it is multiplicative
|
|
magnitude = op(self._magnitude,
|
|
other.to(self.units)._magnitude)
|
|
units = copy.copy(self.units)
|
|
|
|
elif (len(self_non_mul_units) == 1
|
|
# order of the dimension of offset unit == 1 ?
|
|
and self._units[self_non_mul_unit] == 1
|
|
and other._has_compatible_delta(self_non_mul_unit)):
|
|
tu = copy.copy(self.units)
|
|
# Replace offset unit in self by the corresponding delta unit.
|
|
# This is done to prevent a shift by offset in the to()-call.
|
|
tu['delta_' + self_non_mul_unit] = tu.pop(self_non_mul_unit)
|
|
magnitude = op(self._magnitude, other.to(tu).magnitude)
|
|
units = copy.copy(self.units)
|
|
elif (len(other_non_mul_units) == 1
|
|
# order of the dimension of offset unit == 1 ?
|
|
and other._units[other_non_mul_unit] == 1
|
|
and self._has_compatible_delta(other_non_mul_unit)):
|
|
tu = copy.copy(other.units)
|
|
# Replace offset unit in other by the corresponding delta unit.
|
|
# This is done to prevent a shift by offset in the to()-call.
|
|
tu['delta_' + other_non_mul_unit] = tu.pop(other_non_mul_unit)
|
|
magnitude = op(self._convert_magnitude(tu), other._magnitude)
|
|
units = copy.copy(other.units)
|
|
else:
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
|
|
return self.__class__(magnitude, units)
|
|
|
|
def __iadd__(self, other):
|
|
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)
|
|
|
|
__radd__ = __add__
|
|
|
|
def __isub__(self, other):
|
|
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)
|
|
|
|
def __rsub__(self, other):
|
|
return -self._add_sub(other, operator.sub)
|
|
|
|
def _imul_div(self, other, magnitude_op, units_op=None):
|
|
"""Perform multiplication or division operation in-place 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
|
|
|
|
offset_units_self = self._get_non_multiplicative_units()
|
|
no_offset_units_self = len(offset_units_self)
|
|
|
|
if not _check(self, other):
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units,
|
|
getattr(other, 'units', ''))
|
|
if len(offset_units_self) == 1:
|
|
if (self.units[offset_units_self[0]] != 1
|
|
or magnitude_op not in [operator.mul, operator.imul]):
|
|
raise OffsetUnitCalculusError(self.units,
|
|
getattr(other, 'units', ''))
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
self._magnitude = magnitude_op(self._magnitude, other_magnitude)
|
|
self._units = units_op(self._units, UnitsContainer())
|
|
return self
|
|
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
elif no_offset_units_self == 1 and len(self.units) == 1:
|
|
self.ito_base_units()
|
|
|
|
no_offset_units_other = len(other._get_non_multiplicative_units())
|
|
|
|
if not other._ok_for_muldiv(no_offset_units_other):
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
elif no_offset_units_other == 1 and len(other.units) == 1:
|
|
other.ito_base_units()
|
|
|
|
self._magnitude = magnitude_op(self._magnitude, other._magnitude)
|
|
self._units = units_op(self._units, other._units)
|
|
|
|
return self
|
|
|
|
def _mul_div(self, other, magnitude_op, units_op=None):
|
|
"""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
|
|
|
|
offset_units_self = self._get_non_multiplicative_units()
|
|
no_offset_units_self = len(offset_units_self)
|
|
|
|
if not _check(self, other):
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units,
|
|
getattr(other, 'units', ''))
|
|
if len(offset_units_self) == 1:
|
|
if (self.units[offset_units_self[0]] != 1
|
|
or magnitude_op not in [operator.mul, operator.imul]):
|
|
raise OffsetUnitCalculusError(self.units,
|
|
getattr(other, 'units', ''))
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
|
|
magnitude = magnitude_op(self._magnitude, other_magnitude)
|
|
units = units_op(self._units, UnitsContainer())
|
|
|
|
return self.__class__(magnitude, units)
|
|
|
|
new_self = self
|
|
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
elif no_offset_units_self == 1 and len(self.units) == 1:
|
|
new_self = self.to_base_units()
|
|
|
|
no_offset_units_other = len(other._get_non_multiplicative_units())
|
|
|
|
if not other._ok_for_muldiv(no_offset_units_other):
|
|
raise OffsetUnitCalculusError(self.units, other.units)
|
|
elif no_offset_units_other == 1 and len(other.units) == 1:
|
|
other = other.to_base_units()
|
|
|
|
magnitude = magnitude_op(new_self._magnitude, other._magnitude)
|
|
units = units_op(new_self._units, other._units)
|
|
|
|
return self.__class__(magnitude, units)
|
|
|
|
def __imul__(self, other):
|
|
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)
|
|
|
|
__rmul__ = __mul__
|
|
|
|
def __itruediv__(self, other):
|
|
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):
|
|
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)
|
|
|
|
def __rtruediv__(self, other):
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
|
|
no_offset_units_self = len(self._get_non_multiplicative_units())
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units, '')
|
|
elif no_offset_units_self == 1 and len(self.units) == 1:
|
|
self = self.to_base_units()
|
|
|
|
return self.__class__(other_magnitude / self._magnitude, 1 / self._units)
|
|
|
|
def __rfloordiv__(self, other):
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
|
|
no_offset_units_self = len(self._get_non_multiplicative_units())
|
|
if not self._ok_for_muldiv(no_offset_units_self):
|
|
raise OffsetUnitCalculusError(self.units, '')
|
|
elif no_offset_units_self == 1 and len(self.units) == 1:
|
|
self = self.to_base_units()
|
|
|
|
return self.__class__(other_magnitude // self._magnitude, 1 / self._units)
|
|
|
|
__div__ = __truediv__
|
|
__rdiv__ = __rtruediv__
|
|
__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:
|
|
return NotImplemented
|
|
else:
|
|
if not self._ok_for_muldiv:
|
|
raise OffsetUnitCalculusError(self.units)
|
|
|
|
if isinstance(getattr(other, '_magnitude', other), ndarray):
|
|
# arrays are refused as exponent, because they would create
|
|
# len(array) quanitites of len(set(array)) different units
|
|
if np.size(other) > 1:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
new_self = self
|
|
if other == 1:
|
|
return self
|
|
elif other == 0:
|
|
self._units = UnitsContainer()
|
|
else:
|
|
if not self._is_multiplicative:
|
|
if self._REGISTRY.autoconvert_offset_to_baseunit:
|
|
self.ito_base_units()
|
|
else:
|
|
raise OffsetUnitCalculusError(self.units)
|
|
|
|
if getattr(other, 'dimensionless', False):
|
|
other = other.to_base_units()
|
|
self._units **= other.magnitude
|
|
elif not getattr(other, 'dimensionless', True):
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
else:
|
|
self._units **= other
|
|
|
|
self._magnitude **= _to_magnitude(other, self.force_ndarray)
|
|
return self
|
|
|
|
def __pow__(self, other):
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
else:
|
|
if not self._ok_for_muldiv:
|
|
raise OffsetUnitCalculusError(self.units)
|
|
|
|
if isinstance(getattr(other, '_magnitude', other), ndarray):
|
|
# arrays are refused as exponent, because they would create
|
|
# len(array) quantities of len(set(array)) different units
|
|
if np.size(other) > 1:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
|
|
new_self = self
|
|
if other == 1:
|
|
return self
|
|
elif other == 0:
|
|
units = UnitsContainer()
|
|
else:
|
|
if not self._is_multiplicative:
|
|
if self._REGISTRY.autoconvert_offset_to_baseunit:
|
|
new_self = self.to_base_units()
|
|
else:
|
|
raise OffsetUnitCalculusError(self.units)
|
|
|
|
if getattr(other, 'dimensionless', False):
|
|
units = new_self._units ** other.to_base_units().magnitude
|
|
elif not getattr(other, 'dimensionless', True):
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
else:
|
|
units = new_self._units ** other
|
|
|
|
magnitude = new_self._magnitude ** _to_magnitude(other, self.force_ndarray)
|
|
return self.__class__(magnitude, units)
|
|
|
|
def __rpow__(self, other):
|
|
try:
|
|
other_magnitude = _to_magnitude(other, self.force_ndarray)
|
|
except TypeError:
|
|
return NotImplemented
|
|
else:
|
|
if not self.dimensionless:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
if isinstance(self._magnitude, ndarray):
|
|
if np.size(self._magnitude) > 1:
|
|
raise DimensionalityError(self.units, 'dimensionless')
|
|
new_self = self.to_base_units()
|
|
return other**new_self._magnitude
|
|
|
|
def __abs__(self):
|
|
return self.__class__(abs(self._magnitude), self._units)
|
|
|
|
def __round__(self, ndigits=0):
|
|
return self.__class__(round(self._magnitude, ndigits=ndigits), self._units)
|
|
|
|
def __pos__(self):
|
|
return self.__class__(operator.pos(self._magnitude), self._units)
|
|
|
|
def __neg__(self):
|
|
return self.__class__(operator.neg(self._magnitude), self._units)
|
|
|
|
def __eq__(self, other):
|
|
# We compare to the base class of Quantity because
|
|
# each Quantity class is unique.
|
|
if not isinstance(other, _Quantity):
|
|
return (self.dimensionless and
|
|
_eq(self._convert_magnitude(UnitsContainer()), other, False))
|
|
|
|
if _eq(self._magnitude, 0, True) and _eq(other._magnitude, 0, True):
|
|
return self.dimensionality == other.dimensionality
|
|
|
|
if self._units == other._units:
|
|
return _eq(self._magnitude, other._magnitude, False)
|
|
|
|
try:
|
|
return _eq(self.to(other).magnitude, other._magnitude, False)
|
|
except DimensionalityError:
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
out = self.__eq__(other)
|
|
if isinstance(out, ndarray):
|
|
return np.logical_not(out)
|
|
return not out
|
|
|
|
def compare(self, other, op):
|
|
if not isinstance(other, self.__class__):
|
|
if self.dimensionless:
|
|
return op(self._convert_magnitude_not_inplace(UnitsContainer()), other)
|
|
else:
|
|
raise ValueError('Cannot compare Quantity and {0}'.format(type(other)))
|
|
|
|
if self.units == other.units:
|
|
return op(self._magnitude, other._magnitude)
|
|
if self.dimensionality != other.dimensionality:
|
|
raise DimensionalityError(self.units, other.units,
|
|
self.dimensionality, other.dimensionality)
|
|
return op(self.to_base_units().magnitude,
|
|
other.to_base_units().magnitude)
|
|
|
|
__lt__ = lambda self, other: self.compare(other, op=operator.lt)
|
|
__le__ = lambda self, other: self.compare(other, op=operator.le)
|
|
__ge__ = lambda self, other: self.compare(other, op=operator.ge)
|
|
__gt__ = lambda self, other: self.compare(other, op=operator.gt)
|
|
|
|
def __bool__(self):
|
|
return bool(self._magnitude)
|
|
|
|
__nonzero__ = __bool__
|
|
|
|
# NumPy Support
|
|
__radian = 'radian'
|
|
__same_units = 'equal greater greater_equal less less_equal not_equal arctan2'.split()
|
|
#: Dictionary mapping ufunc/attributes names to the units that they
|
|
#: require (conversion will be tried).
|
|
__require_units = {'cumprod': '',
|
|
'arccos': '', 'arcsin': '', 'arctan': '',
|
|
'arccosh': '', 'arcsinh': '', 'arctanh': '',
|
|
'exp': '', 'expm1': '', 'exp2': '',
|
|
'log': '', 'log10': '', 'log1p': '', 'log2': '',
|
|
'sin': __radian, 'cos': __radian, 'tan': __radian,
|
|
'sinh': __radian, 'cosh': __radian, 'tanh': __radian,
|
|
'radians': 'degree', 'degrees': __radian,
|
|
'deg2rad': 'degree', 'rad2deg': __radian,
|
|
'logaddexp': '', 'logaddexp2': ''}
|
|
|
|
#: Dictionary mapping ufunc/attributes names to the units that they
|
|
#: will set on output.
|
|
__set_units = {'cos': '', 'sin': '', 'tan': '',
|
|
'cosh': '', 'sinh': '', 'tanh': '',
|
|
'arccos': __radian, 'arcsin': __radian,
|
|
'arctan': __radian, 'arctan2': __radian,
|
|
'arccosh': __radian, 'arcsinh': __radian,
|
|
'arctanh': __radian,
|
|
'degrees': 'degree', 'radians': __radian,
|
|
'expm1': '', 'cumprod': '',
|
|
'rad2deg': 'degree', 'deg2rad': __radian}
|
|
|
|
#: List of ufunc/attributes names in which units are copied from the
|
|
#: original.
|
|
__copy_units = 'compress conj conjugate copy cumsum diagonal flatten ' \
|
|
'max mean min ptp ravel repeat reshape round ' \
|
|
'squeeze std sum take trace transpose ' \
|
|
'ceil floor hypot rint ' \
|
|
'add subtract ' \
|
|
'copysign nextafter trunc ' \
|
|
'frexp ldexp modf modf__1 ' \
|
|
'absolute negative remainder fmod mod'.split()
|
|
|
|
#: Dictionary mapping ufunc/attributes names to the units that they will
|
|
#: set on output. The value is interpreted as the power to which the unit
|
|
#: will be raised.
|
|
__prod_units = {'var': 2, 'prod': 'size', 'multiply': 'mul',
|
|
'true_divide': 'div', 'divide': 'div', 'floor_divide': 'div',
|
|
'remainder': 'div',
|
|
'sqrt': .5, 'square': 2, 'reciprocal': -1}
|
|
|
|
__skip_other_args = 'ldexp multiply ' \
|
|
'true_divide divide floor_divide fmod mod ' \
|
|
'remainder'.split()
|
|
|
|
__handled = tuple(__same_units) + \
|
|
tuple(__require_units.keys()) + \
|
|
tuple(__prod_units.keys()) + \
|
|
tuple(__copy_units) + tuple(__skip_other_args)
|
|
|
|
def clip(self, first=None, second=None, out=None, **kwargs):
|
|
min = kwargs.get('min', first)
|
|
max = kwargs.get('max', second)
|
|
|
|
if min is None and max is None:
|
|
raise TypeError('clip() takes at least 3 arguments (2 given)')
|
|
|
|
if max is None and 'min' not in kwargs:
|
|
min, max = max, min
|
|
|
|
kwargs = {'out': out}
|
|
|
|
if min is not None:
|
|
if isinstance(min, self.__class__):
|
|
kwargs['min'] = min.to(self).magnitude
|
|
elif self.dimensionless:
|
|
kwargs['min'] = min
|
|
else:
|
|
raise DimensionalityError('dimensionless', self.units)
|
|
|
|
if max is not None:
|
|
if isinstance(max, self.__class__):
|
|
kwargs['max'] = max.to(self).magnitude
|
|
elif self.dimensionless:
|
|
kwargs['max'] = max
|
|
else:
|
|
raise DimensionalityError('dimensionless', self.units)
|
|
|
|
return self.__class__(self.magnitude.clip(**kwargs), self._units)
|
|
|
|
def fill(self, value):
|
|
self._units = value.units
|
|
return self.magnitude.fill(value.magnitude)
|
|
|
|
def put(self, indices, values, mode='raise'):
|
|
if isinstance(values, self.__class__):
|
|
values = values.to(self).magnitude
|
|
elif self.dimensionless:
|
|
values = self.__class__(values, '').to(self)
|
|
else:
|
|
raise DimensionalityError('dimensionless', self.units)
|
|
self.magnitude.put(indices, values, mode)
|
|
|
|
@property
|
|
def real(self):
|
|
return self.__class__(self._magnitude.real, self.units)
|
|
|
|
@property
|
|
def imag(self):
|
|
return self.__class__(self._magnitude.imag, self.units)
|
|
|
|
@property
|
|
def T(self):
|
|
return self.__class__(self._magnitude.T, self.units)
|
|
|
|
def searchsorted(self, v, side='left'):
|
|
if isinstance(v, self.__class__):
|
|
v = v.to(self).magnitude
|
|
elif self.dimensionless:
|
|
v = self.__class__(v, '').to(self)
|
|
else:
|
|
raise DimensionalityError('dimensionless', self.units)
|
|
return self.magnitude.searchsorted(v, side)
|
|
|
|
def __ito_if_needed(self, to_units):
|
|
if self.unitless and to_units == 'radian':
|
|
return
|
|
|
|
self.ito(to_units)
|
|
|
|
def __numpy_method_wrap(self, func, *args, **kwargs):
|
|
"""Convenience method to wrap on the fly numpy method taking
|
|
care of the units.
|
|
"""
|
|
if func.__name__ in self.__require_units:
|
|
self.__ito_if_needed(self.__require_units[func.__name__])
|
|
|
|
value = func(*args, **kwargs)
|
|
|
|
if func.__name__ in self.__copy_units:
|
|
return self.__class__(value, self._units)
|
|
|
|
if func.__name__ in self.__prod_units:
|
|
tmp = self.__prod_units[func.__name__]
|
|
if tmp == 'size':
|
|
return self.__class__(value, self._units ** self._magnitude.size)
|
|
return self.__class__(value, self._units ** tmp)
|
|
|
|
return value
|
|
|
|
def __len__(self):
|
|
return len(self._magnitude)
|
|
|
|
def __iter__(self):
|
|
# Allow exception to propagate in case of non-iterable magnitude
|
|
it_mag = iter(self.magnitude)
|
|
return iter((self.__class__(mag, self._units) for mag in it_mag))
|
|
|
|
def __getattr__(self, item):
|
|
# Attributes starting with `__array_` are common attributes of NumPy ndarray.
|
|
# They are requested by numpy functions.
|
|
if item.startswith('__array_'):
|
|
if isinstance(self._magnitude, ndarray):
|
|
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.
|
|
self._magnitude = _to_magnitude(self._magnitude, force_ndarray=True)
|
|
return getattr(self._magnitude, item)
|
|
elif item in self.__handled:
|
|
if not isinstance(self._magnitude, ndarray):
|
|
self._magnitude = _to_magnitude(self._magnitude, True)
|
|
attr = getattr(self._magnitude, item)
|
|
if callable(attr):
|
|
return functools.partial(self.__numpy_method_wrap, attr)
|
|
return attr
|
|
try:
|
|
return getattr(self._magnitude, item)
|
|
except AttributeError as ex:
|
|
raise AttributeError("Neither Quantity object nor its magnitude ({0})"
|
|
"has attribute '{1}'".format(self._magnitude, item))
|
|
|
|
def __getitem__(self, key):
|
|
try:
|
|
value = self._magnitude[key]
|
|
return self.__class__(value, self._units)
|
|
except TypeError:
|
|
raise TypeError("Neither Quantity object nor its magnitude ({0})"
|
|
"supports indexing".format(self._magnitude))
|
|
|
|
def __setitem__(self, key, value):
|
|
try:
|
|
if math.isnan(value):
|
|
self._magnitude[key] = value
|
|
return
|
|
except (TypeError, DimensionalityError):
|
|
pass
|
|
|
|
try:
|
|
if isinstance(value, self.__class__):
|
|
factor = self.__class__(value.magnitude, value.units / self.units).to_base_units()
|
|
else:
|
|
factor = self.__class__(value, self._units ** (-1)).to_base_units()
|
|
|
|
if isinstance(factor, self.__class__):
|
|
if not factor.dimensionless:
|
|
raise DimensionalityError(value, self.units,
|
|
extra_msg='. Assign a quantity with the same dimensionality or '
|
|
'access the magnitude directly as '
|
|
'`obj.magnitude[%s] = %s`' % (key, value))
|
|
self._magnitude[key] = factor.magnitude
|
|
else:
|
|
self._magnitude[key] = factor
|
|
|
|
except TypeError:
|
|
raise TypeError("Neither Quantity object nor its magnitude ({0})"
|
|
"supports indexing".format(self._magnitude))
|
|
|
|
def tolist(self):
|
|
units = self._units
|
|
return [self.__class__(value, units).tolist() if isinstance(value, list) else self.__class__(value, units)
|
|
for value in self._magnitude.tolist()]
|
|
|
|
__array_priority__ = 17
|
|
|
|
def __array_prepare__(self, obj, context=None):
|
|
# If this uf is handled by Pint, write it down in the handling dictionary.
|
|
|
|
# name of the ufunc, argument of the ufunc, domain of the ufunc
|
|
# In ufuncs with multiple outputs, domain indicates which output
|
|
# is currently being prepared (eg. see modf).
|
|
# In ufuncs with a single output, domain is 0
|
|
uf, objs, huh = context
|
|
|
|
if uf.__name__ in self.__handled and huh == 0:
|
|
# Only one ufunc should be handled at a time.
|
|
# If a ufunc is already being handled (and this is not another domain),
|
|
# something is wrong..
|
|
if self.__handling:
|
|
raise Exception('Cannot handled nested ufuncs.\n'
|
|
'Current: {0}\n'
|
|
'New: {1}'.format(context, self.__handling))
|
|
self.__handling = context
|
|
|
|
return obj
|
|
|
|
def __array_wrap__(self, obj, context=None):
|
|
uf, objs, huh = context
|
|
|
|
# if this ufunc is not handled by Pint, pass it to the magnitude.
|
|
if uf.__name__ not in self.__handled:
|
|
return self.magnitude.__array_wrap__(obj, context)
|
|
|
|
try:
|
|
ufname = uf.__name__ if huh == 0 else '{0}__{1}'.format(uf.__name__, huh)
|
|
|
|
# First, we check the units of the input arguments.
|
|
|
|
if huh == 0:
|
|
# Do this only when the wrap is called for the first ouput.
|
|
|
|
# Store the destination units
|
|
dst_units = None
|
|
# List of magnitudes of Quantities with the right units
|
|
# to be used as argument of the ufunc
|
|
mobjs = None
|
|
|
|
if uf.__name__ in self.__require_units:
|
|
# ufuncs in __require_units
|
|
# require specific units
|
|
# This is more complex that it should be due to automatic
|
|
# conversion between radians/dimensionless
|
|
# TODO: maybe could be simplified using Contexts
|
|
dst_units = self.__require_units[uf.__name__]
|
|
if dst_units == 'radian':
|
|
mobjs = []
|
|
for other in objs:
|
|
unt = getattr(other, 'units', '')
|
|
if unt == 'radian':
|
|
mobjs.append(getattr(other, 'magnitude', other))
|
|
else:
|
|
factor, units = self._REGISTRY.get_base_units(unt)
|
|
if units and units != UnitsContainer({'radian': 1}):
|
|
raise DimensionalityError(units, dst_units)
|
|
mobjs.append(getattr(other, 'magnitude', other) * factor)
|
|
mobjs = tuple(mobjs)
|
|
else:
|
|
dst_units = self._REGISTRY.parse_expression(dst_units).units
|
|
|
|
elif len(objs) > 1 and uf.__name__ not in self.__skip_other_args:
|
|
# ufunc with multiple arguments require that all inputs have
|
|
# the same arguments unless they are in __skip_other_args
|
|
dst_units = objs[0].units
|
|
|
|
# Do the conversion (if needed) and extract the magnitude for each input.
|
|
if mobjs is None:
|
|
if dst_units is not None:
|
|
mobjs = tuple(self._REGISTRY.convert(getattr(other, 'magnitude', other),
|
|
getattr(other, 'units', ''),
|
|
dst_units)
|
|
for other in objs)
|
|
else:
|
|
mobjs = tuple(getattr(other, 'magnitude', other)
|
|
for other in objs)
|
|
|
|
# call the ufunc
|
|
out = uf(*mobjs)
|
|
|
|
# If there are multiple outputs,
|
|
# store them in __handling (uf, objs, huh, out0, out1, ...)
|
|
# and return the first
|
|
if uf.nout > 1:
|
|
self.__handling += out
|
|
out = out[0]
|
|
else:
|
|
# If this is not the first output,
|
|
# just grab the result that was previously calculated.
|
|
out = self.__handling[3 + huh]
|
|
|
|
|
|
# Second, we set the units of the output value.
|
|
if ufname in self.__set_units:
|
|
try:
|
|
out = self.__class__(out, self.__set_units[ufname])
|
|
except:
|
|
raise _Exception(ValueError)
|
|
elif ufname in self.__copy_units:
|
|
try:
|
|
out = self.__class__(out, self.units)
|
|
except:
|
|
raise _Exception(ValueError)
|
|
elif ufname in self.__prod_units:
|
|
tmp = self.__prod_units[ufname]
|
|
if tmp == 'size':
|
|
out = self.__class__(out, self.units ** self._magnitude.size)
|
|
elif tmp == 'div':
|
|
units1 = objs[0].units if isinstance(objs[0], self.__class__) else UnitsContainer()
|
|
units2 = objs[1].units if isinstance(objs[1], self.__class__) else UnitsContainer()
|
|
out = self.__class__(out, units1 / units2)
|
|
elif tmp == 'mul':
|
|
units1 = objs[0].units if isinstance(objs[0], self.__class__) else UnitsContainer()
|
|
units2 = objs[1].units if isinstance(objs[1], self.__class__) else UnitsContainer()
|
|
out = self.__class__(out, units1 * units2)
|
|
else:
|
|
out = self.__class__(out, self.units ** tmp)
|
|
|
|
return out
|
|
except (DimensionalityError, UndefinedUnitError) as ex:
|
|
raise ex
|
|
except _Exception as ex:
|
|
raise ex.internal
|
|
except Exception as ex:
|
|
print(ex)
|
|
finally:
|
|
# If this is the last output argument for the ufunc,
|
|
# we are done handling this ufunc.
|
|
if uf.nout == huh + 1:
|
|
self.__handling = None
|
|
|
|
return self.magnitude.__array_wrap__(obj, context)
|
|
|
|
# Measurement support
|
|
def plus_minus(self, error, relative=False):
|
|
if isinstance(error, self.__class__):
|
|
if relative:
|
|
raise ValueError('{} is not a valid relative error.'.format(error))
|
|
error = error.to(self.units).magnitude
|
|
else:
|
|
if relative:
|
|
error = error * abs(self.magnitude)
|
|
|
|
return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self.units)
|
|
|
|
# methods/properties that help for math operations with offset units
|
|
@property
|
|
def _is_multiplicative(self):
|
|
"""Check if the Quantity object has only multiplicative units.
|
|
"""
|
|
# XXX Turn this into a method/property of _Quantity?
|
|
return not self._get_non_multiplicative_units()
|
|
|
|
def _get_non_multiplicative_units(self):
|
|
"""Return a list of the of non-multiplicative units of the Quantity object
|
|
"""
|
|
offset_units = [unit for unit in self.units.keys()
|
|
if not self._REGISTRY._units[unit].is_multiplicative]
|
|
return offset_units
|
|
|
|
def _get_delta_units(self):
|
|
"""Return list of delta units ot the Quantity object
|
|
"""
|
|
delta_units = [u for u in self.units.keys() if u.startswith("delta_")]
|
|
return delta_units
|
|
|
|
def _has_compatible_delta(self, unit):
|
|
""""Check if Quantity object has a delta_unit that is compatible with unit
|
|
"""
|
|
deltas = self._get_delta_units()
|
|
if 'delta_' + unit in deltas:
|
|
return True
|
|
else: # Look for delta units with same dimension as the offset unit
|
|
offset_unit_dim = self._REGISTRY._units[unit].reference
|
|
for d in deltas:
|
|
if self._REGISTRY._units[d].reference == offset_unit_dim:
|
|
return True
|
|
return False
|
|
|
|
def _ok_for_muldiv(self, no_offset_units=None):
|
|
"""Checks if Quantity object can be multiplied or divided
|
|
|
|
:q: quantity object that is checked
|
|
:no_offset_units: number of offset units in q
|
|
"""
|
|
is_ok = True
|
|
if no_offset_units is None:
|
|
no_offset_units = len(self._get_non_multiplicative_units())
|
|
if no_offset_units > 1:
|
|
is_ok = False
|
|
if no_offset_units == 1:
|
|
if len(self._units) > 1:
|
|
is_ok = False
|
|
if (len(self._units) == 1
|
|
and not self._REGISTRY.autoconvert_offset_to_baseunit):
|
|
is_ok = False
|
|
if next(iter(self._units.values())) != 1:
|
|
is_ok = False
|
|
return is_ok
|