# coding: utf-8
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This module defines the `Quantity` object, which represents a number with some
associated units. `Quantity` objects support operations like ordinary numbers,
but will deal with unit conversions internally.
"""
from __future__ import (absolute_import, unicode_literals, division,
print_function)
# Standard library
import copy
import numbers
import warnings
import numpy as np
# AstroPy
from .core import Unit, UnitBase, CompositeUnit
from ..config import ConfigurationItem
WARN_IMPLICIT_NUMERIC_CONVERSION = ConfigurationItem(
"warn_implicit_numeric_conversion",
True,
"Whether to show a warning message "
"in the log when converting a "
"Quantity to a float/int")
__all__ = ["Quantity"]
def _is_unity(value):
x = value.decompose()
return (len(x.bases) == 0 and x.scale == 1.0)
def _validate_value(value):
""" Make sure that the input is a Python or Numpy numeric type.
Parameters
----------
value : number
An object that will be checked whether it is a numeric type or not.
Returns
-------
newval
The new value either as an array or a scalar
"""
from ..utils.misc import isiterable
if isinstance(value, (numbers.Number, np.number)):
value_obj = value
elif isiterable(value):
value_obj = np.array(value, copy=True)
elif isinstance(value, np.ndarray):
# A length-0 numpy array (i.e. numpy scalar) which we accept as-is
value_obj = np.array(value, copy=True)
else:
raise TypeError("The value must be a valid Python or Numpy numeric "
"type.")
return value_obj
[docs]class Quantity(object):
""" A `Quantity` represents a number with some associated unit.
Parameters
----------
value : number, `Quantity` object, or sequence of `Quantity` objects.
The numerical value of this quantity in the units given by unit.
If a `Quantity` or sequence of them, creates a new `Quantity`
object, converting to `unit` units as needed.
unit : `~astropy.units.UnitBase` instance, str
An object that represents the unit associated with the input value.
Must be an `~astropy.units.UnitBase` object or a string parseable by
the `units` package.
Raises
------
TypeError
If the value provided is not a Python numeric type.
TypeError
If the unit provided is not either a `Unit` object or a parseable
string unit.
"""
def __init__(self, value, unit):
from ..utils.misc import isiterable
self._unit = Unit(unit)
if isinstance(value, Quantity):
self._value = _validate_value(value.to(self._unit).value)
elif isiterable(value) and all([isinstance(v, Quantity) for v in value]):
self._value = _validate_value([q.to(self._unit).value for q in value])
else:
self._value = _validate_value(value)
[docs] def to(self, unit, equivalencies=[]):
""" Returns a new `Quantity` object with the specified units.
Parameters
----------
unit : `~astropy.units.UnitBase` instance, str
An object that represents the unit to convert to. Must be
an `~astropy.units.UnitBase` object or a string parseable
by the `units` package.
equivalencies : list of equivalence pairs, optional
A list of equivalence pairs to try if the units are not
directly convertible. See :ref:`unit_equivalencies`.
"""
new_val = self.unit.to(unit, self.value, equivalencies=equivalencies)
new_unit = Unit(unit)
return Quantity(new_val, new_unit)
@property
[docs] def value(self):
""" The numerical value of this quantity. """
return self._value
@property
[docs] def unit(self):
"""
A `~astropy.units.UnitBase` object representing the unit of this
quantity.
"""
return self._unit
@property
[docs] def si(self):
"""
Returns a copy of the current `Quantity` instance with SI units. The
value of the resulting object will be scaled.
"""
from . import si
si_unit = self.unit.to_system(si)[0]
return Quantity(self.value * si_unit.scale, si_unit / si_unit.scale)
@property
[docs] def cgs(self):
"""
Returns a copy of the current `Quantity` instance with CGS units. The
value of the resulting object will be scaled.
"""
from . import cgs
cgs_unit = self.unit.to_system(cgs)[0]
return Quantity(self.value * cgs_unit.scale, cgs_unit / cgs_unit.scale)
@property
[docs] def isscalar(self):
"""
True if the `value` of this quantity is a scalar, or False if it
is an array-like object.
.. note::
This is subtly different from `numpy.isscalar` in that
`numpy.isscalar` returns False for a zero-dimensional array
(e.g. ``np.array(1)``), while this is True in that case.
"""
from ..utils.misc import isiterable
return not isiterable(self.value)
[docs] def copy(self):
""" Return a copy of this `Quantity` instance """
return self.__class__(self.value, unit=self.unit)
# Arithmetic operations
def __add__(self, other):
""" Addition between `Quantity` objects and other objects. If
they are both `Quantity` objects, results in the units of the
**left** object if they are compatible, otherwise this fails.
"""
if isinstance(other, Quantity):
return Quantity(self.value + other.to(self.unit).value,
unit=self.unit)
else:
raise TypeError(
"Object of type '{0}' cannot be added with a Quantity "
"object. Addition is only supported between Quantity "
"objects with compatible units.".format(other.__class__))
def __sub__(self, other):
""" Subtraction between `Quantity` objects and other objects.
If they are both `Quantity` objects, results in the units of the
**left** object if they are compatible, otherwise this fails.
"""
if isinstance(other, Quantity):
return Quantity(self.value - other.to(self.unit).value,
unit=self.unit)
else:
raise TypeError(
"Object of type '{0}' cannot be subtracted from a Quantity "
"object. Subtraction is only supported between Quantity "
"objects with compatible units.".format(other.__class__))
def __mul__(self, other):
""" Multiplication between `Quantity` objects and other objects."""
if isinstance(other, Quantity):
return Quantity(self.value * other.value,
unit=self.unit * other.unit)
elif isinstance(other, basestring):
return Quantity(self.value, unit=Unit(other) * self.unit)
elif isinstance(other, UnitBase):
return Quantity(self.value, unit=other * self.unit)
else:
try:
return Quantity(other * self.value, unit=self.unit)
except TypeError:
raise TypeError(
"Object of type '{0}' cannot be multiplied with a "
"Quantity object.".format(other.__class__))
def __rmul__(self, other):
""" Right Multiplication between `Quantity` objects and other
objects.
"""
return self.__mul__(other)
def __div__(self, other):
""" Division between `Quantity` objects and other objects."""
if isinstance(other, Quantity):
return Quantity(self.value / other.value,
unit=self.unit / other.unit)
elif isinstance(other, basestring):
return Quantity(self.value, unit=self.unit / Unit(other))
elif isinstance(other, UnitBase):
return Quantity(self.value, unit=self.unit / other)
else:
try:
return Quantity(self.value / other, unit=self.unit)
except TypeError:
raise TypeError(
"Object of type '{0}' cannot be diveded with a Quantity "
"object.".format(other.__class__))
def __rdiv__(self, other):
""" Right Division between `Quantity` objects and other objects."""
if isinstance(other, Quantity):
return Quantity(other.value / self.value,
unit=other.unit / self.unit)
elif isinstance(other, basestring):
return Quantity(self.value, unit=Unit(other) / self.unit)
elif isinstance(other, UnitBase):
return Quantity(1. / self.value, unit=other / self.unit)
else:
try:
return Quantity(other / self.value, unit=1. / self.unit)
except TypeError:
raise TypeError(
"Object of type '{0}' cannot be diveded with a Quantity "
"object.".format(other.__class__))
def __truediv__(self, other):
""" Division between `Quantity` objects. """
return self.__div__(other)
def __rtruediv__(self, other):
""" Division between `Quantity` objects. """
return self.__rdiv__(other)
def __pow__(self, p):
""" Raise `Quantity` object to a power. """
if hasattr(p, 'unit'):
raise TypeError(
'Cannot raise a Quantity object to a power of something '
'with a unit')
return Quantity(self.value ** p, unit=self.unit ** p)
def __neg__(self):
"""
Minus the quantity. This is useful for doing -q where q is a quantity.
"""
return Quantity(-self.value, unit=self.unit)
def __pos__(self):
"""
Plus the quantity. This is implemented in case users use +q where q is
a quantity.
"""
return Quantity(self.value, unit=self.unit)
def __abs__(self):
"""
Absolute value of the quantity.
"""
return Quantity(abs(self.value), unit=self.unit)
# Comparison operations
def __eq__(self, other):
if hasattr(other, 'value') and hasattr(other, 'to'):
return self.value == other.to(self.unit).value
else:
return False
def __ne__(self, other):
if hasattr(other, 'value') and hasattr(other, 'to'):
return self.value != other.to(self.unit).value
else:
return True
def __lt__(self, other):
if isinstance(other, Quantity):
return self.value < other.to(self.unit).value
else:
raise TypeError("Quantity object cannot be compared to an object "
"of type {0}".format(other.__class__))
def __le__(self, other):
if isinstance(other, Quantity):
return self.value <= other.to(self.unit).value
else:
raise TypeError("Quantity object cannot be compared to an object "
"of type {0}".format(other.__class__))
def __gt__(self, other):
if isinstance(other, Quantity):
return self.value > other.to(self.unit).value
else:
raise TypeError("Quantity object cannot be compared to an object "
"of type {0}".format(other.__class__))
def __ge__(self, other):
if isinstance(other, Quantity):
return self.value >= other.to(self.unit).value
else:
raise TypeError("Quantity object cannot be compared to an object "
"of type {0}".format(other.__class__))
#other overrides of special functions
def __hash__(self):
return hash(self.value) ^ hash(self.unit)
def __iter__(self):
if self.isscalar:
raise TypeError(
"'{cls}' object with a scalar value is not iterable"
.format(cls=self.__class__.__name__))
# Otherwise return a generator
def quantity_iter():
for val in self.value:
yield Quantity(val, unit=self.unit)
return quantity_iter()
def __getitem__(self, key):
if self.isscalar:
raise TypeError(
"'{cls}' object with a scalar value does not support "
"indexing".format(cls=self.__class__.__name__))
else:
return Quantity(self.value[key], unit=self.unit)
def __nonzero__(self):
"""Quantities should always be treated as non-False; there is too much
potential for ambiguity otherwise.
"""
return True
def __len__(self):
if self.isscalar:
raise TypeError("'{cls}' object with a scalar value has no "
"len()".format(cls=self.__class__.__name__))
else:
return len(self.value)
# Numerical types
def __float__(self):
if not self.isscalar:
raise TypeError('Only scalar quantities can be converted to '
'Python scalars')
# We show a warning unless the unit is equivalent to unity (i.e. not
# just dimensionless, but also with a scale of 1)
if not _is_unity(self.unit) and WARN_IMPLICIT_NUMERIC_CONVERSION():
warnings.warn("Converting Quantity object in units '{0}' to a "
"Python scalar".format(self.unit))
return float(self.value)
def __int__(self):
if not self.isscalar:
raise TypeError('Only scalar quantities can be converted to '
'Python scalars')
# We show a warning unless the unit is equivalent to unity (i.e. not
# just dimensionless, but also with a scale of 1)
if not _is_unity(self.unit) and WARN_IMPLICIT_NUMERIC_CONVERSION():
warnings.warn("Converting Quantity object in units '{0}' to a "
"Python scalar".format(self.unit))
return int(self.value)
def __long__(self):
if not self.isscalar:
raise TypeError('Only scalar quantities can be converted to '
'Python scalars')
# We show a warning unless the unit is equivalent to unity (i.e. not
# just dimensionless, but also with a scale of 1)
if not _is_unity(self.unit) and WARN_IMPLICIT_NUMERIC_CONVERSION():
warnings.warn("Converting Quantity object in units '{0}' to a "
"Python scalar".format(self.unit))
return long(self.value)
# Array types
def __array__(self):
# We show a warning unless the unit is equivalent to unity (i.e. not
# just dimensionless, but also with a scale of 1)
if not _is_unity(self.unit) and WARN_IMPLICIT_NUMERIC_CONVERSION():
warnings.warn("Converting Quantity object in units '{0}' to a "
"Numpy array".format(self.unit))
return np.array(self.value)
# Display
# TODO: we may want to add a hook for dimensionless quantities?
def __str__(self):
return "{0} {1:s}".format(self.value, self.unit.to_string())
def __repr__(self):
return "<Quantity {0} {1:s}>".format(self.value, self.unit.to_string())
def _repr_latex_(self):
"""
Generate latex representation of unit name. This is used by
the IPython notebook to show it all latexified.
Returns
-------
lstr
LaTeX string
"""
# Format value
latex_value = "{0:g}".format(self.value)
if "e" in latex_value:
latex_value = latex_value.replace('e', '\\times 10^{') + '}'
# Format unit
# [1:-1] strips the '$' on either side needed for math mode
latex_unit = self.unit._repr_latex_()[1:-1] # note this is unicode
return u'${0} \; {1}$'.format(latex_value, latex_unit)
[docs] def decompose(self, bases=[]):
"""
Generates a new `Quantity` with the units
decomposed. Decomposed units have only irreducible units in
them (see `astropy.units.UnitBase.decompose`).
Parameters
----------
bases : sequence of UnitBase, optional
The bases to decompose into. When not provided,
decomposes down to any irreducible units. When provided,
the decomposed result will only contain the given units.
This will raises a `UnitsException` if it's not possible
to do so.
Returns
-------
newq : `~astropy.units.quantity.Quantity`
A new object equal to this quantity with units decomposed.
"""
return self._decompose(False, bases=bases)
def _decompose(self, allowscaledunits=False, bases=[]):
"""
Generates a new `Quantity` with the units decomposed. Decomposed
units have only irreducible units in them (see
`astropy.units.UnitBase.decompose`).
Parameters
----------
allowscaledunits : bool
If True, the resulting `Quantity` may have a scale factor
associated with it. If False, any scaling in the unit will
be subsumed into the value of the resulting `Quantity`
bases : sequence of UnitBase, optional
The bases to decompose into. When not provided,
decomposes down to any irreducible units. When provided,
the decomposed result will only contain the given units.
This will raises a `UnitsException` if it's not possible
to do so.
Returns
-------
newq : `~astropy.units.quantity.Quantity`
A new object equal to this quantity with units decomposed.
"""
newu = self.unit.decompose(bases=bases)
newval = self.value
if not allowscaledunits and hasattr(newu, 'scale'):
newval *= newu.scale
newu = newu / Unit(newu.scale)
return Quantity(newval, newu)