# 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 numpy as np
# AstroPy
from .core import Unit, UnitBase, IrreducibleUnit, CompositeUnit
__all__ = ["Quantity"]
def _validate_value(value):
""" Make sure that the input is a Python 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):
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 numeric type.")
return value_obj
[docs]class Quantity(object):
""" A `Quantity` represents a number with some associated unit.
Parameters
----------
value : number
The numerical value of this quantity in the units given by unit.
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):
self._value = _validate_value(value)
self._unit = Unit(unit)
[docs] def to(self, unit):
""" 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.
"""
new_val = self.unit.to(unit, self.value)
new_unit = Unit(unit)
return Quantity(new_val, new_unit)
@property
def value(self):
""" The numerical value of this quantity. """
return self._value
@value.setter
[docs] def value(self, obj):
""" Setter for the value attribute. We allow the user to change the value by setting this attribute,
so this will validate the new object.
Parameters
----------
obj : number
The numerical value of this quantity in the same units as stored internally.
Raises
------
TypeError
If the value provided is not a Python numeric type.
"""
self._value = _validate_value(obj)
@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 as _si
si_unit_set = set([ss for ss in _si.__dict__.values() if isinstance(ss, IrreducibleUnit)])
si_quantity_value = self.value
if isinstance(self.unit, CompositeUnit):
si_quantity_bases = []
si_quantity_powers = []
for base_unit, power in zip(self.unit.bases, self.unit.powers):
is_si = True
for si_unit in si_unit_set:
if base_unit.is_equivalent(si_unit) and base_unit != si_unit:
scale = (base_unit / si_unit).dimensionless_constant() ** power
si_quantity_value *= scale
is_si = False
si_quantity_bases.append(si_unit)
si_quantity_powers.append(power)
break
if is_si:
si_quantity_bases.append(base_unit)
si_quantity_powers.append(power)
return Quantity(si_quantity_value, CompositeUnit(1., si_quantity_bases, si_quantity_powers).simplify())
else:
for si_unit in si_unit_set:
if self.unit.is_equivalent(si_unit) and self.unit != si_unit:
# Don't have to worry about power here because if it has a power, it's a CompositeUnit
scale = (self.unit / si_unit).dimensionless_constant()
si_quantity_value *= scale
return Quantity(si_quantity_value, si_unit)
return self.copy()
@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 as _cgs
si_quantity = self.si
cgs_quantity_value = si_quantity.value
if isinstance(si_quantity.unit, CompositeUnit):
cgs_quantity_bases = []
cgs_quantity_powers = []
for base_unit, power in zip(si_quantity.unit.bases, si_quantity.unit.powers):
if base_unit in _cgs._cgs_bases.keys():
scale = (base_unit / _cgs._cgs_bases[base_unit]).dimensionless_constant() ** power
cgs_quantity_value *= scale
cgs_quantity_bases.append(_cgs._cgs_bases[base_unit])
cgs_quantity_powers.append(power)
else:
cgs_quantity_bases.append(base_unit)
cgs_quantity_powers.append(power)
return Quantity(cgs_quantity_value, CompositeUnit(1., cgs_quantity_bases, cgs_quantity_powers).simplify())
else:
if si_quantity.unit in _cgs._cgs_bases.keys():
# Don't have to worry about power here because if it has a power, it's a CompositeUnit
scale = (si_quantity.unit / _cgs._cgs_bases[si_quantity.unit]).dimensionless_constant()
cgs_quantity_value *= scale
return Quantity(cgs_quantity_value, _cgs._cgs_bases[si_quantity.unit])
else:
return Quantity(si_quantity.value, si_quantity.unit)
@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 Quantity(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 added with a "
"Quantity object. Addition 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, 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, UnitBase):
return Quantity(self.value, unit=self.unit / other)
elif isinstance(other, numbers.Number) and hasattr(self.unit, "bases"):
new_unit_bases = copy.copy(self.unit.bases)
new_unit_powers = [-p for p in self.unit.powers]
return Quantity(other / self.value, unit=CompositeUnit(1., new_unit_bases, new_unit_powers))
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, 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)
# 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 __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 __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')
return float(self.value)
def __int__(self):
if not self.isscalar:
raise TypeError('Only scalar quantities can be converted to Python scalars')
return int(self.value)
def __long__(self):
if not self.isscalar:
raise TypeError('Only scalar quantities can be converted to Python scalars')
return long(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)
@property
[docs] def decomposed_unit(self):
"""
Generates a new `Quantity` with the units decomposed. Decomposed
units have only irreducible units in them (see
`astropy.units.UnitBase.decompose`).
Returns
-------
newq : `~astropy.units.quantity.Quantity`
A new object equal to this quantity with units decomposed.
"""
return self._decomposed_unit(False)
def _decomposed_unit(self, allowscaledunits=False):
"""
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`
Returns
-------
newq : `~astropy.units.quantity.Quantity`
A new object equal to this quantity with units decomposed.
"""
newu = self.unit.decompose()
newval = self.value
if not allowscaledunits and hasattr(newu, 'scale'):
newval *= newu.scale
newu = newu / Unit(newu.scale)
return Quantity(newval, newu)