#
# 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__ = ['Metric']
from past.builtins import basestring
import astropy.units as u
from .jsonmixin import JsonSerializationMixin
from .naming import Name
[docs]class Metric(JsonSerializationMixin):
"""Container for the definition of a metric.
Metrics can either be instantiated programatically, or from a metric YAML
file through `lsst.verify.MetricSet`.
Parameters
----------
name : `str`
Name of the metric (e.g., ``'PA1'``).
description : `str`
Short description about the metric.
unit : `str` or `astropy.units.Unit`
Units of the metric. `~lsst.verify.Measurement`\ s of this metric must
be in an equivalent (that is, convertable) unit. Argument can either be
an `astropy.unit.Unit` instance, or a `~astropy.unit.Unit`-compatible
string representation. Use an empty string, ``''``, or
`astropy.units.dimensionless_unscaled` for a unitless quantity.
tags : `list` of `str`
Tags associated with this metric. Tags are user-submitted string
tokens that are used to group metrics.
reference_doc : `str`, optional
The document handle that originally defined the metric
(e.g., ``'LPM-17'``).
reference_url : `str`, optional
The document's URL.
reference_page : `str`, optional
Page where metric in defined in the reference document.
"""
description = None
"""Short description of the metric (`str`)."""
reference_doc = None
"""Name of the document that specifies this metric (`str`)."""
reference_url = None
"""URL of the document that specifies this metric (`str`)."""
reference_page = None
"""Page number in the document that specifies this metric (`int`)."""
def __init__(self, name, description, unit, tags=None,
reference_doc=None, reference_url=None, reference_page=None):
self.name = name
self.description = description
self.unit = u.Unit(unit)
if tags is None:
self.tags = set()
else:
# FIXME DM-8477 Need type checking that tags are actually strings
# and are a set.
self.tags = tags
self.reference_doc = reference_doc
self.reference_url = reference_url
self.reference_page = reference_page
@classmethod
[docs] def deserialize(cls, name=None, description=None, unit=None,
tags=None, reference=None):
"""Create a Metric instance from a parsed YAML/JSON document.
Parameters
----------
kwargs : `dict`
Keyword arguments that match fields from the `Metric.json`
serialization.
Returns
-------
metric : `Metric`
A Metric instance.
"""
# keyword args for Metric __init__
args = {
'unit': unit,
'tags': tags,
# Remove trailing newline from folded block description field.
# This isn't necessary if the field is trimmed with `>-` in YAML,
# but won't hurt either.
'description': description.rstrip('\n')
}
if reference is not None:
args['reference_doc'] = reference.get('doc', None)
args['reference_page'] = reference.get('page', None)
args['reference_url'] = reference.get('url', None)
return cls(name, **args)
def __eq__(self, other):
return ((self.name == other.name) and
(self.unit == other.unit) and
(self.tags == other.tags) and
(self.description == other.description) and
(self.reference == other.reference))
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
# self.unit_str provides the astropy.unit.Unit's string representation
# that can be used to create a new Unit. But for readability,
# we use 'dimensionless_unscaled' (an member of astropy.unit) rather
# than an empty string for the Metric's string representation.
if self.unit_str == '':
unit_str = 'dimensionless_unscaled'
else:
unit_str = self.unit_str
return '{self.name!s} ({unit_str}): {self.description}'.format(
self=self, unit_str=unit_str)
@property
def name(self):
"""Metric's name (`Name`)."""
return self._name
@name.setter
def name(self, value):
self._name = Name(metric=value)
@property
def unit(self):
"""The metric's unit (`astropy.units.Unit`)."""
return self._unit
@unit.setter
def unit(self, value):
if not isinstance(value, (u.UnitBase, u.FunctionUnitBase)):
message = ('unit attribute must be an astropy.units.Unit-type. '
' Currently type {0!s}.'.format(type(value)))
if isinstance(value, basestring):
message += (' Set the `unit_str` attribute instead for '
'assigning the unit as a string')
raise ValueError(message)
self._unit = value
@property
def unit_str(self):
"""The string representation of the metric's unit
(`~astropy.units.Unit`-compatible `str`).
"""
return str(self.unit)
@unit_str.setter
def unit_str(self, value):
self.unit = u.Unit(value)
@property
def tags(self):
"""Tag labels (`set` of `str`)."""
return self._tags
@tags.setter
def tags(self, t):
# Ensure that tags is always a set.
if isinstance(t, basestring):
t = [t]
self._tags = set(t)
@property
def reference(self):
"""Documentation reference as human-readable text (`str`, read-only).
Uses `reference_doc`, `reference_page`, and `reference_url`, as
available.
"""
ref_str = ''
if self.reference_doc and self.reference_page:
ref_str = '{doc}, p. {page:d}'.format(doc=self.reference_doc,
page=self.reference_page)
elif self.reference_doc:
ref_str = self.reference_doc
if self.reference_url and self.reference_doc:
ref_str += ', {url}'.format(url=self.reference_url)
elif self.reference_url:
ref_str = self.reference_url
return ref_str
@property
def json(self):
"""`dict` that can be serialized as semantic JSON, compatible with
the SQUASH metric service.
"""
ref_doc = {
'doc': self.reference_doc,
'page': self.reference_page,
'url': self.reference_url}
return JsonSerializationMixin.jsonify_dict({
'name': str(self.name),
'description': self.description,
'unit': self.unit_str,
'tags': self.tags,
'reference': ref_doc})
[docs] def check_unit(self, quantity):
"""Check that a `~astropy.units.Quantity` has equivalent units to
this metric.
Parameters
----------
quantity : `astropy.units.Quantity`
Quantity to be tested.
Returns
-------
is_equivalent : `bool`
`True` if the units are equivalent, meaning that the quantity
can be presented in the units of this metric. `False` if not.
"""
if not quantity.unit.is_equivalent(self.unit):
return False
else:
return True