Coverage for python/lsst/verify/tasks/apdbMetricTask.py: 28%

96 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-15 02: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__ = ["ApdbMetricTask", "ApdbMetricConfig", "ConfigApdbLoader", 

23 "DirectApdbLoader", "ApdbMetricConnections"] 

24 

25import abc 

26import warnings 

27 

28from deprecated.sphinx import deprecated 

29 

30from lsst.pex.config import Config, ConfigurableField, Field, ConfigurableInstance, \ 

31 ConfigDictField, ConfigChoiceField, FieldValidationError 

32from lsst.pipe.base import NoWorkFound, Task, Struct, connectionTypes 

33from lsst.dax.apdb import Apdb, ApdbConfig 

34 

35from lsst.verify.tasks import MetricTask, MetricConfig, MetricConnections 

36 

37 

38@deprecated(reason="APDB loaders have been replaced by ``ApdbMetricConfig.apdb_config_url``. " 

39 "Will be removed after v28.", 

40 version="v28.0", category=FutureWarning) 

41class ConfigApdbLoader(Task): 

42 """A Task that takes a science task config and returns the corresponding 

43 Apdb object. 

44 

45 Parameters 

46 ---------- 

47 *args 

48 **kwargs 

49 Constructor parameters are the same as for `lsst.pipe.base.Task`. 

50 """ 

51 _DefaultName = "configApdb" 

52 ConfigClass = Config 

53 

54 def __init__(self, **kwargs): 

55 super().__init__(**kwargs) 

56 

57 def _getApdb(self, config): 

58 """Extract an Apdb object from an arbitrary task config. 

59 

60 Parameters 

61 ---------- 

62 config : `lsst.pex.config.Config` 

63 A config that may contain a `lsst.dax.apdb.ApdbConfig`. 

64 Behavior is undefined if there is more than one such member. 

65 

66 Returns 

67 ------- 

68 apdb : `lsst.dax.apdb.Apdb`-like or `None` 

69 A `lsst.dax.apdb.Apdb` object or a drop-in replacement, or `None` 

70 if no `lsst.dax.apdb.ApdbConfig` is present in ``config``. 

71 """ 

72 if isinstance(config, ApdbConfig): 

73 return Apdb.from_config(config) 

74 

75 for field in config.values(): 

76 if isinstance(field, ConfigurableInstance): 

77 result = self._getApdbFromConfigurableField(field) 

78 if result: 

79 return result 

80 elif isinstance(field, ConfigChoiceField.instanceDictClass): 

81 try: 

82 # can't test with hasattr because of non-standard getattr 

83 field.names 

84 except FieldValidationError: 

85 result = self._getApdb(field.active) 

86 else: 

87 result = self._getApdbFromConfigIterable(field.active) 

88 if result: 

89 return result 

90 elif isinstance(field, ConfigDictField.DictClass): 

91 result = self._getApdbFromConfigIterable(field.values()) 

92 if result: 

93 return result 

94 elif isinstance(field, Config): 

95 # Can't test for `ConfigField` more directly than this 

96 result = self._getApdb(field) 

97 if result: 

98 return result 

99 return None 

100 

101 def _getApdbFromConfigurableField(self, configurable): 

102 """Extract an Apdb object from a ConfigurableField. 

103 

104 Parameters 

105 ---------- 

106 configurable : `lsst.pex.config.ConfigurableInstance` 

107 A configurable that may contain a `lsst.dax.apdb.ApdbConfig`. 

108 

109 Returns 

110 ------- 

111 apdb : `lsst.dax.apdb.Apdb`-like or `None` 

112 A `lsst.dax.apdb.Apdb` object or a drop-in replacement, if a 

113 suitable config exists. 

114 """ 

115 if issubclass(configurable.ConfigClass, ApdbConfig): 

116 return configurable.apply() 

117 else: 

118 return self._getApdb(configurable.value) 

119 

120 def _getApdbFromConfigIterable(self, configDict): 

121 """Extract an Apdb object from an iterable of configs. 

122 

123 Parameters 

124 ---------- 

125 configDict: iterable of `lsst.pex.config.Config` 

126 A config iterable that may contain a `lsst.dax.apdb.ApdbConfig`. 

127 

128 Returns 

129 ------- 

130 apdb : `lsst.dax.apdb.Apdb`-like or `None` 

131 A `lsst.dax.apdb.Apdb` object or a drop-in replacement, if a 

132 suitable config exists. 

133 """ 

134 for config in configDict: 

135 result = self._getApdb(config) 

