Coverage for python/lsst/verify/tasks/apdbMetricTask.py: 28%
96 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 10:05 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-16 10:05 +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/>.
22__all__ = ["ApdbMetricTask", "ApdbMetricConfig", "ConfigApdbLoader",
23 "DirectApdbLoader", "ApdbMetricConnections"]
25import abc
26import warnings
28from deprecated.sphinx import deprecated
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
35from lsst.verify.tasks import MetricTask, MetricConfig, MetricConnections
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.
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
54 def __init__(self, **kwargs):
55 super().__init__(**kwargs)
57 def _getApdb(self, config):
58 """Extract an Apdb object from an arbitrary task config.
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.
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)
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
101 def _getApdbFromConfigurableField(self, configurable):
102 """Extract an Apdb object from a ConfigurableField.
104 Parameters
105 ----------
106 configurable : `lsst.pex.config.ConfigurableInstance`
107 A configurable that may contain a `lsst.dax.apdb.ApdbConfig`.
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)
120 def _getApdbFromConfigIterable(self, configDict):
121 """Extract an Apdb object from an iterable of configs.
123 Parameters
124 ----------
125 configDict: iterable of `lsst.pex.config.Config`
126 A config iterable that may contain a `lsst.dax.apdb.ApdbConfig`.
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
139 def run(self, config):
140 """Create a database consistent with a science task config.
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.
148 Returns
149 -------
150 result : `lsst.pipe.base.Struct`
151 Result struct with components:
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))
160# TODO: remove on DM-43419
161class DirectApdbLoader(Task):
162 """A Task that takes a Apdb config and returns the corresponding
163 Apdb object.
165 Parameters
166 ----------
167 *args
168 **kwargs
169 Constructor parameters are the same as for `lsst.pipe.base.Task`.
170 """
172 _DefaultName = "directApdb"
173 ConfigClass = Config
175 def __init__(self, **kwargs):
176 super().__init__(**kwargs)
178 def run(self, config):
179 """Create a database from a config.
181 Parameters
182 ----------
183 config : `lsst.dax.apdb.ApdbConfig`
184 A config for the database connection.
186 Returns
187 -------
188 result : `lsst.pipe.base.Struct`
189 Result struct with components:
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))
197class ApdbMetricConnections(
198 MetricConnections,
199 dimensions={"instrument"},
200):
201 """An abstract connections class defining a database input.
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 )
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 )
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)
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")
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)
287class ApdbMetricTask(MetricTask):
288 """A base class for tasks that compute metrics from an alert production
289 database.
291 Parameters
292 ----------
293 **kwargs
294 Constructor parameters are the same as for
295 `lsst.pipe.base.PipelineTask`.
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.
308 ConfigClass = ApdbMetricConfig
310 def __init__(self, **kwargs):
311 super().__init__(**kwargs)
313 if self.config.doReadMarker:
314 self.makeSubtask("dbLoader")
316 @abc.abstractmethod
317 def makeMeasurement(self, dbHandle, outputDataId):
318 """Compute the metric from database data.
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.
328 Returns
329 -------
330 measurement : `lsst.verify.Measurement` or `None`
331 The measurement corresponding to the input data.
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 """
344 def run(self, dbInfo, outputDataId={}):
345 """Compute a measurement from a database.
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.
358 Returns
359 -------
360 result : `lsst.pipe.base.Struct`
361 Result struct with component:
363 ``measurement``
364 the value of the metric (`lsst.verify.Measurement` or `None`)
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.
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)
389 if db is not None:
390 return Struct(measurement=self.makeMeasurement(db, outputDataId))
391 else:
392 raise NoWorkFound("No APDB to measure!")
394 def runQuantum(self, butlerQC, inputRefs, outputRefs):
395 """Do Butler I/O to provide in-memory objects for run.
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)