Coverage for python/lsst/verify/tasks/metadataMetricTask.py: 38%
47 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 20:30 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 20:30 +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` 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="PropertySet",
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.daf.base.PropertySet`
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.daf.base.PropertySet`
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 objects.
220 Parameters
221 ----------
222 *args
223 **kwargs
224 Constructor parameters are the same as for
225 `lsst.pipe.base.PipelineTask`.
227 Notes
228 -----
229 This class should be customized by overriding `getInputMetadataKeys`
230 and `makeMeasurement`. You should not need to override `run`.
232 This class makes no assumptions about how to handle missing data;
233 `makeMeasurement` may be called with `None` values, and is responsible
234 for deciding how to deal with them.
235 """
236 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
237 # methods rather than subtask(s) to keep the configs for
238 # `MetricsControllerTask` as simple as possible. This was judged more
239 # important than ensuring that no implementation details of MetricTask
240 # can leak into application-specific code.
242 ConfigClass = MetadataMetricConfig
244 @abc.abstractmethod
245 def makeMeasurement(self, values):
246 """Compute the metric given the values of the metadata.
248 Parameters
249 ----------
250 values : `dict` [`str`, any]
251 A `dict` representation of the metadata passed to `run`. It has the
252 same keys as returned by `getInputMetadataKeys`, and maps them to
253 the values extracted from the metadata. Any value may be `None` to
254 represent missing data.
256 Returns
257 -------
258 measurement : `lsst.verify.Measurement` or `None`
259 The measurement corresponding to the input data.
261 Raises
262 ------
263 lsst.verify.tasks.MetricComputationError
264 Raised if an algorithmic or system error prevents calculation of
265 the metric. See `run` for expected behavior.
266 """
268 def run(self, metadata):
269 """Compute a measurement from science task metadata.
271 Parameters
272 ----------
273 metadata : `lsst.daf.base.PropertySet` or `None`
274 A metadata object for the unit of science processing to use for
275 this metric, or a collection of such objects if this task combines
276 many units of processing into a single metric.
278 Returns
279 -------
280 result : `lsst.pipe.base.Struct`
281 A `~lsst.pipe.base.Struct` containing the following component:
283 - ``measurement``: the value of the metric
284 (`lsst.verify.Measurement` or `None`)
286 Raises
287 ------
288 lsst.verify.tasks.MetricComputationError
289 Raised if the strings returned by `getInputMetadataKeys` match
290 more than one key in any metadata object.
292 Notes
293 -----
294 This implementation calls `getInputMetadataKeys`, then searches for
295 matching keys in each metadata. It then passes the values of these
296 keys (or `None` if no match) to `makeMeasurement`, and returns its
297 result to the caller.
298 """
299 metadataKeys = self.getInputMetadataKeys(self.config)
301 if metadata is not None:
302 data = self.extractMetadata(metadata, metadataKeys)
303 else:
304 data = {dataName: None for dataName in metadataKeys}
306 return Struct(measurement=self.makeMeasurement(data))