Coverage for python/lsst/verify/datum.py: 30%

100 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-11 06:50 +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'] 

22 

23import numpy as np 

24from astropy.tests.helper import quantity_allclose 

25import astropy.units as u 

26 

27from .jsonmixin import JsonSerializationMixin 

28 

29 

30class QuantityAttributeMixin: 

31 """Mixin with common attributes for classes that wrap an 

32 `astropy.units.Quantity`. 

33 

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 """ 

38 

39 @property 

40 def quantity(self): 

41 """Value of the datum (`astropy.units.Quantity`, `str`, `bool`, 

42 `None`).""" 

43 return self._quantity 

44 

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 

51 

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 

57 

58 @property 

59 def unit(self): 

60 """Read-only `astropy.units.Unit` of the `quantity`. 

61 

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 

69 

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) 

81 

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 '' 

90 

91 @staticmethod 

92 def _rebuild_quantity(value, unit): 

93 """Rebuild a quantity from the value and unit serialized to JSON. 

94 

95 Parameters 

96 ---------- 

97 value : `list`, `float`, `int`, `str`, `bool` 

98 Serialized quantity value. 

99 unit : `str` 

100 Serialized quantity unit string. 

101 

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 

116 

117 

118class Datum(QuantityAttributeMixin, JsonSerializationMixin): 

119 """A value annotated with units, a plot label and description. 

120 

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`. 

124 

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 

141 

142 self.label = label 

143 self.description = description 

144 

145 self._quantity = None 

146 

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.') 

156 

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. 

160 

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`. 

172 

173 Returns 

174 ------- 

175 datum : `Datum` 

176 Datum instantiated from provided JSON fields. 

177 

178 Examples 

179 -------- 

180 With this class method, a `Datum` may be round-tripped from its 

181 JSON serialized form. 

182 

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) 

196 

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 

206 

207 d = { 

208 'value': v, 

209 'unit': self.unit_str, 

210 'label': self.label, 

211 'description': self.description 

212 } 

213 return d 

214 

215 @property 

216 def label(self): 

217 """Label for plotting (without units).""" 

218 return self._label 

219 

220 @label.setter 

221 def label(self, value): 

222 assert isinstance(value, str) or value is None 

223 self._label = value 

224 

225 @property 

226 def description(self): 

227 """Extended description.""" 

228 return self._description 

229 

230 @description.setter 

231 def description(self, value): 

232 assert isinstance(value, str) or value is None 

233 self._description = value 

234 

235 def __eq__(self, other): 

236 if self.label != other.label: 

237 return False 

238 

239 if self.description != other.description: 

240 return False 

241 

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 

248 

249 return True 

250 

251 def __ne__(self, other): 

252 return not self.__eq__(other) 

253 

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)