Coverage for python/lsst/verify/metric.py: 46%

91 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-28 09:41 +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__ = ['Metric'] 

22 

23import astropy.units as u 

24 

25from .jsonmixin import JsonSerializationMixin 

26from .naming import Name 

27 

28 

29class Metric(JsonSerializationMixin): 

30 r"""Container for the definition of a metric. 

31 

32 Metrics can either be instantiated programatically, or from a metric YAML 

33 file through `lsst.verify.MetricSet`. 

34 

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

58 

59 description = None 

60 """Short description of the metric (`str`).""" 

61 

62 reference_doc = None 

63 """Name of the document that specifies this metric (`str`).""" 

64 

65 reference_url = None 

66 """URL of the document that specifies this metric (`str`).""" 

67 

68 reference_page = None 

69 """Page number in the document that specifies this metric (`int`).""" 

70 

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 

85 

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. 

90 

91 Parameters 

92 ---------- 

93 kwargs : `dict` 

94 Keyword arguments that match fields from the `Metric.json` 

95 serialization. 

96 

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 } 

111 

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) 

116 

117 return cls(name, **args) 

118 

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

125 

126 def __ne__(self, other): 

127 return not self.__eq__(other) 

128 

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) 

140 

141 @property 

142 def name(self): 

143 """Metric's name (`Name`).""" 

144 return self._name 

145 

146 @name.setter 

147 def name(self, value): 

148 self._name = Name(metric=value) 

149 

150 @property 

151 def unit(self): 

152 """The metric's unit (`astropy.units.Unit`).""" 

153 return self._unit 

154 

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 

165 

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) 

172 

173 @unit_str.setter 

174 def unit_str(self, value): 

175 self.unit = u.Unit(value) 

176 

177 @property 

178 def tags(self): 

179 """Tag labels (`set` of `str`).""" 

180 return self._tags 

181 

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) 

188 

189 @property 

190 def reference(self): 

191 """Documentation reference as human-readable text (`str`, read-only). 

192 

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 

202 

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 

207 

208 return ref_str 

209 

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}) 

225 

226 def check_unit(self, quantity): 

227 """Check that a `~astropy.units.Quantity` has equivalent units to 

228 this metric. 

229 

230 Parameters 

231 ---------- 

232 quantity : `astropy.units.Quantity` 

233 Quantity to be tested. 

234 

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