#
# 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)