Coverage for python/lsst/verify/datum.py: 30%
100 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-22 09:11 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-22 09:11 +0000
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__ = ['Datum']
23import numpy as np
24from astropy.tests.helper import quantity_allclose
25import astropy.units as u
27from .jsonmixin import JsonSerializationMixin
30class QuantityAttributeMixin:
31 """Mixin with common attributes for classes that wrap an
32 `astropy.units.Quantity`.
34 Subclasses must have a self._quantity attribute that is an
35 `astropy.units.Quantity`, `str`, `bool`, or `None` (only numeric values are
36 astropy quantities).
37 """
39 @property
40 def quantity(self):
41 """Value of the datum (`astropy.units.Quantity`, `str`, `bool`,
42 `None`)."""
43 return self._quantity
45 @staticmethod
46 def _is_non_quantity_type(q):
47 """Test if a quantity is a acceptable (`str`, `bool`, `int`, or
48 `None`), but not `astropy.quantity`."""
49 return isinstance(q, str) or isinstance(q, bool) or \
50 isinstance(q, int) or q is None
52 @quantity.setter
53 def quantity(self, q):
54 assert isinstance(q, u.Quantity) or \
55 QuantityAttributeMixin._is_non_quantity_type(q)
56 self._quantity = q
58 @property
59 def unit(self):
60 """Read-only `astropy.units.Unit` of the `quantity`.
62 If the `quantity` is a `str` or `bool`, the unit is `None`.
63 """
64 q = self.quantity
65 if QuantityAttributeMixin._is_non_quantity_type(q):
66 return None
67 else:
68 return q.unit
70 @property
71 def unit_str(self):
72 """Read-only `astropy.units.Unit`-compatible `str` indicating units of
73 `quantity`.
74 """
75 if self.unit is None:
76 # unitless quantites have an empty string for a unit; retain this
77 # behaviour for str and bool quantities.
78 return ''
79 else:
80 return str(self.unit)
82 @property
83 def latex_unit(self):
84 """Units as a LaTeX string, wrapped in ``$``."""
85 if self.unit is not None and self.unit != '':
86 fmtr = u.format.Latex()
87 return fmtr.to_string(self.unit)
88 else:
89 return ''
91 @staticmethod
92 def _rebuild_quantity(value, unit):
93 """Rebuild a quantity from the value and unit serialized to JSON.
95 Parameters
96 ----------
97 value : `list`, `float`, `int`, `str`, `bool`
98 Serialized quantity value.
99 unit : `str`
100 Serialized quantity unit string.
102 Returns
103 -------
104 q : `astropy.units.Quantity`, `str`, `int`, `bool` or `None`
105 Astropy quantity.
106 """
107 if QuantityAttributeMixin._is_non_quantity_type(value):
108 _quantity = value
109 elif isinstance(value, list):
110 # an astropy quantity array
111 _quantity = np.array(value) * u.Unit(unit)
112 else:
113 # scalar astropy quantity
114 _quantity = value * u.Unit(unit)
115 return _quantity
118class Datum(QuantityAttributeMixin, JsonSerializationMixin):
119 """A value annotated with units, a plot label and description.
121 Datum supports natively support Astropy `~astropy.units.Quantity` and
122 units. In addition, a Datum can also wrap strings, booleans and integers.
123 A Datums's value can also be `None`.
125 Parameters
126 ----------
127 quantity : `astropy.units.Quantity`, `int`, `float` or iterable.
128 Value of the `Datum`.
129 unit : `str`
130 Units of ``quantity`` as a `str` if ``quantity`` is not supplied as an
131 `astropy.units.Quantity`. See http://docs.astropy.org/en/stable/units/.
132 Units are not used by `str`, `bool`, `int` or `None` types.
133 label : `str`, optional
134 Label suitable for plot axes (without units).
135 description : `str`, optional
136 Extended description of the `Datum`.
137 """
138 def __init__(self, quantity=None, unit=None, label=None, description=None):
139 self._label = None
140 self._description = None
142 self.label = label
143 self.description = description
145 self._quantity = None
147 if isinstance(quantity, u.Quantity) or \
148 QuantityAttributeMixin._is_non_quantity_type(quantity):
149 self.quantity = quantity
150 elif unit is not None:
151 self.quantity = u.Quantity(quantity, unit=unit)
152 else:
153 raise ValueError('`unit` argument must be supplied to Datum '
154 'if `quantity` is not an astropy.unit.Quantity, '
155 'str, bool, int or None.')
157 @classmethod
158 def deserialize(cls, label=None, description=None, value=None, unit=None):
159 """Deserialize fields from a Datum JSON object into a `Datum` instance.
161 Parameters
162 ----------
163 value : `float`, `int`, `bool`, `str`, or `list`
164 Values, which may be scalars or lists of scalars.
165 unit : `str` or `None`
166 An `astropy.units`-compatible string with units of ``value``,
167 or `None` if the value does not have physical units.
168 label : `str`, optional
169 Label suitable for plot axes (without units).
170 description : `str`, optional
171 Extended description of the `Datum`.
173 Returns
174 -------
175 datum : `Datum`
176 Datum instantiated from provided JSON fields.
178 Examples
179 --------
180 With this class method, a `Datum` may be round-tripped from its
181 JSON serialized form.
183 >>> datum = Datum(50. * u.mmag, label='sigma',
184 ... description="Photometric uncertainty.")
185 >>> print(datum)
186 sigma = 50.0 mmag
187 Photometric uncertainty.
188 >>> json_data = datum.json
189 >>> new_datum = datum.deserialize(**json_data)
190 >>> print(new_datum)
191 sigma = 50.0 mmag
192 Photometric uncertainty.
193 """
194 return cls(quantity=value, unit=unit, label=label,
195 description=description)
197 @property
198 def json(self):
199 """Datum as a `dict` compatible with overall `Job` JSON schema."""
200 if QuantityAttributeMixin._is_non_quantity_type(self.quantity):
201 v = self.quantity
202 elif len(self.quantity.shape) > 0:
203 v = self.quantity.value.tolist()
204 else:
205 v = self.quantity.value
207 d = {
208 'value': v,
209 'unit': self.unit_str,
210 'label': self.label,
211 'description': self.description
212 }
213 return d
215 @property
216 def label(self):
217 """Label for plotting (without units)."""
218 return self._label
220 @label.setter
221 def label(self, value):
222 assert isinstance(value, str) or value is None
223 self._label = value
225 @property
226 def description(self):
227 """Extended description."""
228 return self._description
230 @description.setter
231 def description(self, value):
232 assert isinstance(value, str) or value is None
233 self._description = value
235 def __eq__(self, other):
236 if self.label != other.label:
237 return False
239 if self.description != other.description:
240 return False
242 if isinstance(self.quantity, u.Quantity):
243 if not quantity_allclose(self.quantity, other.quantity):
244 return False
245 else:
246 if self.quantity != other.quantity:
247 return False
249 return True
251 def __ne__(self, other):
252 return not self.__eq__(other)
254 def __str__(self):
255 template = ''
256 if self.label is not None:
257 template += '{self.label} = '
258 template += '{self.quantity}'
259 if self.description is not None:
260 template += '\n{self.description}'
261 return template.format(self=self)