Coverage for python/lsst/verify/metric.py: 35%
91 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 02:15 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-12 02:15 -0700
1# This file is part of verify.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21__all__ = ['Metric']
23import astropy.units as u
25from .jsonmixin import JsonSerializationMixin
26from .naming import Name
29class Metric(JsonSerializationMixin):
30 r"""Container for the definition of a metric.
32 Metrics can either be instantiated programatically, or from a metric YAML
33 file through `lsst.verify.MetricSet`.
35 Parameters
36 ----------
37 name : `str`
38 Name of the metric (e.g., ``'PA1'``).
39 description : `str`
40 Short description about the metric.
41 unit : `str` or `astropy.units.Unit`
42 Units of the metric. `~lsst.verify.Measurement`\ s of this metric must
43 be in an equivalent (that is, convertable) unit. Argument can either be
44 an `astropy.unit.Unit` instance, or a `~astropy.unit.Unit`-compatible
45 string representation. Use an empty string, ``''``, or
46 `astropy.units.dimensionless_unscaled` for a unitless quantity.
47 tags : `list` of `str`
48 Tags associated with this metric. Tags are user-submitted string
49 tokens that are used to group metrics.
50 reference_doc : `str`, optional
51 The document handle that originally defined the metric
52 (e.g., ``'LPM-17'``).
53 reference_url : `str`, optional
54 The document's URL.
55 reference_page : `str`, optional
56 Page where metric in defined in the reference document.
57 """
59 description = None
60 """Short description of the metric (`str`)."""
62 reference_doc = None
63 """Name of the document that specifies this metric (`str`)."""
65 reference_url = None
66 """URL of the document that specifies this metric (`str`)."""
68 reference_page = None
69 """Page number in the document that specifies this metric (`int`)."""
71 def __init__(self, name, description, unit, tags=None,
72 reference_doc=None, reference_url=None, reference_page=None):
73 self.name = name
74 self.description = description
75 self.unit = u.Unit(unit)
76 if tags is None:
77 self.tags = set()
78 else:
79 # FIXME DM-8477 Need type checking that tags are actually strings
80 # and are a set.
81 self.tags = tags
82 self.reference_doc = reference_doc
83 self.reference_url = reference_url
84 self.reference_page = reference_page
86 @classmethod
87 def deserialize(cls, name=None, description=None, unit=None,
88 tags=None, reference=None):
89 """Create a Metric instance from a parsed YAML/JSON document.
91 Parameters
92 ----------
93 kwargs : `dict`
94 Keyword arguments that match fields from the `Metric.json`
95 serialization.
97 Returns
98 -------
99 metric : `Metric`
100 A Metric instance.
101 """
102 # keyword args for Metric __init__
103 args = {
104 'unit': unit,
105 'tags': tags,
106 # Remove trailing newline from folded block description field.
107 # This isn't necessary if the field is trimmed with `>-` in YAML,
108 # but won't hurt either.
109 'description': description.rstrip('\n')
110 }
112 if reference is not None:
113 args['reference_doc'] = reference.get('doc', None)
114 args['reference_page'] = reference.get('page', None)
115 args['reference_url'] = reference.get('url', None)
117 return cls(name, **args)
119 def __eq__(self, other):
120 return ((self.name == other.name)
121 and (self.unit == other.unit)
122 and (self.tags == other.tags)
123 and (self.description == other.description)
124 and (self.reference == other.reference))
126 def __ne__(self, other):
127 return not self.__eq__(other)
129 def __str__(self):
130 # self.unit_str provides the astropy.unit.Unit's string representation
131 # that can be used to create a new Unit. But for readability,
132 # we use 'dimensionless_unscaled' (an member of astropy.unit) rather
133 # than an empty string for the Metric's string representation.
134 if self.unit_str == '':
135 unit_str = 'dimensionless_unscaled'
136 else:
137 unit_str = self.unit_str
138 return '{self.name!s} ({unit_str}): {self.description}'.format(
139 self=self, unit_str=unit_str)
141 @property
142 def name(self):
143 """Metric's name (`Name`)."""
144 return self._name
146 @name.setter
147 def name(self, value):
148 self._name = Name(metric=value)
150 @property
151 def unit(self):
152 """The metric's unit (`astropy.units.Unit`)."""
153 return self._unit
155 @unit.setter
156 def unit(self, value):
157 if not isinstance(value, (u.UnitBase, u.FunctionUnitBase)):
158 message = ('unit attribute must be an astropy.units.Unit-type. '
159 ' Currently type {0!s}.'.format(type(value)))
160 if isinstance(value, str):
161 message += (' Set the `unit_str` attribute instead for '
162 'assigning the unit as a string')
163 raise ValueError(message)
164 self._unit = value
166 @property
167 def unit_str(self):
168 """The string representation of the metric's unit
169 (`~astropy.units.Unit`-compatible `str`).
170 """
171 return str(self.unit)
173 @unit_str.setter
174 def unit_str(self, value):
175 self.unit = u.Unit(value)
177 @property
178 def tags(self):
179 """Tag labels (`set` of `str`)."""
180 return self._tags
182 @tags.setter
183 def tags(self, t):
184 # Ensure that tags is always a set.
185 if isinstance(t, str):
186 t = [t]
187 self._tags = set(t)
189 @property
190 def reference(self):
191 """Documentation reference as human-readable text (`str`, read-only).
193 Uses `reference_doc`, `reference_page`, and `reference_url`, as
194 available.
195 """
196 ref_str = ''
197 if self.reference_doc and self.reference_page:
198 ref_str = '{doc}, p. {page:d}'.format(doc=self.reference_doc,
199 page=self.reference_page)
200 elif self.reference_doc:
201 ref_str = self.reference_doc
203 if self.reference_url and self.reference_doc:
204 ref_str += ', {url}'.format(url=self.reference_url)
205 elif self.reference_url:
206 ref_str = self.reference_url
208 return ref_str
210 @property
211 def json(self):
212 """`dict` that can be serialized as semantic JSON, compatible with
213 the SQUASH metric service.
214 """
215 ref_doc = {
216 'doc': self.reference_doc,
217 'page': self.reference_page,
218 'url': self.reference_url}
219 return JsonSerializationMixin.jsonify_dict({
220 'name': str(self.name),
221 'description': self.description,
222 'unit': self.unit_str,
223 'tags': self.tags,
224 'reference': ref_doc})
226 def check_unit(self, quantity):
227 """Check that a `~astropy.units.Quantity` has equivalent units to
228 this metric.
230 Parameters
231 ----------
232 quantity : `astropy.units.Quantity`
233 Quantity to be tested.
235 Returns
236 -------
237 is_equivalent : `bool`
238 `True` if the units are equivalent, meaning that the quantity
239 can be presented in the units of this metric. `False` if not.
240 """
241 if not quantity.unit.is_equivalent(self.unit):
242 return False
243 else:
244 return True