#
# 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__ = ['ThresholdSpecification']
import operator
import astropy.units as u
from astropy.tests.helper import quantity_allclose
from ..jsonmixin import JsonSerializationMixin
from ..datum import Datum
from ..naming import Name
from .base import Specification
[docs]class ThresholdSpecification(Specification):
    """A threshold-type specification, associated with a `Metric`, that
    defines a binary comparison against a measurement.
    Parameters
    ----------
    name : `str`
        Name of the specification for a metric. LPM-17, for example,
        uses ``'design'``, ``'minimum'`` and ``'stretch'`` terminology.
    quantity : `astropy.units.Quantity`
        The specification threshold level.
    operator_str : `str`
        The threshold's binary comparison operator. The operator is oriented
        so that ``measurement {{ operator }} threshold quantity`` is the
        specification test. Can be one of: ``'<'``, ``'<='``, ``'>'``,
        ``'>='``, ``'=='``, or ``'!='``.
    metadata_query : `dict`, optional
        Dictionary of key-value terms that the measurement's metadata must
        have for this specification to apply.
    tags : sequence of `str`, optional
        Sequence of tags that group this specification with others.
    kwargs : `dict`
        Keyword arguments passed directly to the
        `lsst.validate.base.Specification` constructor.
    Raises
    ------
    TypeError
        If ``name`` is not compartible with `~lsst.verify.Name`,
        or `threshold` is not a `~astropy.units.Quantity`, or if the
        ``operator_str`` cannot be converted into a Python binary comparison
        operator.
    """
    threshold = None
    """The specification threshold level (`astropy.units.Quantity`)."""
    def __init__(self, name, threshold, operator_str, **kwargs):
        Specification.__init__(self, name, **kwargs)
        self.threshold = threshold
        if not isinstance(self.threshold, u.Quantity):
            message = 'threshold {0!r} must be an astropy.units.Quantity'
            raise TypeError(message.format(self.threshold))
        if not self.threshold.isscalar:
            raise TypeError('threshold must be scalar')
        try:
            self.operator_str = operator_str
        except ValueError:
            message = '{0!r} is not a known operator'.format(operator_str)
            raise TypeError(message)
    @property
    def type(self):
        return 'threshold'
    def __eq__(self, other):
        return (self.type == other.type) and \
            
(self.name == other.name) and \
            
quantity_allclose(self.threshold, other.threshold) and \
            
(self.operator_str == other.operator_str)
    def __ne__(self, other):
        return not self.__eq__(other)
    def __repr__(self):
        return "ThresholdSpecification({0!r}, {1!r}, {2!r})".format(
            self.name,
            self.threshold,
            self.operator_str)
    def __str__(self):
        return '{self.operator_str} {self.threshold}'.format(self=self)
    def _repr_latex_(self):
        """Get a LaTeX-formatted string representation of the threshold
        specification test.
        Returns
        -------
        rep : `str`
            String representation.
        """
        template = ('$x$ {self.operator_str} '
                    '{self.threshold.value} '
                    '{self.threshold.unit:latex_inline}')
        return template.format(self=self)
    @property
    def datum(self):
        """Representation of this `ThresholdSpecification`\ 's threshold as
        a `Datum`.
        """
        return Datum(self.threshold, label=str(self.name))
    @classmethod
[docs]    def deserialize(cls, name=None, threshold=None,
                    metric=None, package=None, **kwargs):
        """Deserialize from keys in a specification YAML document or a
        JSON serialization into a `ThresholdSpecification` instance.
        Parameters
        ----------
        name : `str` or `lsst.validate.base.Name`
            Specification name, either as a string or
            `~lsst.validate.base.Name`.
        threshold : `dict`
            A `dict` with fields:
            - ``'value'``: threshold value (`float` or `int`).
            - ``'unit'``: threshold unit, as an `astropy.units.Unit`-
              compatible `str`.
            - ``'operator'``: a binary comparison operator, described in
              the class parameters documentation (`str`).
        metric : `str` or `lsst.validate.base.Name`, optional
            Name of the fully-qualified name of the metric the specification
            corresponds to. This parameter is optional if ``name`` is already
            fully-qualified.
        package : `str` or `lsst.validate.base.Name`, optional
            Name of the package the specification corresponds to. This
            parameter is optional if ``name`` or ``metric`` are already
            fully-qualified.
        kwargs : `dict`
            Keyword arguments passed directly to the
            `lsst.validate.base.Specification` constructor.
        Returns
        -------
        specification : `ThresholdSpecification`
            A specification instance.
        """
        _name = Name(metric=metric, spec=name)
        operator_str = threshold['operator']
        _threshold = u.Quantity(threshold['value'],
                                u.Unit(threshold['unit']))
        return cls(_name, _threshold, operator_str, **kwargs) 
    def _serialize_type(self):
        """Serialize attributes of this specification type to a `dict` that is
        JSON-serializable.
        """
        return JsonSerializationMixin.jsonify_dict(
            {
                'value': self.threshold.value,
                'unit': self.threshold.unit.to_string(),
                'operator': self.operator_str
            }
        )
    @property
    def operator_str(self):
        """Threshold comparision operator ('str').
        A measurement *passes* the specification if::
           measurement {{ operator }} threshold == True
        The operator string is a standard Python binary comparison token, such
        as: ``'<'``, ``'>'``, ``'<='``, ``'>='``, ``'=='`` or ``'!='``.
        """
        return self._operator_str
    @operator_str.setter
    def operator_str(self, v):
        # Cache the operator function as a means of validating the input too
        self._operator = ThresholdSpecification.convert_operator_str(v)
        self._operator_str = v
    @property
    def operator(self):
        """Binary comparision operator that tests success of a measurement
        fulfilling a specification of this metric.
        Measured value is on left side of comparison and specification level
        is on right side.
        """
        return self._operator
    @staticmethod
[docs]    def convert_operator_str(op_str):
        """Convert a string representing a binary comparison operator to
        the operator function itself.
        Operators are oriented so that the measurement is on the left-hand
        side, and specification threshold on the right hand side.
        The following operators are permitted:
        ========== =============
        ``op_str`` Function
        ========== =============
        ``>=``     `operator.ge`
        ``>``      `operator.gt`
        ``<``      `operator.lt`
        ``<=``     `operator.le`
        ``==``     `operator.eq`
        ``!=``     `operator.ne`
        ========== =============
        Parameters
        ----------
        op_str : `str`
            A string representing a binary operator.
        Returns
        -------
        op_func : obj
            An operator function from the `operator` standard library
            module.
        Raises
        ------
        ValueError
            Raised if ``op_str`` is not a supported binary comparison operator.
        """
        operators = {'>=': operator.ge,
                     '>': operator.gt,
                     '<': operator.lt,
                     '<=': operator.le,
                     '==': operator.eq,
                     '!=': operator.ne}
        try:
            return operators[op_str]
        except KeyError:
            message = '{0!r} is not a supported threshold operator'.format(
                op_str)
            raise ValueError(message) 
[docs]    def check(self, measurement):
        """Check if a measurement passes this specification.
        Parameters
        ----------
        measurement : `astropy.units.Quantity`
            The measurement value. The measurement `~astropy.units.Quantity`
            must have units *compatible* with `threshold`.
        Returns
        -------
        passed : `bool`
            `True` if the measurement meets the specification,
            `False` otherwise.
        Raises
        ------
        astropy.units.UnitError
            Raised if the measurement cannot be compared to the threshold.
            For example, if the measurement is not an `astropy.units.Quantity`
            or if the units are not compatible.
        """
        return self.operator(measurement, self.threshold)