Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow object arrays of mixed unit Quantities to be printed #3778

Merged
merged 5 commits into from May 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Expand Up @@ -254,6 +254,12 @@ New Features

- ``astropy.units``

- Fixed printing of object ndarrays containing multiple Quantity
objects with differing / incompatible units. Note: Unit conversion errors
now cause a ``UnitConversionError`` exception to be raised. However, this
is a subclass of the ``UnitsError`` exception used previously, so existing
code that catches ``UnitsError`` should still work. [#3778]

- ``astropy.utils``

- ``astropy.vo``
Expand Down
8 changes: 4 additions & 4 deletions astropy/nddata/compat.py
Expand Up @@ -6,7 +6,7 @@

import numpy as np

from ..units import UnitsError, Unit
from ..units import UnitsError, UnitConversionError, Unit
from .. import log

from .nddata import NDData
Expand Down Expand Up @@ -115,9 +115,9 @@ def uncertainty(self, value):
try:
scaling = (1 * value._unit).to(self.unit)
except UnitsError:
raise UnitsError('Cannot convert unit of uncertainty '
'to unit of '
'{0} object.'.format(class_name))
raise UnitConversionError(
'Cannot convert unit of uncertainty to unit of '
'{0} object.'.format(class_name))
value.array *= scaling
elif not self.unit and value._unit:
# Raise an error if uncertainty has unit and data does not
Expand Down
3 changes: 3 additions & 0 deletions astropy/time/core.py
Expand Up @@ -20,6 +20,7 @@

from .. import units as u
from .. import _erfa as erfa
from ..units import UnitConversionError
from ..utils.compat.odict import OrderedDict
from ..utils.compat.misc import override__dir__
from ..extern import six
Expand Down Expand Up @@ -282,6 +283,8 @@ def _get_time_fmt(self, val, val2, format, scale):
try:
return FormatClass(val, val2, scale, self.precision,
self.in_subfmt, self.out_subfmt)
except UnitConversionError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the addition here is necessary. Really, one is just testing whether a format works at all, and a blanket except would arguably have been fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could change that, but I don't think I'm comfortable with a bare except here, at least for now. I agree it's a bit awkward though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine just to remove the additional exception. I don't see how UnitConversionError could indicate anything else than "this format doesn't work, go on try the next".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, let me clarify, the reason this needs to be caught and handled separately is that there were some tests where a UnitsError was expected to be raised. Now a UnitConversionError is raised, but since that's a subclass of ValueError it needs to be handled separately.

I can't remember exactly what the use case was but there is code expecting this to raise unit conversion errors so that they can be handled separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, yes, I think I now understand: that much be the interaction with Quantity in TimeDelta

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked, and there are a few tests that fail without this line. For example TestTimeQuantity.test_invalid_quantity_input fails, because it tests instantiating a Time object from a quantity in units of meters. This should definitely result in a unit conversion error, instead of just being ignored (instead you end up getting an error "Input values did not match the format class jd", which is true in a sense, but it's not as useful as a unit error would be here.

raise
except (ValueError, TypeError):
pass
else:
Expand Down
17 changes: 12 additions & 5 deletions astropy/units/core.py
Expand Up @@ -27,8 +27,8 @@
# TODO: Support functional units, e.g. log(x), ln(x)

__all__ = [
'UnitsError', 'UnitsWarning', 'UnitBase', 'NamedUnit',
'IrreducibleUnit', 'Unit', 'def_unit', 'CompositeUnit',
'UnitsError', 'UnitsWarning', 'UnitConversionError', 'UnitBase',
'NamedUnit', 'IrreducibleUnit', 'Unit', 'def_unit', 'CompositeUnit',
'PrefixUnit', 'UnrecognizedUnit', 'get_current_unit_registry',
'set_enabled_units', 'add_enabled_units',
'set_enabled_equivalencies', 'add_enabled_equivalencies',
Expand Down Expand Up @@ -451,6 +451,13 @@ class UnitScaleError(UnitsError, ValueError):
pass


class UnitConversionError(UnitsError, ValueError):
"""
Used specifically for errors related to converting between units or
interpreting units in terms of other units.
"""


# Maintain error in old location for backward compatibility
from .format import fits as _fits
_fits.UnitScaleError = UnitScaleError
Expand Down Expand Up @@ -837,7 +844,7 @@ def get_err_str(unit):
unit_str = get_err_str(orig_unit)
other_str = get_err_str(orig_other)

raise UnitsError(
raise UnitConversionError(
"{0} and {1} are not convertible".format(
unit_str, other_str))

Expand Down Expand Up @@ -910,7 +917,7 @@ def _to(self, other):
in zip(self_decomposed.bases, other_decomposed.bases))):
return self_decomposed.scale / other_decomposed.scale

raise UnitsError(
raise UnitConversionError(
"'{0!r}' is not a scaled version of '{1!r}'".format(self, other))

def to(self, other, value=1.0, equivalencies=[]):
Expand Down Expand Up @@ -1636,7 +1643,7 @@ def decompose(self, bases=set()):
return CompositeUnit(scale, [base], [1],
_error_check=False)

raise UnitsError(
raise UnitConversionError(
"Unit {0} can not be decomposed into the requested "
"bases".format(self))

Expand Down
4 changes: 2 additions & 2 deletions astropy/units/quantity_helper.py
Expand Up @@ -2,7 +2,7 @@
# quantities (http://pythonhosted.org/quantities/) package.

import numpy as np
from .core import (UnitsError, dimensionless_unscaled,
from .core import (UnitsError, UnitConversionError, dimensionless_unscaled,
get_current_unit_registry)
from ..utils.compat.fractions import Fraction

Expand Down Expand Up @@ -279,7 +279,7 @@ def get_converters_and_unit(f, *units):
converters[changeable] = get_converter(units[changeable],
units[fixed])
except UnitsError:
raise UnitsError(
raise UnitConversionError(
"Can only apply '{0}' function to quantities "
"with compatible dimensions"
.format(f.__name__))
Expand Down
20 changes: 20 additions & 0 deletions astropy/units/tests/test_quantity.py
Expand Up @@ -1155,3 +1155,23 @@ def test_insert():
q2 = q.insert(1, 10 * u.m, axis=1)
assert np.all(q2.value == [[ 1, 10, 2],
[ 3, 10, 4]])


def test_repr_array_of_quantity():
"""
Test print/repr of object arrays of Quantity objects with different
units.

Regression test for the issue first reported in
https://github.com/astropy/astropy/issues/3777
"""

a = np.array([1 * u.m, 2 * u.s], dtype=object)
if NUMPY_LT_1_7:
# Numpy 1.6.x has some different defaults for how to display object
# arrays (it uses the str() of the objects instead of the repr()
assert repr(a) == 'array([1.0 m, 2.0 s], dtype=object)'
assert str(a) == '[1.0 m 2.0 s]'
else:
assert repr(a) == 'array([<Quantity 1.0 m>, <Quantity 2.0 s>], dtype=object)'
assert str(a) == '[<Quantity 1.0 m> <Quantity 2.0 s>]'
2 changes: 1 addition & 1 deletion docs/units/conversion.rst
Expand Up @@ -34,7 +34,7 @@ If you attempt to convert to a incompatible unit, an exception will result:
>>> cms.to(u.km)
Traceback (most recent call last):
...
UnitsError: 'cm / s' (speed) and 'km' (length) are not convertible
UnitConversionError: 'cm / s' (speed) and 'km' (length) are not convertible

You can check whether a particular conversion is possible using the
`~astropy.units.core.UnitBase.is_equivalent` method::
Expand Down
6 changes: 3 additions & 3 deletions docs/units/equivalencies.rst
Expand Up @@ -38,7 +38,7 @@ Length and angles are not normally convertible, so
>>> (8.0 * u.arcsec).to(u.parsec)
Traceback (most recent call last):
...
UnitsError: 'arcsec' (angle) and 'pc' (length) are not convertible
UnitConversionError: 'arcsec' (angle) and 'pc' (length) are not convertible

However, when passing the result of
:func:`~astropy.units.equivalencies.parallax` as the third argument to the
Expand Down Expand Up @@ -68,11 +68,11 @@ dimensionless). For instance, normally the following raise exceptions::
>>> u.degree.to('')
Traceback (most recent call last):
...
UnitsError: 'deg' (angle) and '' (dimensionless) are not convertible
UnitConversionError: 'deg' (angle) and '' (dimensionless) are not convertible
>>> (u.kg * u.m**2 * (u.cycle / u.s)**2).to(u.J)
Traceback (most recent call last):
...
UnitsError: 'cycle2 kg m2 / s2' and 'J' (energy) are not convertible
UnitConversionError: 'cycle2 kg m2 / s2' and 'J' (energy) are not convertible

But when passing we pass the proper conversion function,
:func:`~astropy.units.equivalencies.dimensionless_angles`, it works.
Expand Down
2 changes: 1 addition & 1 deletion docs/units/index.rst
Expand Up @@ -104,7 +104,7 @@ conversion from wavelength to frequency doesn't normally work:
>>> (1000 * u.nm).to(u.Hz)
Traceback (most recent call last):
...
UnitsError: 'nm' (length) and 'Hz' (frequency) are not convertible
UnitConversionError: 'nm' (length) and 'Hz' (frequency) are not convertible

but by passing an equivalency list, in this case ``spectral()``, it does:

Expand Down