Coverage for python/lsst/verify/tasks/metricTask.py: 48%

49 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-11 02:59 -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 

23__all__ = ["MetricComputationError", "MetricTask", "MetricConfig", 

24 "MetricConnections"] 

25 

26 

27import abc 

28 

29import lsst.pipe.base as pipeBase 

30from lsst.pipe.base import connectionTypes 

31 

32from lsst.verify import Name 

33 

34 

35class MetricComputationError(RuntimeError): 

36 """This class represents unresolvable errors in computing a metric. 

37 

38 `lsst.verify.tasks.MetricTask` raises ``MetricComputationError`` 

39 instead of other data- or processing-related exceptions to let code that 

40 calls a mix of data processing and metric tasks distinguish between 

41 the two. Therefore, most ``MetricComputationError`` instances should be 

42 chained to another exception representing the underlying problem. 

43 """ 

44 pass 

45 

46 

47class MetricConnections(pipeBase.PipelineTaskConnections, 

48 defaultTemplates={"package": None, "metric": None}, 

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

50 ): 

51 """An abstract connections class defining a metric output. 

52 

53 This class assumes detector-level metrics, which is the most common case. 

54 Subclasses can redeclare ``measurement`` and ``dimensions`` to override 

55 this assumption. 

56 

57 Notes 

58 ----- 

59 ``MetricConnections`` defines the following dataset templates: 

60 ``package`` 

61 Name of the metric's namespace. By 

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

63 the name of the package the metric is most closely 

64 associated with. 

65 ``metric`` 

66 Name of the metric, excluding any namespace. 

67 """ 

68 measurement = connectionTypes.Output( 

69 name="metricvalue_{package}_{metric}", 

70 doc="The metric value computed by this task.", 

71 storageClass="MetricValue", 

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

73 ) 

74 

75 

76class MetricConfig(pipeBase.PipelineTaskConfig, 

77 pipelineConnections=MetricConnections): 

78 

79 def validate(self): 

80 super().validate() 

81 

82 if "." in self.connections.package: 

83 raise ValueError(f"package name {self.connections.package} must " 

84 "not contain periods") 

85 if "." in self.connections.metric: 

86 raise ValueError(f"metric name {self.connections.metric} must " 

87 "not contain periods; use connections.package " 

88 "instead") 

89 

90 @property 

91 def metricName(self): 

92 """The metric calculated by a `MetricTask` with this config 

93 (`lsst.verify.Name`, read-only). 

94 """ 

95 return Name(package=self.connections.package, 

96 metric=self.connections.metric) 

97 

98 

99class MetricTask(pipeBase.PipelineTask, metaclass=abc.ABCMeta): 

100 """A base class for tasks that compute one metric from input datasets. 

101 

102 Parameters 

103 ---------- 

104 *args 

105 **kwargs 

106 Constructor parameters are the same as for 

107 `lsst.pipe.base.PipelineTask`. 

108 

109 Notes 

110 ----- 

111 In general, both the ``MetricTask``'s metric and its input data are 

112 configurable. Metrics may be associated with a data ID at any level of 

113 granularity, including repository-wide. 

114 

115 Like `lsst.pipe.base.PipelineTask`, this class should be customized by 

116 overriding `run` and by providing a `lsst.pipe.base.connectionTypes.Input` 

117 for each parameter of `run`. For requirements that are specific to 

118 ``MetricTask``, see `run`. 

119 """ 

120 

121 ConfigClass = MetricConfig 

122 

123 def __init__(self, **kwargs): 

124 super().__init__(**kwargs) 

125 

126 @abc.abstractmethod 

127 def run(self, **kwargs): 

