Coverage for python/lsst/verify/spec/threshold.py: 45%

69 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 09:25 +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__ = ['ThresholdSpecification'] 

22 

23import operator 

24 

25import astropy.units as u 

26from astropy.tests.helper import quantity_allclose 

27 

28from ..jsonmixin import JsonSerializationMixin 

29from ..datum import Datum 

30from ..naming import Name 

31from .base import Specification 

32 

33 

34class ThresholdSpecification(Specification): 

35 """A threshold-type specification, associated with a `Metric`, that 

36 defines a binary comparison against a measurement. 

37 

38 Parameters 

39 ---------- 

40 name : `str` 

41 Name of the specification for a metric. LPM-17, for example, 

42 uses ``'design'``, ``'minimum'`` and ``'stretch'`` terminology. 

43 quantity : `astropy.units.Quantity` 

44 The specification threshold level. 

45 operator_str : `str` 

46 The threshold's binary comparison operator. The operator is oriented 

47 so that ``measurement {{ operator }} threshold quantity`` is the 

48 specification test. Can be one of: ``'<'``, ``'<='``, ``'>'``, 

49 ``'>='``, ``'=='``, or ``'!='``. 

50 metadata_query : `dict`, optional 

51 Dictionary of key-value terms that the measurement's metadata must 

52 have for this specification to apply. 

53 tags : sequence of `str`, optional 

54 Sequence of tags that group this specification with others. 

55 kwargs : `dict` 

56 Keyword arguments passed directly to the 

57 `lsst.validate.base.Specification` constructor. 

58 

59 Raises 

60 ------ 

61 TypeError 

62 If ``name`` is not compartible with `~lsst.verify.Name`, 

63 or `threshold` is not a `~astropy.units.Quantity`, or if the 

64 ``operator_str`` cannot be converted into a Python binary comparison 

65 operator. 

66 """ 

67 

68 threshold = None 

69 """The specification threshold level (`astropy.units.Quantity`).""" 

70 

71 def __init__(self, name, threshold, operator_str, **kwargs): 

72 Specification.__init__(self, name, **kwargs) 

73 

74 self.threshold = threshold 

75 if not isinstance(self.threshold, u.Quantity): 

76 message = 'threshold {0!r} must be an astropy.units.Quantity' 

77 raise TypeError(message.format(self.threshold)) 

78 if not self.threshold.isscalar: 

79 raise TypeError('threshold must be scalar') 

80 

81 try: 

82 self.operator_str = operator_str 

83 except ValueError: 

84 message = '{0!r} is not a known operator'.format(operator_str) 

85 raise TypeError(message) 

86 

87 @property 

88 def type(self): 

89 return 'threshold' 

90 

91 def __eq__(self, other): 

92 return (self.type == other.type) and \ 

93 (self.name == other.name) and \ 

94 quantity_allclose(self.threshold, other.threshold) and \ 

95 (self.operator_str == other.operator_str) 

96 

97 def __ne__(self, other): 

98 return not self.__eq__(other) 

99 

100 def __repr__(self): 

101 return "ThresholdSpecification({0!r}, {1!r}, {2!r})".format( 

102 self.name, 

103 self.threshold, 

104 self.operator_str) 

105 

106 def __str__(self): 

107 return '{self.operator_str} {self.threshold}'.format(self=self) 

108 

109 def _repr_latex_(self): 

110 """Get a LaTeX-formatted string representation of the threshold 

111 specification test. 

112 

113 Returns 

114 ------- 

115 rep : `str` 

116 String representation. 

117 """ 

118 template = ('$x$ {self.operator_str} ' 

119 '{self.threshold.value} ' 

120 '{self.threshold.unit:latex_inline}') 

121 return template.format(self=self) 

122 

123 @property 

124 def datum(self): 

125 r"""Representation of this `ThresholdSpecification`\ 's threshold as 

126 a `Datum`. 

127 """ 

128 return Datum(self.threshold, label=str(self.name)) 

129 

130 @classmethod 

131 def deserialize(cls, name=None, threshold=None, 

132 metric=None, package=None, **kwargs): 

