#
# LSST Data Management System
#
# This product includes software developed by the
# LSST Project (http://www.lsst.org/).
#
# See COPYRIGHT file at the top of the source tree.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the LSST License Statement and
# the GNU General Public License along with this program. If not,
# see <https://www.lsstcorp.org/LegalNotices/>.
#
from __future__ import print_function, division
__all__ = ['Datum']
from builtins import object
from past.builtins import basestring
import numpy as np
from astropy.tests.helper import quantity_allclose
import astropy.units as u
from .jsonmixin import JsonSerializationMixin
class QuantityAttributeMixin(object):
"""Mixin with common attributes for classes that wrap an
`astropy.units.Quantity`.
Subclasses must have a self._quantity attribute that is an
`astropy.units.Quantity`, `str`, `bool`, or `None` (only numeric values are
astropy quantities).
"""
@property
def quantity(self):
"""Value of the datum (`astropy.units.Quantity`, `str`, `bool`,
`None`)."""
return self._quantity
@staticmethod
def _is_non_quantity_type(q):
"""Test if a quantity is a acceptable (`str`, `bool`, `int`, or
`None`), but not `astropy.quantity`."""
return isinstance(q, basestring) or isinstance(q, bool) or \
isinstance(q, int) or q is None
@quantity.setter
def quantity(self, q):
assert isinstance(q, u.Quantity) or \
QuantityAttributeMixin._is_non_quantity_type(q)
self._quantity = q
@property
def unit(self):
"""Read-only `astropy.units.Unit` of the `quantity`.
If the `quantity` is a `str` or `bool`, the unit is `None`.
"""
q = self.quantity
if QuantityAttributeMixin._is_non_quantity_type(q):
return None
else:
return q.unit
@property
def unit_str(self):
"""Read-only `astropy.units.Unit`-compatible `str` indicating units of
`quantity`.
"""
if self.unit is None:
# unitless quantites have an empty string for a unit; retain this
# behaviour for str and bool quantities.
return ''
else:
return str(self.unit)
@property
def latex_unit(self):
"""Units as a LaTeX string, wrapped in ``$``."""
if self.unit is not None and self.unit != '':
fmtr = u.format.Latex()
return fmtr.to_string(self.unit)
else:
return ''
@staticmethod
def _rebuild_quantity(value, unit):
"""Rebuild a quantity from the value and unit serialized to JSON.
Parameters
----------
value : `list`, `float`, `int`, `str`, `bool`
Serialized quantity value.
unit : `str`
Serialized quantity unit string.
Returns
-------
q : `astropy.units.Quantity`, `str`, `int`, `bool` or `None`
Astropy quantity.
"""
if QuantityAttributeMixin._is_non_quantity_type(value):
_quantity = value
elif isinstance(value, list):
# an astropy quantity array
_quantity = np.array(value) * u.Unit(unit)
else:
# scalar astropy quantity
_quantity = value * u.Unit(unit)
return _quantity
[docs]class Datum(QuantityAttributeMixin, JsonSerializationMixin):
"""A value annotated with units, a plot label and description.
Datum supports natively support Astropy `~astropy.units.Quantity` and
units. In addition, a Datum can also wrap strings, booleans and integers.
A Datums's value can also be `None`.
Parameters
----------
quantity : `astropy.units.Quantity`, `int`, `float` or iterable.
Value of the `Datum`.
unit : `str`
Units of ``quantity`` as a `str` if ``quantity`` is not supplied as an
`astropy.units.Quantity`. See http://docs.astropy.org/en/stable/units/.
Units are not used by `str`, `bool`, `int` or `None` types.
label : `str`, optional
Label suitable for plot axes (without units).
description : `str`, optional
Extended description of the `Datum`.
"""
def __init__(self, quantity=None, unit=None, label=None, description=None):
self._label = None
self._description = None
self.label = label
self.description = description
self._quantity = None
if isinstance(quantity, u.Quantity) or \
QuantityAttributeMixin._is_non_quantity_type(quantity):
self.quantity = quantity
elif unit is not None:
self.quantity = u.Quantity(quantity, unit=unit)
else:
raise ValueError('`unit` argument must be supplied to Datum '
'if `quantity` is not an astropy.unit.Quantity, '
'str, bool, int or None.')
@classmethod
[docs] def deserialize(cls, label=None, description=None, value=None, unit=None):
"""Deserialize fields from a Datum JSON object into a `Datum` instance.
Parameters
----------
value : `float`, `int`, `bool`, `str`, or `list`
Values, which may be scalars or lists of scalars.
unit : `str` or `None`
An `astropy.units`-compatible string with units of ``value``,
or `None` if the value does not have physical units.
label : `str`, optional
Label suitable for plot axes (without units).
description : `str`, optional
Extended description of the `Datum`.
Returns
-------
datum : `Datum`
Datum instantiated from provided JSON fields.
Examples
--------
With this class method, a `Datum` may be round-tripped from its
JSON serialized form.
>>> datum = Datum(50. * u.mmag, label='sigma',
... description="Photometric uncertainty.")
>>> print(datum)
sigma = 50.0 mmag
Photometric uncertainty.
>>> json_data = datum.json
>>> new_datum = datum.deserialize(**json_data)
>>> print(new_datum)
sigma = 50.0 mmag
Photometric uncertainty.
"""
return cls(quantity=value, unit=unit, label=label,
description=description)
@property
def json(self):
"""Datum as a `dict` compatible with overall `Job` JSON schema."""
if QuantityAttributeMixin._is_non_quantity_type(self.quantity):
v = self.quantity
elif len(self.quantity.shape) > 0:
v = self.quantity.value.tolist()
else:
v = self.quantity.value
d = {
'value': v,
'unit': self.unit_str,
'label': self.label,
'description': self.description
}
return d
@property
def label(self):
"""Label for plotting (without units)."""
return self._label
@label.setter
def label(self, value):
assert isinstance(value, basestring) or value is None
self._label = value
@property
def description(self):
"""Extended description."""
return self._description
@description.setter
def description(self, value):
assert isinstance(value, basestring) or value is None
self._description = value
def __eq__(self, other):
if self.label != other.label:
return False
if self.description != other.description:
return False
if isinstance(self.quantity, u.Quantity):
if not quantity_allclose(self.quantity, other.quantity):
return False
else:
if self.quantity != other.quantity:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
template = ''
if self.label is not None:
template += '{self.label} = '
template += '{self.quantity}'
if self.description is not None:
template += '\n{self.description}'
return template.format(self=self)