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

47 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-21 03:05 -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` 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="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 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.pipe.base.TaskMetadata` 

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.pipe.base.TaskMetadata` 

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 

219 objects. 

220 

221 Parameters 

222 ---------- 

223 *args 

224 **kwargs 

225 Constructor parameters are the same as for 

226 `lsst.pipe.base.PipelineTask`. 

227 

228 Notes 

229 ----- 

230 This class should be customized by overriding `getInputMetadataKeys` 

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

232 

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

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

235 for deciding how to deal with them. 

236 """ 

237 # Design note: getInputMetadataKeys and makeMeasurement are overrideable 

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

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

240 # important than ensuring that no implementation details of MetricTask 

241 # can leak into application-specific code. 

242 

243 ConfigClass = MetadataMetricConfig 

244 

245 @abc.abstractmethod 

246 def makeMeasurement(self, values): 

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

248 

249 Parameters 

250 ---------- 

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

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

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

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

255 represent missing data. 

256 

257 Returns 

258 ------- 

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

260 The measurement corresponding to the input data. 

261 

262 Raises 

263 ------ 

264 lsst.verify.tasks.MetricComputationError 

265 Raised if an algorithmic or system error prevents calculation of 

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

267 """ 

268 

269 def run(self, metadata): 

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

271 

272 Parameters 

273 ---------- 

274 metadata : `lsst.pipe.base.TaskMetadata` or `None` 

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

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

277 many units of processing into a single metric. 

278 

279 Returns 

280 ------- 

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

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

283 

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

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

286 

287 Raises 

288 ------ 

289 lsst.verify.tasks.MetricComputationError 

290 Raised if the strings returned by `getInputMetadataKeys` match 

291 more than one key in any metadata object. 

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 if metadata is not None: 

303 data = self.extractMetadata(metadata, metadataKeys) 

304 else: 

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

306 

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