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

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 # Sort to ensure default order is consistent between runs
97 default=sorted(SingleMetadataMetricConnections.dimensions),
98 dtype=str,
99 doc="Override for the dimensions of the 'metadata' input, when "
100 "instrumenting Tasks that don't produce one metadata object "
101 "per visit.",
102 )
105class _AbstractMetadataMetricTask(MetricTask):
106 """A base class for tasks that compute metrics from metadata values.
108 This class contains code that is agnostic to whether the input is one
109 metadata object or many.
111 Parameters
112 ----------
113 *args
114 **kwargs
115 Constructor parameters are the same as for
116 `lsst.pipe.base.PipelineTask`.
118 Notes
119 -----
120 This class should be customized by overriding `getInputMetadataKeys`
121 and `makeMeasurement`. You should not need to override `run`.
123 This class makes no assumptions about how to handle missing data;
124 `makeMeasurement` may be called with `None` values, and is responsible
125 for deciding how to deal with them.
126 """
127 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
128 # methods rather than subtask(s) to keep the configs for
129 # `MetricsControllerTask` as simple as possible. This was judged more
130 # important than ensuring that no implementation details of MetricTask
131 # can leak into application-specific code.
133 @classmethod
134 @abc.abstractmethod
135 def getInputMetadataKeys(cls, config):
136 """Return the metadata keys read by this task.
138 Parameters
139 ----------
140 config : ``cls.ConfigClass``
141 Configuration for this task.
143 Returns
144 -------
145 keys : `dict` [`str`, `str`]
146 The keys are the (arbitrary) names of values needed by
147 `makeMeasurement`, the values are the metadata keys to be looked
148 up. Metadata keys are assumed to include task prefixes in the
149 format of `lsst.pipe.base.Task.getFullMetadata()`. This method may
150 return a substring of the desired (full) key, but multiple matches
151 for any key will cause an error.
152 """
154 @staticmethod
155 def _searchKeys(metadata, keyFragment):
156 """Search the metadata for all keys matching a substring.
158 Parameters
159 ----------
160 metadata : `lsst.daf.base.PropertySet`
161 A metadata object with task-qualified keys as returned by
162 `lsst.pipe.base.Task.getFullMetadata()`.
163 keyFragment : `str`
164 A substring for a full metadata key.
166 Returns
167 -------
168 keys : `set` of `str`
169 All keys in ``metadata`` that have ``keyFragment`` as a substring.
170 """
171 keys = metadata.paramNames(topLevelOnly=False)
172 return {key for key in keys if keyFragment in key}
174 @staticmethod
175 def _extractMetadata(metadata, metadataKeys):
176 """Read multiple keys from a metadata object.
178 Parameters
179 ----------
180 metadata : `lsst.daf.base.PropertySet`
181 A metadata object, assumed not `None`.
182 metadataKeys : `dict` [`str`, `str`]
183 Keys are arbitrary labels, values are metadata keys (or their
184 substrings) in the format of
185 `lsst.pipe.base.Task.getFullMetadata()`.
187 Returns
188 -------
189 metadataValues : `dict` [`str`, any]
190 Keys are the same as for ``metadataKeys``, values are the value of
191 each metadata key, or `None` if no matching key was found.
193 Raises
194 ------
195 lsst.verify.tasks.MetricComputationError
196 Raised if any metadata key string has more than one match
197 in ``metadata``.
198 """
199 data = {}
200 for dataName, keyFragment in metadataKeys.items():
201 matchingKeys = MetadataMetricTask._searchKeys(
202 metadata, keyFragment)
203 if len(matchingKeys) == 1:
204 key, = matchingKeys
205 data[dataName] = metadata.getScalar(key)
206 elif not matchingKeys:
207 data[dataName] = None
208 else:
209 error = "String %s matches multiple metadata keys: %s" \
210 % (keyFragment, matchingKeys)
211 raise MetricComputationError(error)
212 return data
215class MetadataMetricTask(_AbstractMetadataMetricTask):
216 """A base class for tasks that compute metrics from single metadata objects.
218 Parameters
219 ----------
220 *args
221 **kwargs
222 Constructor parameters are the same as for
223 `lsst.pipe.base.PipelineTask`.
225 Notes
226 -----
227 This class should be customized by overriding `getInputMetadataKeys`
228 and `makeMeasurement`. You should not need to override `run`.
230 This class makes no assumptions about how to handle missing data;
231 `makeMeasurement` may be called with `None` values, and is responsible
232 for deciding how to deal with them.
233 """
234 # Design note: getInputMetadataKeys and makeMeasurement are overrideable
235 # methods rather than subtask(s) to keep the configs for
236 # `MetricsControllerTask` as simple as possible. This was judged more
237 # important than ensuring that no implementation details of MetricTask
238 # can leak into application-specific code.
240 ConfigClass = MetadataMetricConfig
242 @abc.abstractmethod
243 def makeMeasurement(self, values):
244 """Compute the metric given the values of the metadata.
246 Parameters
247 ----------
248 values : `dict` [`str`, any]
249 A `dict` representation of the metadata passed to `run`. It has the
250 same keys as returned by `getInputMetadataKeys`, and maps them to
251 the values extracted from the metadata. Any value may be `None` to
252 represent missing data.
254 Returns
255 -------
256 measurement : `lsst.verify.Measurement` or `None`
257 The measurement corresponding to the input data.
259 Raises
260 ------
261 lsst.verify.tasks.MetricComputationError
262 Raised if an algorithmic or system error prevents calculation of
263 the metric. See `run` for expected behavior.
264 """
266 def run(self, metadata):
267 """Compute a measurement from science task metadata.
269 Parameters
270 ----------
271 metadata : `lsst.daf.base.PropertySet` or `None`
272 A metadata object for the unit of science processing to use for
273 this metric, or a collection of such objects if this task combines
274 many units of processing into a single metric.
276 Returns
277 -------
278 result : `lsst.pipe.base.Struct`
279 A `~lsst.pipe.base.Struct` containing the following component:
281 - ``measurement``: the value of the metric
282 (`lsst.verify.Measurement` or `None`)
284 Raises
285 ------
286 lsst.verify.tasks.MetricComputationError
287 Raised if the strings returned by `getInputMetadataKeys` match
288 more than one key in any metadata object.
290 Notes
291 -----
292 This implementation calls `getInputMetadataKeys`, then searches for
293 matching keys in each metadata. It then passes the values of these
294 keys (or `None` if no match) to `makeMeasurement`, and returns its
295 result to the caller.
296 """
297 metadataKeys = self.getInputMetadataKeys(self.config)
299 if metadata is not None:
300 data = self._extractMetadata(metadata, metadataKeys)
301 else:
302 data = {dataName: None for dataName in metadataKeys}
304 return Struct(measurement=self.makeMeasurement(data))