Coverage for python/lsst/verify/tasks/metadataMetricTask.py: 45%
47 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-03 01:31 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-07-03 01:31 -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` or name of
55 the `~lsst.pipe.base.CmdLineTask` whose metadata 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`.
124 This class makes no assumptions about how to handle missing data;
125 `run` may be called with `None` values, and is responsible
126 for deciding how to deal with them.
127 """
128 # Design note: getInputMetadataKeys and MetadataMetricTask.makeMeasurement
129 # are overrideable methods rather than subtask(s) to keep the configs for
130 # `MetricsControllerTask` as simple as possible. This was judged more
131 # important than ensuring that no implementation details of MetricTask
132 # can leak into application-specific code.
134 @classmethod
135 @abc.abstractmethod
136 def getInputMetadataKeys(cls, config):
137 """Return the metadata keys read by this task.
139 Parameters
140 ----------
141 config : ``cls.ConfigClass``
142 Configuration for this task.
144 Returns
145 -------
146 keys : `dict` [`str`, `str`]
147 The keys are the (arbitrary) names of values to use in task code,
148 the values are the metadata keys to be looked up (see the
149 ``metadataKeys`` parameter to `extractMetadata`). Metadata keys are
150 assumed to include task prefixes in the
151 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may
152 return a substring of the desired (full) key, but the string must
153 match a unique metadata key.
154 """
156 @staticmethod
157 def _searchKeys(metadata, keyFragment):
158 """Search the metadata for all keys matching a substring.
160 Parameters
161 ----------
162 metadata : `lsst.pipe.base.TaskMetadata`
163 A metadata object with task-qualified keys as returned by
164 `lsst.pipe.base.Task.getFullMetadata()`.
165 keyFragment : `str`
166 A substring for a full metadata key.
168 Returns
169 -------
170 keys : `set` of `str`
171 All keys in ``metadata`` that have ``keyFragment`` as a substring.
172 """
173 keys = metadata.paramNames(topLevelOnly=False)
174 return {key for key in keys if keyFragment in key}
176 @staticmethod
177 def extractMetadata(metadata, metadataKeys):
178 """Read multiple keys from a metadata object.
180 Parameters
181 ----------
182 metadata : `lsst.pipe.base.TaskMetadata`
183 A metadata object, assumed not `None`.
184 metadataKeys : `dict` [`str`, `str`]
185 Keys are arbitrary labels, values are metadata keys (or their
186 substrings) in the format of
187 `lsst.pipe.base.Task.getFullMetadata()`.
189 Returns
190 -------
191 metadataValues : `dict` [`str`, any]
192 Keys are the same as for ``metadataKeys``, values are the value of
193 each metadata key, or `None` if no matching key was found.
195 Raises
196 ------
197 lsst.verify.tasks.MetricComputationError
198 Raised if any metadata key string has more than one match
199 in ``metadata``.
200 """
201 data = {}
202 for dataName, keyFragment in metadataKeys.items():
203 matchingKeys = MetadataMetricTask._searchKeys(
204 metadata, keyFragment)
205 if len(matchingKeys) == 1:
206 key, = matchingKeys
207 data[dataName] = metadata.getScalar(key)
208 elif not matchingKeys:
209 data[dataName] = None
210 else:
211 error = "String %s matches multiple metadata keys: %s" \
212 % (keyFragment, matchingKeys)
213 raise MetricComputationError(error)
214 return data
217class MetadataMetricTask(AbstractMetadataMetricTask):
218 """A base class for tasks that compute metrics from single metadata
219 objects.
221 Parameters
222 ----------
223 *args
224 **kwargs
225 Constructor parameters are the same as for
226 `lsst.pipe.base.PipelineTask`.
228 Notes
229 -----
230 This class should be customized by overriding `getInputMetadataKeys`
231 and `makeMeasurement`. You should not need to override `run`.
233 This class makes no assumptions about how to handle missing data;
234 `makeMeasurement` may be called with `None` values, and is responsible
235 for deciding how to deal with them.
236 """
237 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
238 # methods rather than subtask(s) to keep the configs for
239 # `MetricsControllerTask` as simple as possible. This was judged more
240 # important than ensuring that no implementation details of MetricTask
241 # can leak into application-specific code.
243 ConfigClass = MetadataMetricConfig
245 @abc.abstractmethod
246 def makeMeasurement(self, values):
247 """Compute the metric given the values of the metadata.
249 Parameters
250 ----------
251 values : `dict` [`str`, any]
252 A `dict` representation of the metadata passed to `run`. It has the
253 same keys as returned by `getInputMetadataKeys`, and maps them to
254 the values extracted from the metadata. Any value may be `None` to
255 represent missing data.
257 Returns
258 -------
259 measurement : `lsst.verify.Measurement` or `None`
260 The measurement corresponding to the input data.
262 Raises
263 ------
264 lsst.verify.tasks.MetricComputationError
265 Raised if an algorithmic or system error prevents calculation of
266 the metric. See `run` for expected behavior.
267 """
269 def run(self, metadata):
270 """Compute a measurement from science task metadata.
272 Parameters
273 ----------
274 metadata : `lsst.pipe.base.TaskMetadata` or `None`
275 A metadata object for the unit of science processing to use for
276 this metric, or a collection of such objects if this task combines
277 many units of processing into a single metric.
279 Returns
280 -------
281 result : `lsst.pipe.base.Struct`
282 A `~lsst.pipe.base.Struct` containing the following component:
284 - ``measurement``: the value of the metric
285 (`lsst.verify.Measurement` or `None`)
287 Raises
288 ------
289 lsst.verify.tasks.MetricComputationError
290 Raised if the strings returned by `getInputMetadataKeys` match
291 more than one key in any metadata object.
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 if metadata is not None:
303 data = self.extractMetadata(metadata, metadataKeys)
304 else:
305 data = {dataName: None for dataName in metadataKeys}
307 return Struct(measurement=self.makeMeasurement(data))