136 if result: 

137 return result 

138 

139 def run(self, config): 

140 """Create a database consistent with a science task config. 

141 

142 Parameters 

143 ---------- 

144 config : `lsst.pex.config.Config` 

145 A config that should contain a `lsst.dax.apdb.ApdbConfig`. 

146 Behavior is undefined if there is more than one such member. 

147 

148 Returns 

149 ------- 

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

151 Result struct with components: 

152 

153 ``apdb`` 

154 A database configured the same way as in ``config``, if one 

155 exists (`lsst.dax.apdb.Apdb` or `None`). 

156 """ 

157 return Struct(apdb=self._getApdb(config)) 

158 

159 

160# TODO: remove on DM-43419 

161class DirectApdbLoader(Task): 

162 """A Task that takes a Apdb config and returns the corresponding 

163 Apdb object. 

164 

165 Parameters 

166 ---------- 

167 *args 

168 **kwargs 

169 Constructor parameters are the same as for `lsst.pipe.base.Task`. 

170 """ 

171 

172 _DefaultName = "directApdb" 

173 ConfigClass = Config 

174 

175 def __init__(self, **kwargs): 

176 super().__init__(**kwargs) 

177 

178 def run(self, config): 

179 """Create a database from a config. 

180 

181 Parameters 

182 ---------- 

183 config : `lsst.dax.apdb.ApdbConfig` 

184 A config for the database connection. 

185 

186 Returns 

187 ------- 

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

189 Result struct with components: 

190 

191 ``apdb`` 

192 A database configured the same way as in ``config``. 

193 """ 

194 return Struct(apdb=(Apdb.from_config(config) if config else None)) 

195 

196 

197class ApdbMetricConnections( 

198 MetricConnections, 

199 dimensions={"instrument"}, 

200): 

201 """An abstract connections class defining a database input. 

202 

203 Notes 

204 ----- 

205 ``ApdbMetricConnections`` defines the following dataset templates: 

206 ``package`` 

207 Name of the metric's namespace. By 

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

209 the name of the package the metric is most closely 

210 associated with. 

211 ``metric`` 

212 Name of the metric, excluding any namespace. 

213 """ 

214 dbInfo = connectionTypes.Input( 

215 name="apdb_marker", 

216 doc="The dataset(s) indicating that AP processing has finished for a " 

217 "given data ID. If ``config.doReadMarker`` is set, the datasets " 

218 "are also used by ``dbLoader`` to construct an Apdb object.", 

219 storageClass="Config", 

220 multiple=True, 

221 minimum=1, 

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

223 ) 

224 # Replaces MetricConnections.measurement, which is detector-level 

225 measurement = connectionTypes.Output( 

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

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

228 storageClass="MetricValue", 

229 dimensions={"instrument"}, 

230 ) 

231 

232 

233class ApdbMetricConfig(MetricConfig, 

234 pipelineConnections=ApdbMetricConnections): 

235 """A base class for APDB metric task configs. 

236 """ 

237 dbLoader = ConfigurableField( # TODO: remove on DM-43419 

238 target=DirectApdbLoader, 

239 doc="Task for loading a database from ``dbInfo``. Its run method must " 

240 "take one object of the dataset type indicated by ``dbInfo`` and return " 

241 "a Struct with an 'apdb' member. Ignored if ``doReadMarker`` is unset.", 

242 deprecated="This field has been replaced by ``apdb_config_url``; set " 

243 "``doReadMarker=False`` to use it. Will be removed after v28.", 

244 ) 

245 apdb_config_url = Field( 

246 dtype=str, 

247 default=None, 

248 optional=False, 

249 doc="A config file specifying the APDB and its connection parameters, " 

250 "typically written by the apdb-cli command-line utility.", 

251 ) 

252 doReadMarker = Field( # TODO: remove on DM-43419 

253 dtype=bool, 

254 default=True, 

255 doc="Use the ``dbInfo`` input to set up the APDB, instead of the new " 

256 "config (``apdb_config_url``). This field is provided for " 

257 "backward-compatibility ONLY and will be removed without notice " 

258 "after v28.", 

259 ) 

260 

261 # TODO: remove on DM-43419 

262 def validate(self): 

263 # Sidestep Config.validate to avoid validating uninitialized 

264 # fields we're not using. 

265 skip = {"apdb_config_url"} if self.doReadMarker else set() 

266 for name, field in self._fields.items(): 

267 if name not in skip: 

268 field.validate(self) 

269 

270 # Copied from MetricConfig.validate 

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

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

