Coverage for python/lsst/verify/tasks/metadataMetricTask.py : 33%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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__ = ["MetadataMetricTask", "MetadataMetricConfig",
23 "SingleMetadataMetricConnections"]
25import abc
27import lsst.pex.config
29from lsst.pipe.base import Struct, connectionTypes
30from lsst.verify.tasks import MetricTask, MetricConfig, MetricConnections, \
31 MetricComputationError
34class SingleMetadataMetricConnections(
35 MetricConnections,
36 dimensions={"instrument", "visit", "detector"},
37 defaultTemplates={"labelName": "", "package": None, "metric": None}):
38 """An abstract connections class defining a metadata input.
40 Notes
41 -----
42 ``SingleMetadataMetricConnections`` defines the following dataset
43 templates:
45 ``package``
46 Name of the metric's namespace. By
47 :ref:`verify_metrics <verify-metrics-package>` convention, this is
48 the name of the package the metric is most closely
49 associated with.
50 ``metric``
51 Name of the metric, excluding any namespace.
52 ``labelName``
53 Pipeline label of the `~lsst.pipe.base.PipelineTask` or name of
54 the `~lsst.pipe.base.CmdLineTask` whose metadata are being read.
55 """
56 metadata = connectionTypes.Input(
57 name="{labelName}_metadata",
58 doc="The target top-level task's metadata. The name must be set to "
59 "the metadata's butler type, such as 'processCcd_metadata'.",
60 storageClass="PropertySet",
61 dimensions={"instrument", "visit", "detector"},
62 multiple=False,
63 )
65 def __init__(self, *, config=None):
66 """Customize the connections for a specific MetricTask instance.
68 Parameters
69 ----------
70 config : `MetadataMetricConfig`
71 A config for `MetadataMetricTask` or one of its subclasses.
72 """
73 super().__init__(config=config)
74 if config and config.metadataDimensions != self.metadata.dimensions:
75 # Hack, but only way to get a connection without fixed dimensions
76 newMetadata = connectionTypes.Input(
77 name=self.metadata.name,
78 doc=self.metadata.doc,
79 storageClass=self.metadata.storageClass,
80 dimensions=config.metadataDimensions,
81 multiple=self.metadata.multiple,
82 )
83 self.metadata = newMetadata
84 # Registry must match actual connections
85 self.allConnections['metadata'] = self.metadata
86 # Task requires that quantum dimensions match input dimensions
87 self.dimensions = config.metadataDimensions
90class MetadataMetricConfig(
91 MetricConfig,
92 pipelineConnections=SingleMetadataMetricConnections):
93 """A base class for metadata metric task configs.
94 """
95 metadataDimensions = lsst.pex.config.ListField(
96 default=SingleMetadataMetricConnections.dimensions,
97 dtype=str,
98 doc="Override for the dimensions of the 'metadata' input, when "
99 "instrumenting Tasks that don't produce one metadata object "
100 "per visit.",
101 )
104class _AbstractMetadataMetricTask(MetricTask):
105 """A base class for tasks that compute metrics from metadata values.
107 This class contains code that is agnostic to whether the input is one
108 metadata object or many.
110 Parameters
111 ----------
112 *args
113 **kwargs
114 Constructor parameters are the same as for
115 `lsst.pipe.base.PipelineTask`.
117 Notes
118 -----
119 This class should be customized by overriding `getInputMetadataKeys`
120 and `makeMeasurement`. You should not need to override `run`.
122 This class makes no assumptions about how to handle missing data;
123 `makeMeasurement` may be called with `None` values, and is responsible
124 for deciding how to deal with them.
125 """
126 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
127 # methods rather than subtask(s) to keep the configs for
128 # `MetricsControllerTask` as simple as possible. This was judged more
129 # important than ensuring that no implementation details of MetricTask
130 # can leak into application-specific code.
132 @classmethod
133 @abc.abstractmethod
134 def getInputMetadataKeys(cls, config):
135 """Return the metadata keys read by this task.
137 Parameters
138 ----------
139 config : ``cls.ConfigClass``
140 Configuration for this task.
142 Returns
143 -------
144 keys : `dict` [`str`, `str`]
145 The keys are the (arbitrary) names of values needed by
146 `makeMeasurement`, the values are the metadata keys to be looked
147 up. Metadata keys are assumed to include task prefixes in the
148 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may
149 return a substring of the desired (full) key, but multiple matches
150 for any key will cause an error.
151 """
153 @staticmethod
154 def _searchKeys(metadata, keyFragment):
155 """Search the metadata for all keys matching a substring.
157 Parameters
158 ----------
159 metadata : `lsst.daf.base.PropertySet`
160 A metadata object with task-qualified keys as returned by
161 `lsst.pipe.base.Task.getFullMetadata()`.
162 keyFragment : `str`
163 A substring for a full metadata key.
165 Returns
166 -------
167 keys : `set` of `str`
168 All keys in ``metadata`` that have ``keyFragment`` as a substring.
169 """
170 keys = metadata.paramNames(topLevelOnly=False)
171 return {key for key in keys if keyFragment in key}
173 @staticmethod
174 def _extractMetadata(metadata, metadataKeys):
175 """Read multiple keys from a metadata object.
177 Parameters
178 ----------
179 metadata : `lsst.daf.base.PropertySet`
180 A metadata object, assumed not `None`.
181 metadataKeys : `dict` [`str`, `str`]
182 Keys are arbitrary labels, values are metadata keys (or their
183 substrings) in the format of
184 `lsst.pipe.base.Task.getFullMetadata()`.
186 Returns
187 -------
188 metadataValues : `dict` [`str`, any]
189 Keys are the same as for ``metadataKeys``, values are the value of
190 each metadata key, or `None` if no matching key was found.
192 Raises
193 ------
194 lsst.verify.tasks.MetricComputationError
195 Raised if any metadata key string has more than one match
196 in ``metadata``.
197 """
198 data = {}
199 for dataName, keyFragment in metadataKeys.items():
200 matchingKeys = MetadataMetricTask._searchKeys(
201 metadata, keyFragment)
202 if len(matchingKeys) == 1:
203 key, = matchingKeys
204 data[dataName] = metadata.getScalar(key)
205 elif not matchingKeys:
206 data[dataName] = None
207 else:
208 error = "String %s matches multiple metadata keys: %s" \
209 % (keyFragment, matchingKeys)
210 raise MetricComputationError(error)
211 return data
214class MetadataMetricTask(_AbstractMetadataMetricTask):
215 """A base class for tasks that compute metrics from single metadata 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`.
229 This class makes no assumptions about how to handle missing data;
230 `makeMeasurement` may be called with `None` values, and is responsible
231 for deciding how to deal with them.
232 """
233 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
234 # methods rather than subtask(s) to keep the configs for
235 # `MetricsControllerTask` as simple as possible. This was judged more
236 # important than ensuring that no implementation details of MetricTask
237 # can leak into application-specific code.
239 ConfigClass = MetadataMetricConfig
241 @abc.abstractmethod
242 def makeMeasurement(self, values):
243 """Compute the metric given the values of the metadata.
245 Parameters
246 ----------
247 values : `dict` [`str`, any]
248 A `dict` representation of the metadata passed to `run`. It has the
249 same keys as returned by `getInputMetadataKeys`, and maps them to
250 the values extracted from the metadata. Any value may be `None` to
251 represent missing data.
253 Returns
254 -------
255 measurement : `lsst.verify.Measurement` or `None`
256 The measurement corresponding to the input data.
258 Raises
259 ------
260 lsst.verify.tasks.MetricComputationError
261 Raised if an algorithmic or system error prevents calculation of
262 the metric. See `run` for expected behavior.
263 """
265 def run(self, metadata):
266 """Compute a measurement from science task metadata.
268 Parameters
269 ----------
270 metadata : `lsst.daf.base.PropertySet` or `None`
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.
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 if metadata is not None:
299 data = self._extractMetadata(metadata, metadataKeys)
300 else:
301 data = {dataName: None for dataName in metadataKeys}
303 return Struct(measurement=self.makeMeasurement(data))