Coverage for python/lsst/verify/tasks/metadataMetricTask.py: 53%
44 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-09 09:47 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-09 09:47 +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__ = ["AbstractMetadataMetricTask",
23 "MetadataMetricTask", "MetadataMetricConfig",
24 "SingleMetadataMetricConnections"]
26import abc
28import lsst.pex.config
30from lsst.pipe.base import Struct, connectionTypes
31from lsst.verify.tasks import MetricTask, MetricConfig, MetricConnections, \
32 MetricComputationError
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.
41 Notes
42 -----
43 ``SingleMetadataMetricConnections`` defines the following dataset
44 templates:
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 )
66 def __init__(self, *, config=None):
67 """Customize the connections for a specific MetricTask instance.
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 self.dimensions.clear()
77 self.dimensions.update(config.metadataDimensions)
78 self.metadata = connectionTypes.Input(
79 name=self.metadata.name,
80 doc=self.metadata.doc,
81 storageClass=self.metadata.storageClass,
82 dimensions=frozenset(config.metadataDimensions),
83 multiple=self.metadata.multiple,
84 )
87class MetadataMetricConfig(
88 MetricConfig,
89 pipelineConnections=SingleMetadataMetricConnections):
90 """A base class for metadata metric task configs.
91 """
92 metadataDimensions = lsst.pex.config.ListField(
93 # Sort to ensure default order is consistent between runs
94 default=sorted(SingleMetadataMetricConnections.dimensions),
95 dtype=str,
96 doc="Override for the dimensions of the 'metadata' input, when "
97 "instrumenting Tasks that don't produce one metadata object "
98 "per visit.",
99 )
102class AbstractMetadataMetricTask(MetricTask):
103 """A base class for tasks that compute metrics from metadata values.
105 This class contains code that is agnostic to whether the input is one
106 metadata object or many.
108 Parameters
109 ----------
110 *args
111 **kwargs
112 Constructor parameters are the same as for
113 `lsst.pipe.base.PipelineTask`.
115 Notes
116 -----
117 This class should be customized by overriding `getInputMetadataKeys`
118 and `run`.
119 """
120 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement
121 # are overrideable methods rather than subtask(s) to keep the configs for
122 # `MetricsControllerTask` as simple as possible. This was judged more
123 # important than ensuring that no implementation details of MetricTask
124 # can leak into application-specific code.
126 @classmethod
127 @abc.abstractmethod
128 def getInputMetadataKeys(cls, config):
129 """Return the metadata keys read by this task.
131 Parameters
132 ----------
133 config : ``cls.ConfigClass``
134 Configuration for this task.
136 Returns
137 -------
138 keys : `dict` [`str`, `str`]
139 The keys are the (arbitrary) names of values to use in task code,
140 the values are the metadata keys to be looked up (see the
141 ``metadataKeys`` parameter to `extractMetadata`). Metadata keys are
142 assumed to include task prefixes in the
143 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may
144 return a substring of the desired (full) key, but the string must
145 match a unique metadata key.
146 """
148 @staticmethod
149 def _searchKeys(metadata, keyFragment):
150 """Search the metadata for all keys matching a substring.
152 Parameters
153 ----------
154 metadata : `lsst.pipe.base.TaskMetadata`
155 A metadata object with task-qualified keys as returned by
156 `lsst.pipe.base.Task.getFullMetadata()`.
157 keyFragment : `str`
158 A substring for a full metadata key.
160 Returns
161 -------
162 keys : `set` of `str`
163 All keys in ``metadata`` that have ``keyFragment`` as a substring.
164 """
165 keys = metadata.paramNames(topLevelOnly=False)
166 return {key for key in keys if keyFragment in key}
168 @staticmethod
169 def extractMetadata(metadata, metadataKeys):
170 """Read multiple keys from a metadata object.
172 Parameters
173 ----------
174 metadata : `lsst.pipe.base.TaskMetadata`
175 A metadata object.
176 metadataKeys : `dict` [`str`, `str`]
177 Keys are arbitrary labels, values are metadata keys (or their
178 substrings) in the format of
179 `lsst.pipe.base.Task.getFullMetadata()`.
181 Returns
182 -------
183 metadataValues : `dict` [`str`, any]
184 Keys are the same as for ``metadataKeys``, values are the value of
185 each metadata key, or `None` if no matching key was found.
187 Raises
188 ------
189 lsst.verify.tasks.MetricComputationError
190 Raised if any metadata key string has more than one match
191 in ``metadata``.
192 """
193 data = {}
194 for dataName, keyFragment in metadataKeys.items():
195 matchingKeys = MetadataMetricTask._searchKeys(
196 metadata, keyFragment)
197 if len(matchingKeys) == 1:
198 key, = matchingKeys
199 data[dataName] = metadata.getScalar(key)
200 elif not matchingKeys:
201 data[dataName] = None
202 else:
203 error = "String %s matches multiple metadata keys: %s" \
204 % (keyFragment, matchingKeys)
205 raise MetricComputationError(error)
206 return data
209class MetadataMetricTask(AbstractMetadataMetricTask):
210 """A base class for tasks that compute metrics from single metadata
211 objects.
213 Parameters
214 ----------
215 *args
216 **kwargs
217 Constructor parameters are the same as for
218 `lsst.pipe.base.PipelineTask`.
220 Notes
221 -----
222 This class should be customized by overriding `getInputMetadataKeys`
223 and `makeMeasurement`. You should not need to override `run`.
224 """
225 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
226 # methods rather than subtask(s) to keep the configs for
227 # `MetricsControllerTask` as simple as possible. This was judged more
228 # important than ensuring that no implementation details of MetricTask
229 # can leak into application-specific code.
231 ConfigClass = MetadataMetricConfig
233 @abc.abstractmethod
234 def makeMeasurement(self, values):
235 """Compute the metric given the values of the metadata.
237 Parameters
238 ----------
239 values : `dict` [`str`, any]
240 A `dict` representation of the metadata passed to `run`. It has the
241 same keys as returned by `getInputMetadataKeys`, and maps them to
242 the values extracted from the metadata. Any value may be `None` to
243 represent missing data.
245 Returns
246 -------
247 measurement : `lsst.verify.Measurement` or `None`
248 The measurement corresponding to the input data.
250 Raises
251 ------
252 lsst.verify.tasks.MetricComputationError
253 Raised if an algorithmic or system error prevents calculation of
254 the metric. See `run` for expected behavior.
255 lsst.pipe.base.NoWorkFound
256 Raised if the metric is ill-defined or otherwise inapplicable.
257 Typically this means that the pipeline step or option being
258 measured was not run.
259 """
261 def run(self, metadata):
262 """Compute a measurement from science task metadata.
264 Parameters
265 ----------
266 metadata : `lsst.pipe.base.TaskMetadata`
267 A metadata object for the unit of science processing to use for
268 this metric, or a collection of such objects if this task combines
269 many units of processing into a single metric.
271 Returns
272 -------
273 result : `lsst.pipe.base.Struct`
274 A `~lsst.pipe.base.Struct` containing the following component:
276 - ``measurement``: the value of the metric
277 (`lsst.verify.Measurement` or `None`)
279 Raises
280 ------
281 lsst.verify.tasks.MetricComputationError
282 Raised if the strings returned by `getInputMetadataKeys` match
283 more than one key in any metadata object.
284 lsst.pipe.base.NoWorkFound
285 Raised if the metric is ill-defined or otherwise inapplicable.
286 Typically this means that the pipeline step or option being
287 measured was not run.
289 Notes
290 -----
291 This implementation calls `getInputMetadataKeys`, then searches for
292 matching keys in each metadata. It then passes the values of these
293 keys (or `None` if no match) to `makeMeasurement`, and returns its
294 result to the caller.
295 """
296 metadataKeys = self.getInputMetadataKeys(self.config)
298 data = self.extractMetadata(metadata, metadataKeys)
300 return Struct(measurement=self.makeMeasurement(data))