128 """Run the MetricTask on in-memory data. 

129 

130 Parameters 

131 ---------- 

132 **kwargs 

133 Keyword arguments matching the inputs given in the class config; 

134 see `lsst.pipe.base.PipelineTask.run` for more details. 

135 

136 Returns 

137 ------- 

138 struct : `lsst.pipe.base.Struct` 

139 A `~lsst.pipe.base.Struct` containing at least the 

140 following component: 

141 

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

143 (`lsst.verify.Measurement` or `None`). This method is not 

144 responsible for adding mandatory metadata (e.g., the data ID); 

145 this is handled by the caller. 

146 

147 Raises 

148 ------ 

149 lsst.verify.tasks.MetricComputationError 

150 Raised if an algorithmic or system error prevents calculation 

151 of the metric. Examples include corrupted input data or 

152 unavoidable exceptions raised by analysis code. The 

153 `~lsst.verify.tasks.MetricComputationError` should be chained to a 

154 more specific exception describing the root cause. 

155 

156 Not having enough data for a metric to be applicable is not an 

157 error, and should not trigger this exception. 

158 

159 Notes 

160 ----- 

161 All input data must be treated as optional. This maximizes the 

162 ``MetricTask``'s usefulness for incomplete pipeline runs or runs with 

163 optional processing steps. If a metric cannot be calculated because 

164 the necessary inputs are missing, the ``MetricTask`` must return `None` 

165 in place of the measurement. 

166 """ 

167 

168 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

169 """Do Butler I/O to provide in-memory objects for run. 

170 

171 This specialization of runQuantum performs error-handling specific to 

172 MetricTasks. Most or all of this functionality may be moved to 

173 activators in the future. 

174 """ 

175 # Synchronize changes to this method with ApdbMetricTask 

176 try: 

177 inputs = butlerQC.get(inputRefs) 

178 outputs = self.run(**inputs) 

179 if outputs.measurement is not None: 

180 butlerQC.put(outputs, outputRefs) 

181 else: 

182 self.log.debug("Skipping measurement of %r on %s " 

183 "as not applicable.", self, inputRefs) 

184 except MetricComputationError: 

185 self.log.error( 

186 "Measurement of %r failed on %s->%s", 

187 self, inputRefs, outputRefs, exc_info=True) 

188 

189 def adaptArgsAndRun(self, inputData, inputDataIds, outputDataId): 

190 """A wrapper around `run` used by 

191 `~lsst.verify.gen2tasks.MetricsControllerTask`. 

192 

193 Task developers should not need to call or override this method. 

194 

195 Parameters 

196 ---------- 

197 inputData : `dict` from `str` to any 

198 Dictionary whose keys are the names of input parameters and values 

199 are Python-domain data objects (or lists of objects) retrieved 

200 from data butler. Input objects may be `None` to represent 

201 missing data. 

202 inputDataIds : `dict` from `str` to `list` of dataId 

203 Dictionary whose keys are the names of input parameters and values 

204 are data IDs (or lists of data IDs) that the task consumes for 

205 corresponding dataset type. Data IDs are guaranteed to match data 

206 objects in ``inputData``. 

207 outputDataId : `dict` from `str` to dataId 

208 Dictionary containing a single key, ``"measurement"``, which maps 

209 to a single data ID for the measurement. The data ID must have the 

210 same granularity as the metric. 

211 

212 Returns 

213 ------- 

214 struct : `lsst.pipe.base.Struct` 

215 A `~lsst.pipe.base.Struct` containing at least the 

216 following component: 

217 

218 - ``measurement``: the value of the metric, computed from 

219 ``inputData`` (`lsst.verify.Measurement` or `None`). The 

220 measurement is guaranteed to contain not only the value of the 

221 metric, but also any mandatory supplementary information. 

222 

223 Raises 

224 ------ 

225 lsst.verify.tasks.MetricComputationError 

226 Raised if an algorithmic or system error prevents calculation 

227 of the metric. Examples include corrupted input data or 

228 unavoidable exceptions raised by analysis code. The 

229 `~lsst.verify.tasks.MetricComputationError` should be chained to a 

230 more specific exception describing the root cause. 

231 

232 Not having enough data for a metric to be applicable is not an 

233 error, and should not trigger this exception. 

234 

235 Notes 

236 ----- 

237 This implementation calls `run` on the contents of ``inputData``, 

238 followed by calling `addStandardMetadata` on the result before 

239 returning it. 

240 

241 Examples 

242 -------- 

243 Consider a metric that characterizes PSF variations across the entire 

244 field of view, given processed images. Then, if `run` has the 

245 signature ``run(images)``: 

246 

247 .. code-block:: py 

248 

249 inputData = {'images': [image1, image2, ...]} 

250 inputDataIds = {'images': [{'visit': 42, 'ccd': 1}, 

251 {'visit': 42, 'ccd': 2}, 

252 ...]} 

253 outputDataId = {'measurement': {'visit': 42}} 

254 result = task.adaptArgsAndRun( 

255 inputData, inputDataIds, outputDataId) 

256 """ 

