Coverage for python/lsst/verify/tasks/metadataMetricTask.py: 38%

47 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 20:30 +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 

22__all__ = ["AbstractMetadataMetricTask", 

23 "MetadataMetricTask", "MetadataMetricConfig", 

24 "SingleMetadataMetricConnections"] 

25 

26import abc 

27 

28import lsst.pex.config 

29 

30from lsst.pipe.base import Struct, connectionTypes 

31from lsst.verify.tasks import MetricTask, MetricConfig, MetricConnections, \ 

32 MetricComputationError 

33 

34 

35class SingleMetadataMetricConnections( 

36 MetricConnections, 

37 dimensions={"instrument", "visit", "detector"}, 

38 defaultTemplates={"labelName": "", "package": None, "metric": None}): 

39 """An abstract connections class defining a metadata input. 

40 

41 Notes 

42 ----- 

43 ``SingleMetadataMetricConnections`` defines the following dataset 

44 templates: 

45 

46 ``package`` 

47 Name of the metric's namespace. By 

48 :ref:`verify_metrics <verify-metrics-package>` convention, this is 

49 the name of the package the metric is most closely 

50 associated with. 

51 ``metric`` 

52 Name of the metric, excluding any namespace. 

53 ``labelName`` 

54 Pipeline label of the `~lsst.pipe.base.PipelineTask` or name of 

55 the `~lsst.pipe.base.CmdLineTask` whose metadata are being read. 

56 """ 

57 metadata = connectionTypes.Input( 

58 name="{labelName}_metadata", 

59 doc="The target top-level task's metadata. The name must be set to " 

60 "the metadata's butler type, such as 'processCcd_metadata'.", 

61 storageClass="PropertySet", 

62 dimensions={"instrument", "visit", "detector"}, 

63 multiple=False, 

64 ) 

65 

66 def __init__(self, *, config=None): 

67 """Customize the connections for a specific MetricTask instance. 

68 

69 Parameters 

70 ---------- 

71 config : `MetadataMetricConfig` 

72 A config for `MetadataMetricTask` or one of its subclasses. 

73 """ 

74 super().__init__(config=config) 

75 if config and config.metadataDimensions != self.metadata.dimensions: 

76 # Hack, but only way to get a connection without fixed dimensions 

77 newMetadata = connectionTypes.Input( 

78 name=self.metadata.name, 

79 doc=self.metadata.doc, 

80 storageClass=self.metadata.storageClass, 

81 dimensions=config.metadataDimensions, 

82 multiple=self.metadata.multiple, 

83 ) 

84 self.metadata = newMetadata 

85 # Registry must match actual connections 

86 self.allConnections['metadata'] = self.metadata 

87 # Task requires that quantum dimensions match input dimensions 

88 self.dimensions = config.metadataDimensions 

89 

90 

91class MetadataMetricConfig( 

92 MetricConfig, 

93 pipelineConnections=SingleMetadataMetricConnections): 

94 """A base class for metadata metric task configs. 

95 """ 

96 metadataDimensions = lsst.pex.config.ListField( 

97 # Sort to ensure default order is consistent between runs 

98 default=sorted(SingleMetadataMetricConnections.dimensions), 

99 dtype=str, 

100 doc="Override for the dimensions of the 'metadata' input, when " 

101 "instrumenting Tasks that don't produce one metadata object " 

102 "per visit.", 

103 ) 

104 

105 

106class AbstractMetadataMetricTask(MetricTask): 

107 """A base class for tasks that compute metrics from metadata values. 

108 

109 This class contains code that is agnostic to whether the input is one 

110 metadata object or many. 

111 

112 Parameters 

113 ---------- 

114 *args 

115 **kwargs 

116 Constructor parameters are the same as for 

117 `lsst.pipe.base.PipelineTask`. 

118 

119 Notes 

120 ----- 

121 This class should be customized by overriding `getInputMetadataKeys` 

122 and `run`. 

123 

124 This class makes no assumptions about how to handle missing data; 

125 `run` may be called with `None` values, and is responsible 

126 for deciding how to deal with them. 

127 """ 

128 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement 

129 # are overrideable methods rather than subtask(s) to keep the configs for 

130 # `MetricsControllerTask` as simple as possible. This was judged more 

131 # important than ensuring that no implementation details of MetricTask 

132 # can leak into application-specific code. 

133 

134 @classmethod 

135 @abc.abstractmethod 

136 def getInputMetadataKeys(cls, config): 

137 """Return the metadata keys read by this task. 

138 

139 Parameters 

140 ---------- 

141 config : ``cls.ConfigClass`` 

142 Configuration for this task. 

143 

144 Returns 

145 ------- 

146 keys : `dict` [`str`, `str`] 

147 The keys are the (arbitrary) names of values to use in task code, 

148 the values are the metadata keys to be looked up (see the 

149 ``metadataKeys`` parameter to `extractMetadata`). Metadata keys are 

150 assumed to include task prefixes in the 

151 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may 

152 return a substring of the desired (full) key, but the string must 

153 match a unique metadata key. 

154 """ 

155 

156 @staticmethod 

157 def _searchKeys(metadata, keyFragment): 

