Source code for siquant.quantities

try:
    from operator import matmul
except ImportError:
    pass  # matmul not supported

from copy import copy, deepcopy
from functools import total_ordering

from .exceptions import UnitMismatchError, unexpected_type_error
from .util import immutable


[docs]def make(quantity, units): """Entry point for creating quantities consistently :param quantity: The value to tag with specific units. :type quantity: ``_T`` :param units: The units of this quantity. :type units: :class:`~siquant.units.SIUnit` :rtype: ``_Q`` = :class:`~Quantity` """ return units.factory(quantity, units)
[docs]def converter(units): """Create a converter function which will return Quantities. :param units: The units to convert a quantity to. :type units: :class:`~siquant.units.SIUnit` :rtype: ``Callable[[Any], _Q]`` """ def _converter(value): if isinstance(value, Quantity): return value.cvt_to(units) return make(value, units) return _converter
[docs]def validator(dimensions): """Create a validator function which checks if a value matches expected dimensions. .. seealso:: Predefined :mod:`~siquant.dimensions`. Creating new :func:`~siquant.dimensions.SIDimensions`. :param dimensions: The expected dimensions. :type dimensions: ``tuple`` :rtype: ``Callable[[Any, ...], bool]`` """ if not isinstance(dimensions, tuple): raise unexpected_type_error("dimensions", tuple, dimensions) if not len(dimensions) == 7: raise ValueError("Dimensions tuple must have 7 elements.", dimensions) def _validator(*values): return are_of(dimensions, *values) return _validator
[docs]def are_of(dimensions, *quantities): """Check if quantities all match dimensions. :param dimensions: The expected dimensionality. :type dimensions: ``tuple`` :param quantities: Variadic. The instances to check against. :rtype: ``bool`` """ try: return all(q.is_of(dimensions) for q in quantities) except AttributeError: return False
[docs]@immutable @total_ordering class Quantity: """Quantity wraps a value with units and provides arithmetic passthrough operations. .. note:: Creation of Quantity directly is discouraged. The preferred method are: q = value * si.meters q = make(value, si.meters) Both of these methods delegate instantiation to :attr:`~siquant.units.SIUnit.factory` in order to more easily support clean extensibility. :ivar quantity: The wrapped value. read only. :vartype quantity: ``_T`` :ivar units: The units of this quantity. read only. :vartype units: :class:`~siquant.units.SIUnit` :param quantity: The quantity to be wrapped. :type quantity: ``_T`` :param units: The units the quantity's value is expressed in. :type units: :class:`~siquant.units.SIUnit` """ __slots__ = ("quantity", "units") def __init__(self, quantity, units): if isinstance(quantity, Quantity): units = quantity.units * units quantity = quantity.quantity super().__setattr__("quantity", quantity) super().__setattr__("units", units)
[docs] def is_of(self, dimensions): """ :param dimensions: :return: ``bool`` """ return self.units.dimensions == dimensions
[docs] def get_as(self, units): """ Extract the underlying quantity in express units. :param units: The units to express the underlying value in. :type units: :class:`~siquant.units.SIUnit` :rtype: ``_T`` """ if self.units == units: return self.quantity if not self.units.compatible(units): raise UnitMismatchError(self.units, units) return self.units.scale / units.scale * self.quantity
[docs] def round_as(self, units, places=0): """ Extract the underlying quantity rounded to express units. :param units: The units to express the underlying value in. :param places: The number of decimal places to round to. :return: ``_T`` """ return round(self.get_as(units), places)
[docs] def cvt_to(quantity, units): """Create an equivalent Quantity expressed in the provided units. :param units: The units to express this quantity in. :type units: :class:`~siquant.units.SIUnit` :return: ``_Q`` = :class:`~siquant.quantities.Quantity` """ return make(quantity.get_as(units), units)
[docs] def round_to(self, units, places=0): """Create an equivalent Quantity rounded in provided units. :param units: The units to express the quantity in. :type units: :class:`~siquant.units.SIUnit` :param places: The number of decimal places to round to. :type places: ``int`` :rtype: ``_Q`` = :class:`~siquant.quantities.Quantity` """ return make(round(self.get_as(units), places), units)
[docs] def compatible(self, other): """ :param other: The quantity to check for dimensional compatibility. :type other: ``_Q`` = :class:`~siquant.quantities.Quantity` :rtype: bool """ if not isinstance(other, Quantity): raise unexpected_type_error("other", Quantity, other) return self.units.compatible(other.units)
def __abs_epsilon(self, atol=1e-6): if isinstance(atol, Quantity): return atol return atol * self.units def __rel_epsilon(self, other, rtol=1e-9): return rtol * max(abs(self), abs(other), 1 * self.units) def abs_approx(self, other, atol=1e-6): return self.approx(other, rtol=0, atol=atol) def rel_approx(self, other, rtol=1e-9): return self.approx(other, rtol=rtol, atol=0)
[docs] def approx(self, other, rtol=1e-9, atol=1e-6): """ :raises: ``TypeError`` if other is not a Quantity :param other: :param rtol: :type rtol: :param atol: :type atol: :return: """ if not self.compatible(other): return False epsilon = max(self.__rel_epsilon(other, rtol), self.__abs_epsilon(atol)) return abs(other - self) <= epsilon
def __add__(self, other): if other == 0: return self if isinstance(other, Quantity): units = min(self.units, other.units) return make(self.get_as(units) + other.get_as(units), units) return NotImplemented def __radd__(self, other): if other == 0: return self return NotImplemented __iadd__ = __add__ def __sub__(self, other): if other == 0: return self if isinstance(other, Quantity): units = min(self.units, other.units) return make(self.get_as(units) - other.get_as(units), units) return NotImplemented def __rsub__(self, other): if other == 0: return -self return NotImplemented __isub__ = __sub__ def __neg__(self): return make(-self.quantity, self.units) def __bool__(self): return bool(self.quantity) def __eq__(self, other): if isinstance(other, Quantity): return self.units.compatible(other.units) and self.quantity == other.get_as( self.units ) return NotImplemented def __ne__(self, other): if isinstance(other, Quantity): return not self.units.compatible( other.units ) or self.quantity != other.get_as(self.units) return NotImplemented def __hash__(self): return hash((self.quantity * self.units.scale, self.units.base_units())) def __str__(self): return "%s %s" % (self.quantity, self.units) def __repr__(self): return "%s(%r, %r)" % (self.__class__.__name__, self.quantity, self.units) def __abs__(self): return make(abs(self.quantity), self.units) def __lt__(self, other): if isinstance(other, Quantity): return self.quantity < other.get_as(self.units) return NotImplemented def __mul__(self, rhs): if isinstance(rhs, Quantity): return make(self.quantity * rhs.quantity, self.units * rhs.units) return make(self.quantity * rhs, self.units) __imul__ = __mul__ def __rmul__(self, lhs): return make(lhs * self.quantity, self.units) def __matmul__(self, rhs): if isinstance(rhs, Quantity): return make(matmul(self.quantity, rhs.quantity), self.units * rhs.units) return make(matmul(self.quantity, rhs), self.units) __imatmul__ = __matmul__ def __rmatmul__(self, lhs): return make(matmul(lhs, self.quantity), self.units) def __truediv__(self, rhs): if isinstance(rhs, Quantity): return make(self.quantity / rhs.quantity, self.units / rhs.units) return make(self.quantity / rhs, self.units) __itruediv__ = __truediv__ def __rtruediv__(self, lhs): return Quantity(lhs / self.quantity, ~self.units) def __pow__(self, exponent): try: return make(self.quantity ** exponent, self.units ** exponent) except TypeError: return NotImplemented def __invert__(self): return make(1 / self.quantity, ~self.units) def __float__(self): return float(self.quantity) def __int__(self): return int(self.quantity) def __len__(self): return len(self.quantity) def __iter__(self): return (make(q, self.units) for q in self.quantity) def __getitem__(self, idx): return make(self.quantity[idx], self.units) def __copy__(self): return make(copy(self.quantity), self.units) def __deepcopy__(self, memodict): return make(deepcopy(self.quantity), self.units)