257 result = self.run(**inputData) 

258 if result.measurement is not None: 

259 self.addStandardMetadata(result.measurement, 

260 outputDataId["measurement"]) 

261 return result 

262 

263 @classmethod 

264 def getInputDatasetTypes(cls, config): 

265 """Return input dataset types for this task. 

266 

267 Parameters 

268 ---------- 

269 config : ``cls.ConfigClass`` 

270 Configuration for this task. 

271 

272 Returns 

273 ------- 

274 datasets : `dict` from `str` to `str` 

275 Dictionary where the key is the name of the input dataset (must 

276 match a parameter to `run`) and the value is the name of its 

277 Butler dataset type. 

278 

279 Notes 

280 ----- 

281 The default implementation extracts a 

282 `~lsst.pipe.base.PipelineTaskConnections` object from ``config``. 

283 """ 

284 # Get connections from config for backward-compatibility 

285 connections = config.connections.ConnectionsClass(config=config) 

286 return {name: getattr(connections, name).name 

287 for name in connections.inputs} 

288 

289 @classmethod 

290 def areInputDatasetsScalar(cls, config): 

291 """Return input dataset multiplicity. 

292 

293 Parameters 

294 ---------- 

295 config : ``cls.ConfigClass`` 

296 Configuration for this task. 

297 

298 Returns 

299 ------- 

300 datasets : `Dict` [`str`, `bool`] 

301 Dictionary where the key is the name of the input dataset (must 

302 match a parameter to `run`) and the value is `True` if `run` takes 

303 only one object and `False` if it takes a list. 

304 

305 Notes 

306 ----- 

307 The default implementation extracts a 

308 `~lsst.pipe.base.PipelineTaskConnections` object from ``config``. 

309 """ 

310 connections = config.connections.ConnectionsClass(config=config) 

311 return {name: not getattr(connections, name).multiple 

312 for name in connections.inputs} 

313 

314 def addStandardMetadata(self, measurement, outputDataId): 

315 """Add data ID-specific metadata required for all metrics. 

316 

317 This method currently does not add any metadata, but may do so 

318 in the future. 

319 

320 Parameters 

321 ---------- 

322 measurement : `lsst.verify.Measurement` 

323 The `~lsst.verify.Measurement` that the metadata are added to. 

324 outputDataId : ``dataId`` 

325 The data ID to which the measurement applies, at the appropriate 

326 level of granularity. 

327 

328 Notes 

329 ----- 

330 This method should not be overridden by subclasses. 

331 

332 This method is not responsible for shared metadata like the execution 

333 environment (which should be added by this ``MetricTask``'s caller), 

334 nor for metadata specific to a particular metric (which should be 

335 added when the metric is calculated). 

336 

337 .. warning:: 

338 This method's signature will change whenever additional data needs 

339 to be provided. This is a deliberate restriction to ensure that all 

340 subclasses pass in the new data as well. 

341 """ 

342 pass