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

44 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-28 03:06 -0700

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` whose metadata 

55 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="TaskMetadata", 

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 self.dimensions.clear() 

77 self.dimensions.update(config.metadataDimensions) 

78 self.metadata = connectionTypes.Input( 

79 name=self.metadata.name, 

80 doc=self.metadata.doc, 

81 storageClass=self.metadata.storageClass, 

82 dimensions=frozenset(config.metadataDimensions), 

83 multiple=self.metadata.multiple, 

84 ) 

85 

86 

87class MetadataMetricConfig( 

88 MetricConfig, 

89 pipelineConnections=SingleMetadataMetricConnections): 

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

91 """ 

92 metadataDimensions = lsst.pex.config.ListField( 

93 # Sort to ensure default order is consistent between runs 

94 default=sorted(SingleMetadataMetricConnections.dimensions), 

95 dtype=str, 

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

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

98 "per visit.", 

99 ) 

100 

101 

102class AbstractMetadataMetricTask(MetricTask): 

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

104 

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

106 metadata object or many. 

107 

108 Parameters 

109 ---------- 

110 *args 

111 **kwargs 

112 Constructor parameters are the same as for 

113 `lsst.pipe.base.PipelineTask`. 

114 

115 Notes 

116 ----- 

117 This class should be customized by overriding `getInputMetadataKeys` 

118 and `run`. 

119 """ 

120 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement 

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

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

123 # important than ensuring that no implementation details of MetricTask 

124 # can leak into application-specific code. 

125 

126 @classmethod 

127 @abc.abstractmethod 

128 def getInputMetadataKeys(cls, config): 

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

130 

131 Parameters 

132 ---------- 

133 config : ``cls.ConfigClass`` 

134 Configuration for this task. 

135 

136 Returns 

137 ------- 

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

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

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

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

142 assumed to include task prefixes in the 

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

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

145 match a unique metadata key. 

146 """ 

147 

148 @staticmethod 

149 def _searchKeys(metadata, keyFragment): 

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

151 

152 Parameters 

153 ---------- 

154 metadata : `lsst.pipe.base.TaskMetadata` 

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

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

157 keyFragment : `str` 

158 A substring for a full metadata key. 

159 

160 Returns 

161 ------- 

162 keys : `set` of `str` 

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

164 """ 

165 keys = metadata.paramNames(topLevelOnly=False) 

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

167 

168 @staticmethod 

169 def extractMetadata(metadata, metadataKeys): 

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

171 

172 Parameters 

173 ---------- 

174 metadata : `lsst.pipe.base.TaskMetadata` 

175 A metadata object. 

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

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

178 substrings) in the format of 

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

180 

181 Returns 

182 ------- 

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

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

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

186 

187 Raises 

188 ------ 

189 lsst.verify.tasks.MetricComputationError 

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

191 in ``metadata``. 

192 """ 

193 data = {} 

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

195 matchingKeys = MetadataMetricTask._searchKeys( 

196 metadata, keyFragment) 

197 if len(matchingKeys) == 1: 

198 key, = matchingKeys 

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

200 elif not matchingKeys: 

201 data[dataName] = None 

202 else: 

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

204 % (keyFragment, matchingKeys) 

205 raise MetricComputationError(error) 

206 return data 

207 

208 

209class MetadataMetricTask(AbstractMetadataMetricTask): 

210 """A base class for tasks that compute metrics from single metadata 

211 objects. 

212 

213 Parameters 

214 ---------- 

215 *args 

216 **kwargs 

217 Constructor parameters are the same as for 

218 `lsst.pipe.base.PipelineTask`. 

219 

220 Notes 

221 ----- 

222 This class should be customized by overriding `getInputMetadataKeys` 

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

224 """ 

225 # Design note: getInputMetadataKeys and makeMeasurement are overrideable 

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

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

228 # important than ensuring that no implementation details of MetricTask 

229 # can leak into application-specific code. 

230 

231 ConfigClass = MetadataMetricConfig 

232 

233 @abc.abstractmethod 

234 def makeMeasurement(self, values): 

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

236 

237 Parameters 

238 ---------- 

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

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

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

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

243 represent missing data. 

244 

245 Returns 

246 ------- 

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

248 The measurement corresponding to the input data. 

249 

250 Raises 

251 ------ 

252 lsst.verify.tasks.MetricComputationError 

253 Raised if an algorithmic or system error prevents calculation of 

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

255 lsst.pipe.base.NoWorkFound 

256 Raised if the metric is ill-defined or otherwise inapplicable. 

257 Typically this means that the pipeline step or option being 

258 measured was not run. 

259 """ 

260 

261 def run(self, metadata): 

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

263 

264 Parameters 

265 ---------- 

266 metadata : `lsst.pipe.base.TaskMetadata` 

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

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

269 many units of processing into a single metric. 

270 

271 Returns 

272 ------- 

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

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

275 

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

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

278 

279 Raises 

280 ------ 

281 lsst.verify.tasks.MetricComputationError 

282 Raised if the strings returned by `getInputMetadataKeys` match 

283 more than one key in any metadata object. 

284 lsst.pipe.base.NoWorkFound 

285 Raised if the metric is ill-defined or otherwise inapplicable. 

286 Typically this means that the pipeline step or option being 

287 measured was not run. 

288 

289 Notes 

290 ----- 

291 This implementation calls `getInputMetadataKeys`, then searches for 

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

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

294 result to the caller. 

295 """ 

296 metadataKeys = self.getInputMetadataKeys(self.config) 

297 

298 data = self.extractMetadata(metadata, metadataKeys) 

299 

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