273 "not contain periods") 

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

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

276 "not contain periods; use connections.package " 

277 "instead") 

278 

279 if self.doReadMarker: 

280 warnings.warn("The encoding of config information in apdbMarker is " 

281 "deprecated, replaced by ``apdb_config_url``; set " 

282 "``doReadMarker=False`` to use it. ``apdb_config_url`` " 

283 "will be required after v28.", 

284 FutureWarning) 

285 

286 

287class ApdbMetricTask(MetricTask): 

288 """A base class for tasks that compute metrics from an alert production 

289 database. 

290 

291 Parameters 

292 ---------- 

293 **kwargs 

294 Constructor parameters are the same as for 

295 `lsst.pipe.base.PipelineTask`. 

296 

297 Notes 

298 ----- 

299 This class should be customized by overriding `makeMeasurement`. You 

300 should not need to override `run`. 

301 """ 

302 # Design note: makeMeasurement is an overrideable method rather than a 

303 # subtask to keep the configs for `MetricsControllerTask` as simple as 

304 # possible. This was judged more important than ensuring that no 

305 # implementation details of MetricTask can leak into 

306 # application-specific code. 

307 

308 ConfigClass = ApdbMetricConfig 

309 

310 def __init__(self, **kwargs): 

311 super().__init__(**kwargs) 

312 

313 if self.config.doReadMarker: 

314 self.makeSubtask("dbLoader") 

315 

316 @abc.abstractmethod 

317 def makeMeasurement(self, dbHandle, outputDataId): 

318 """Compute the metric from database data. 

319 

320 Parameters 

321 ---------- 

322 dbHandle : `lsst.dax.apdb.Apdb` 

323 A database instance. 

324 outputDataId : any data ID type 

325 The subset of the database to which this measurement applies. 

326 May be empty to represent the entire dataset. 

327 

328 Returns 

329 ------- 

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

331 The measurement corresponding to the input data. 

332 

333 Raises 

334 ------ 

335 lsst.verify.tasks.MetricComputationError 

336 Raised if an algorithmic or system error prevents calculation of 

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

338 lsst.pipe.base.NoWorkFound 

339 Raised if the metric is ill-defined or otherwise inapplicable to 

340 the database state. Typically this means that the pipeline step or 

341 option being measured was not run. 

342 """ 

343 

344 def run(self, dbInfo, outputDataId={}): 

345 """Compute a measurement from a database. 

346 

347 Parameters 

348 ---------- 

349 dbInfo : `list` 

350 The datasets (of the type indicated by the config) from 

351 which to load the database. If more than one dataset is provided 

352 (as may be the case if DB writes are fine-grained), all are 

353 assumed identical. 

354 outputDataId: any data ID type, optional 

355 The output data ID for the metric value. Defaults to the empty ID, 

356 representing a value that covers the entire dataset. 

357 

358 Returns 

359 ------- 

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

361 Result struct with component: 

362 

363 ``measurement`` 

364 the value of the metric (`lsst.verify.Measurement` or `None`) 

365 

366 Raises 

367 ------ 

368 lsst.verify.tasks.MetricComputationError 

369 Raised if an algorithmic or system error prevents calculation of 

370 the metric. 

371 lsst.pipe.base.NoWorkFound 

372 Raised if the metric is ill-defined or otherwise inapplicable to 

373 the database state. Typically this means that the pipeline step or 

374 option being measured was not run. 

375 

376 Notes 

377 ----- 

378 This implementation calls 

379 `~lsst.verify.tasks.ApdbMetricConfig.dbLoader` to acquire a database 

380 handle, then passes it and the value of 

381 ``outputDataId`` to `makeMeasurement`. The result of `makeMeasurement` 

382 is returned to the caller. 

383 """ 

384 if self.config.doReadMarker: 

385 db = self.dbLoader.run(dbInfo[0]).apdb 

386 else: 

387 db = Apdb.from_uri(self.config.apdb_config_url) 

388 

389 if db is not None: 

390 return Struct(measurement=self.makeMeasurement(db, outputDataId)) 

391 else: 

392 raise NoWorkFound("No APDB to measure!") 

393 

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

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

396 

397 This specialization of runQuantum passes the output data ID to `run`. 

398 """ 

399 inputs = butlerQC.get(inputRefs) 

400 outputs = self.run(**inputs, 

401 outputDataId=outputRefs.measurement.dataId) 

402 if outputs.measurement is not None: 

403 butlerQC.put(outputs, outputRefs) 

404 else: 

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

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