Coverage for python/lsst/verify/tasks/metadataMetricTask.py: 42%
45 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 02:21 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-04-14 02:21 -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/>.
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 # 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
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 )
106class AbstractMetadataMetricTask(MetricTask):
107 """A base class for tasks that compute metrics from metadata values.
109 This class contains code that is agnostic to whether the input is one
110 metadata object or many.
112 Parameters
113 ----------
114 *args
115 **kwargs
116 Constructor parameters are the same as for
117 `lsst.pipe.base.PipelineTask`.
119 Notes
120 -----
121 This class should be customized by overriding `getInputMetadataKeys`
122 and `run`.
123 """
124 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement
125 # are overrideable methods rather than subtask(s) to keep the configs for
126 # `MetricsControllerTask` as simple as possible. This was judged more
127 # important than ensuring that no implementation details of MetricTask
128 # can leak into application-specific code.
130 @classmethod
131 @abc.abstractmethod
132 def getInputMetadataKeys(cls, config):
133 """Return the metadata keys read by this task.
135 Parameters
136 ----------
137 config : ``cls.ConfigClass``
138 Configuration for this task.
140 Returns
141 -------
142 keys : `dict` [`str`, `str`]
143 The keys are the (arbitrary) names of values to use in task code,
144 the values are the metadata keys to be looked up (see the
145 ``metadataKeys`` parameter to `extractMetadata`). Metadata keys are
146 assumed to include task prefixes in the
147 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may
148 return a substring of the desired (full) key, but the string must
149 match a unique metadata key.
150 """
152 @staticmethod
153 def _searchKeys(metadata, keyFragment):
154 """Search the metadata for all keys matching a substring.
156 Parameters
157 ----------
158 metadata : `lsst.pipe.base.TaskMetadata`
159 A metadata object with task-qualified keys as returned by
160 `lsst.pipe.base.Task.getFullMetadata()`.
161 keyFragment : `str`
162 A substring for a full metadata key.
164 Returns
165 -------
166 keys : `set` of `str`
167 All keys in ``metadata`` that have ``keyFragment`` as a substring.
168 """
169 keys = metadata.paramNames(topLevelOnly=False)
170 return {key for key in keys if keyFragment in key}
172 @staticmethod
173 def extractMetadata(metadata, metadataKeys):
174 """Read multiple keys from a metadata object.
176 Parameters
177 ----------
178 metadata : `lsst.pipe.base.TaskMetadata`
179 A metadata object.
180 metadataKeys : `dict` [`str`, `str`]
181 Keys are arbitrary labels, values are metadata keys (or their
182 substrings) in the format of
183 `lsst.pipe.base.Task.getFullMetadata()`.
185 Returns
186 -------
187 metadataValues : `dict` [`str`, any]
188 Keys are the same as for ``metadataKeys``, values are the value of
189 each metadata key, or `None` if no matching key was found.
191 Raises
192 ------
193 lsst.verify.tasks.MetricComputationError
194 Raised if any metadata key string has more than one match
195 in ``metadata``.
196 """
197 data = {}
198 for dataName, keyFragment in metadataKeys.items():
199 matchingKeys = MetadataMetricTask._searchKeys(
200 metadata, keyFragment)
201 if len(matchingKeys) == 1:
202 key, = matchingKeys
203 data[dataName] = metadata.getScalar(key)
204 elif not matchingKeys:
205 data[dataName] = None
206 else:
207 error = "String %s matches multiple metadata keys: %s" \
208 % (keyFragment, matchingKeys)
209 raise MetricComputationError(error)
210 return data
213class MetadataMetricTask(AbstractMetadataMetricTask):
214 """A base class for tasks that compute metrics from single metadata
215 objects.
217 Parameters
218 ----------
219 *args
220 **kwargs
221 Constructor parameters are the same as for
222 `lsst.pipe.base.PipelineTask`.
224 Notes
225 -----
226 This class should be customized by overriding `getInputMetadataKeys`
227 and `makeMeasurement`. You should not need to override `run`.
228 """
229 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
230 # methods rather than subtask(s) to keep the configs for
231 # `MetricsControllerTask` as simple as possible. This was judged more
232 # important than ensuring that no implementation details of MetricTask
233 # can leak into application-specific code.
235 ConfigClass = MetadataMetricConfig
237 @abc.abstractmethod
238 def makeMeasurement(self, values):
239 """Compute the metric given the values of the metadata.
241 Parameters
242 ----------
243 values : `dict` [`str`, any]
244 A `dict` representation of the metadata passed to `run`. It has the
245 same keys as returned by `getInputMetadataKeys`, and maps them to
246 the values extracted from the metadata. Any value may be `None` to
247 represent missing data.
249 Returns
250 -------
251 measurement : `lsst.verify.Measurement` or `None`
252 The measurement corresponding to the input data.
254 Raises
255 ------
256 lsst.verify.tasks.MetricComputationError
257 Raised if an algorithmic or system error prevents calculation of
258 the metric. See `run` for expected behavior.
259 lsst.pipe.base.NoWorkFound
260 Raised if the metric is ill-defined or otherwise inapplicable.
261 Typically this means that the pipeline step or option being
262 measured was not run.
263 """
265 def run(self, metadata):
266 """Compute a measurement from science task metadata.
268 Parameters
269 ----------
270 metadata : `lsst.pipe.base.TaskMetadata`
271 A metadata object for the unit of science processing to use for
272 this metric, or a collection of such objects if this task combines
273 many units of processing into a single metric.
275 Returns
276 -------
277 result : `lsst.pipe.base.Struct`
278 A `~lsst.pipe.base.Struct` containing the following component:
280 - ``measurement``: the value of the metric
281 (`lsst.verify.Measurement` or `None`)
283 Raises
284 ------
285 lsst.verify.tasks.MetricComputationError
286 Raised if the strings returned by `getInputMetadataKeys` match
287 more than one key in any metadata object.
288 lsst.pipe.base.NoWorkFound
289 Raised if the metric is ill-defined or otherwise inapplicable.
290 Typically this means that the pipeline step or option being
291 measured was not run.
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)
302 data = self.extractMetadata(metadata, metadataKeys)
304 return Struct(measurement=self.makeMeasurement(data))