158 """Search the metadata for all keys matching a substring. 

159 

160 Parameters 

161 ---------- 

162 metadata : `lsst.daf.base.PropertySet` 

163 A metadata object with task-qualified keys as returned by 

164 `lsst.pipe.base.Task.getFullMetadata()`. 

165 keyFragment : `str` 

166 A substring for a full metadata key. 

167 

168 Returns 

169 ------- 

170 keys : `set` of `str` 

171 All keys in ``metadata`` that have ``keyFragment`` as a substring. 

172 """ 

173 keys = metadata.paramNames(topLevelOnly=False) 

174 return {key for key in keys if keyFragment in key} 

175 

176 @staticmethod 

177 def extractMetadata(metadata, metadataKeys): 

178 """Read multiple keys from a metadata object. 

179 

180 Parameters 

181 ---------- 

182 metadata : `lsst.daf.base.PropertySet` 

183 A metadata object, assumed not `None`. 

184 metadataKeys : `dict` [`str`, `str`] 

185 Keys are arbitrary labels, values are metadata keys (or their 

186 substrings) in the format of 

187 `lsst.pipe.base.Task.getFullMetadata()`. 

188 

189 Returns 

190 ------- 

191 metadataValues : `dict` [`str`, any] 

192 Keys are the same as for ``metadataKeys``, values are the value of 

193 each metadata key, or `None` if no matching key was found. 

194 

195 Raises 

196 ------ 

197 lsst.verify.tasks.MetricComputationError 

198 Raised if any metadata key string has more than one match 

199 in ``metadata``. 

200 """ 

201 data = {} 

202 for dataName, keyFragment in metadataKeys.items(): 

203 matchingKeys = MetadataMetricTask._searchKeys( 

204 metadata, keyFragment) 

205 if len(matchingKeys) == 1: 

206 key, = matchingKeys 

207 data[dataName] = metadata.getScalar(key) 

208 elif not matchingKeys: 

209 data[dataName] = None 

210 else: 

211 error = "String %s matches multiple metadata keys: %s" \ 

212 % (keyFragment, matchingKeys) 

213 raise MetricComputationError(error) 

214 return data 

215 

216 

217class MetadataMetricTask(AbstractMetadataMetricTask): 

218 """A base class for tasks that compute metrics from single metadata objects. 

219 

220 Parameters 

221 ---------- 

222 *args 

223 **kwargs 

224 Constructor parameters are the same as for 

225 `lsst.pipe.base.PipelineTask`. 

226 

227 Notes 

228 ----- 

229 This class should be customized by overriding `getInputMetadataKeys` 

230 and `makeMeasurement`. You should not need to override `run`. 

231 

232 This class makes no assumptions about how to handle missing data; 

233 `makeMeasurement` may be called with `None` values, and is responsible 

234 for deciding how to deal with them. 

235 """ 

236 # Design note: getInputMetadataKeys and makeMeasurement are overrideable 

237 # methods rather than subtask(s) to keep the configs for 

238 # `MetricsControllerTask` as simple as possible. This was judged more 

239 # important than ensuring that no implementation details of MetricTask 

240 # can leak into application-specific code. 

241 

242 ConfigClass = MetadataMetricConfig 

243 

244 @abc.abstractmethod 

245 def makeMeasurement(self, values): 

246 """Compute the metric given the values of the metadata. 

247 

248 Parameters 

249 ---------- 

250 values : `dict` [`str`, any] 

251 A `dict` representation of the metadata passed to `run`. It has the 

252 same keys as returned by `getInputMetadataKeys`, and maps them to 

253 the values extracted from the metadata. Any value may be `None` to 

254 represent missing data. 

255 

256 Returns 

257 ------- 

258 measurement : `lsst.verify.Measurement` or `None` 

259 The measurement corresponding to the input data. 

260 

261 Raises 

262 ------ 

263 lsst.verify.tasks.MetricComputationError 

264 Raised if an algorithmic or system error prevents calculation of 

265 the metric. See `run` for expected behavior. 

266 """ 

267 

268 def run(self, metadata): 

269 """Compute a measurement from science task metadata. 

270 

271 Parameters 

272 ---------- 

273 metadata : `lsst.daf.base.PropertySet` or `None` 

274 A metadata object for the unit of science processing to use for 

275 this metric, or a collection of such objects if this task combines 

276 many units of processing into a single metric. 

277 

278 Returns 

279 ------- 

280 result : `lsst.pipe.base.Struct` 

281 A `~lsst.pipe.base.Struct` containing the following component: 

282 

283 - ``measurement``: the value of the metric 

284 (`lsst.verify.Measurement` or `None`) 

285 

286 Raises 

287 ------ 

288 lsst.verify.tasks.MetricComputationError 

289 Raised if the strings returned by `getInputMetadataKeys` match 

290 more than one key in any metadata object. 

291 

292 Notes 

293 ----- 

294 This implementation calls `getInputMetadataKeys`, then searches for 

295 matching keys in each metadata. It then passes the values of these 

296 keys (or `None` if no match) to `makeMeasurement`, and returns its 

297 result to the caller. 

298 """ 

299 metadataKeys = self.getInputMetadataKeys(self.config) 

300 

301 if metadata is not None: 

302 data = self.extractMetadata(metadata, metadataKeys) 

303 else: 

304 data = {dataName: None for dataName in metadataKeys} 

305 

306 return Struct(measurement=self.makeMeasurement(data))