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

45 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-08 10:57 +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` 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 # 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 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement 

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

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

127 # important than ensuring that no implementation details of MetricTask 

128 # can leak into application-specific code. 

129 

130 @classmethod 

131 @abc.abstractmethod 

132 def getInputMetadataKeys(cls, config): 

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

134 

135 Parameters 

136 ---------- 

137 config : ``cls.ConfigClass`` 

138 Configuration for this task. 

139 

140 Returns 

141 ------- 

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

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

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

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

146 assumed to include task prefixes in the 

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

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

149 match a unique metadata key. 

150 """ 

151 

152 @staticmethod 

153 def _searchKeys(metadata, keyFragment): 

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

155 

156 Parameters 

157 ---------- 

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

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

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

161 keyFragment : `str` 

162 A substring for a full metadata key. 

163 

164 Returns 

165 ------- 

166 keys : `set` of `str` 

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

168 """ 

169 keys = metadata.paramNames(topLevelOnly=False) 

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

171 

172 @staticmethod 

173 def extractMetadata(metadata, metadataKeys): 

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

175 

176 Parameters 

177 ---------- 

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

179 A metadata object. 

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

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

182 substrings) in the format of 

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

184 

185 Returns 

186 ------- 

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

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

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

190 

191 Raises 

192 ------ 

193 lsst.verify.tasks.MetricComputationError 

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

195 in ``metadata``. 

196 """ 

197 data = {} 

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

199 matchingKeys = MetadataMetricTask._searchKeys( 

200 metadata, keyFragment) 

201 if len(matchingKeys) == 1: 

202 key, = matchingKeys 

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

204 elif not matchingKeys: 

205 data[dataName] = None 

206 else: 

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

208 % (keyFragment, matchingKeys) 

209 raise MetricComputationError(error) 

210 return data 

211 

212 

213class MetadataMetricTask(AbstractMetadataMetricTask): 

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

215 objects. 

216 

217 Parameters 

218 ---------- 

219 *args 

220 **kwargs 

221 Constructor parameters are the same as for 

222 `lsst.pipe.base.PipelineTask`. 

223 

224 Notes 

225 ----- 

226 This class should be customized by overriding `getInputMetadataKeys` 

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

228 """ 

229 # Design note: getInputMetadataKeys and makeMeasurement are overrideable 

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

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

232 # important than ensuring that no implementation details of MetricTask 

233 # can leak into application-specific code. 

234 

235 ConfigClass = MetadataMetricConfig 

236 

237 @abc.abstractmethod 

238 def makeMeasurement(self, values): 

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

240 

241 Parameters 

242 ---------- 

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

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

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

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

247 represent missing data. 

248 

249 Returns 

250 ------- 

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

252 The measurement corresponding to the input data. 

253 

254 Raises 

255 ------ 

256 lsst.verify.tasks.MetricComputationError 

257 Raised if an algorithmic or system error prevents calculation of 

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

259 lsst.pipe.base.NoWorkFound 

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

261 Typically this means that the pipeline step or option being 

262 measured was not run. 

263 """ 

264 

265 def run(self, metadata): 

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

267 

268 Parameters 

269 ---------- 

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

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

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

273 many units of processing into a single metric. 

274 

275 Returns 

276 ------- 

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

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

279 

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

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

282 

283 Raises 

284 ------ 

285 lsst.verify.tasks.MetricComputationError 

286 Raised if the strings returned by `getInputMetadataKeys` match 

287 more than one key in any metadata object. 

288 lsst.pipe.base.NoWorkFound 

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

290 Typically this means that the pipeline step or option being 

291 measured was not run. 

292 

293 Notes 

294 ----- 

295 This implementation calls `getInputMetadataKeys`, then searches for 

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

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

298 result to the caller. 

299 """ 

300 metadataKeys = self.getInputMetadataKeys(self.config) 

301 

302 data = self.extractMetadata(metadata, metadataKeys) 

303 

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