133 """Deserialize from keys in a specification YAML document or a 

134 JSON serialization into a `ThresholdSpecification` instance. 

135 

136 Parameters 

137 ---------- 

138 name : `str` or `lsst.validate.base.Name` 

139 Specification name, either as a string or 

140 `~lsst.validate.base.Name`. 

141 threshold : `dict` 

142 A `dict` with fields: 

143 

144 - ``'value'``: threshold value (`float` or `int`). 

145 - ``'unit'``: threshold unit, as an `astropy.units.Unit`- 

146 compatible `str`. 

147 - ``'operator'``: a binary comparison operator, described in 

148 the class parameters documentation (`str`). 

149 metric : `str` or `lsst.validate.base.Name`, optional 

150 Name of the fully-qualified name of the metric the specification 

151 corresponds to. This parameter is optional if ``name`` is already 

152 fully-qualified. 

153 package : `str` or `lsst.validate.base.Name`, optional 

154 Name of the package the specification corresponds to. This 

155 parameter is optional if ``name`` or ``metric`` are already 

156 fully-qualified. 

157 kwargs : `dict` 

158 Keyword arguments passed directly to the 

159 `lsst.validate.base.Specification` constructor. 

160 

161 Returns 

162 ------- 

163 specification : `ThresholdSpecification` 

164 A specification instance. 

165 """ 

166 _name = Name(metric=metric, spec=name) 

167 operator_str = threshold['operator'] 

168 _threshold = u.Quantity(threshold['value'], 

169 u.Unit(threshold['unit'])) 

170 return cls(_name, _threshold, operator_str, **kwargs) 

171 

172 def _serialize_type(self): 

173 """Serialize attributes of this specification type to a `dict` that is 

174 JSON-serializable. 

175 """ 

176 return JsonSerializationMixin.jsonify_dict( 

177 { 

178 'value': self.threshold.value, 

179 'unit': self.threshold.unit.to_string(), 

180 'operator': self.operator_str 

181 } 

182 ) 

183 

184 @property 

185 def operator_str(self): 

186 """Threshold comparision operator ('str'). 

187 

188 A measurement *passes* the specification if:: 

189 

190 measurement {{ operator }} threshold == True 

191 

192 The operator string is a standard Python binary comparison token, such 

193 as: ``'<'``, ``'>'``, ``'<='``, ``'>='``, ``'=='`` or ``'!='``. 

194 """ 

195 return self._operator_str 

196 

197 @operator_str.setter 

198 def operator_str(self, v): 

199 # Cache the operator function as a means of validating the input too 

200 self._operator = ThresholdSpecification.convert_operator_str(v) 

201 self._operator_str = v 

202 

203 @property 

204 def operator(self): 

205 """Binary comparision operator that tests success of a measurement 

206 fulfilling a specification of this metric. 

207 

208 Measured value is on left side of comparison and specification level 

209 is on right side. 

210 """ 

211 return self._operator 

212 

213 @staticmethod 

214 def convert_operator_str(op_str): 

215 """Convert a string representing a binary comparison operator to 

216 the operator function itself. 

217 

218 Operators are oriented so that the measurement is on the left-hand 

219 side, and specification threshold on the right hand side. 

220 

221 The following operators are permitted: 

222 

223 ========== ============= 

224 ``op_str`` Function 

225 ========== ============= 

226 ``>=`` `operator.ge` 

227 ``>`` `operator.gt` 

228 ``<`` `operator.lt` 

229 ``<=`` `operator.le` 

230 ``==`` `operator.eq` 

231 ``!=`` `operator.ne` 

232 ========== ============= 

233 

234 Parameters 

235 ---------- 

236 op_str : `str` 

237 A string representing a binary operator. 

238 

239 Returns 

240 ------- 

241 op_func : obj 

242 An operator function from the `operator` standard library 

243 module. 

244 

245 Raises 

246 ------ 

247 ValueError 

248 Raised if ``op_str`` is not a supported binary comparison operator. 

249 """ 

250 operators = {'>=': operator.ge, 

251 '>': operator.gt, 

252 '<': operator.lt, 

253 '<=': operator.le, 

254 '==': operator.eq, 

255 '!=': operator.ne} 

256 try: 

257 return operators[op_str] 

258 except KeyError: 

259 message = '{0!r} is not a supported threshold operator'.format( 

260 op_str) 

261 raise ValueError(message) 

262 

263 def check(self, measurement): 

264 """Check if a measurement passes this specification. 

265 

266 Parameters 

267 ---------- 

268 measurement : `astropy.units.Quantity` 

269 The measurement value. The measurement `~astropy.units.Quantity` 

270 must have units *compatible* with `threshold`. 

271 

272 Returns 

273 ------- 

274 passed : `bool` 

275 `True` if the measurement meets the specification, 

276 `False` otherwise. 

277 

278 Raises 

279 ------ 

280 astropy.units.UnitError 

281 Raised if the measurement cannot be compared to the threshold. 

282 For example, if the measurement is not an `astropy.units.Quantity` 

283 or if the units are not compatible. 

284 """ 

285 return self.operator(measurement, self.threshold)