Coverage for python/lsst/analysis/tools/interfaces/_actions.py: 64%
87 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 04:57 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 04:57 -0700
1# This file is part of analysis_tools.
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/>.
22from __future__ import annotations
24__all__ = (
25 "AnalysisAction",
26 "KeyedDataAction",
27 "VectorAction",
28 "ScalarAction",
29 "MetricResultType",
30 "MetricAction",
31 "PlotResultType",
32 "PlotElement",
33 "PlotAction",
34 "JointResults",
35 "JointAction",
36 "NoMetric",
37 "NoPlot",
38)
40import warnings
41from abc import abstractmethod
42from dataclasses import dataclass
43from typing import TYPE_CHECKING, Iterable
45import lsst.pex.config as pexConfig
46from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField
48from ..contexts import ContextApplier
49from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Tensor, Vector
51if TYPE_CHECKING: 51 ↛ 52line 51 didn't jump to line 52, because the condition on line 51 was never true
52 from matplotlib.axes import Axes
55class AnalysisAction(ConfigurableAction):
56 """Base class interface for the various actions used in analysis tools.
58 This extends the basic `ConfigurableAction` class to include interfaces for
59 defining what an action expects to consume, and what it expects to produce.
60 """
62 def __init_subclass__(cls, **kwargs):
63 if "getInputSchema" not in dir(cls): 63 ↛ 64line 63 didn't jump to line 64, because the condition on line 63 was never true
64 raise NotImplementedError(f"Class {cls} must implement method getInputSchema")
66 # This is a descriptor that functions like a function in most contexts
67 # and can be treated as such
68 applyContext = ContextApplier()
69 r"""Apply a `Context` to an `AnalysisAction` recursively.
71 Generally this method is called from within an `AnalysisTool` to
72 configure all `AnalysisAction`\ s at one time to make sure that they
73 all are consistently configured. However, it is permitted to call this
74 method if you are aware of the effects, or from within a specific
75 execution environment like a python shell or notebook.
77 Parameters
78 ----------
79 context : `Context`
80 The specific execution context, this may be a single context or
81 a joint context, see `Context` for more info.
82 """
84 @abstractmethod
85 def getInputSchema(self) -> KeyedDataSchema:
86 """Return the schema an `AnalysisAction` expects to be present in the
87 arguments supplied to the __call__ method.
89 Returns
90 -------
91 result : `KeyedDataSchema`
92 The schema this action requires to be present when calling this
93 action, keys are unformatted.
94 """
95 raise NotImplementedError("This is not implemented on the base class")
97 def getOutputSchema(self) -> KeyedDataSchema | None:
98 """Return the schema an `AnalysisAction` will produce, if the
99 ``__call__`` method returns `KeyedData`, otherwise this may return
100 None.
102 Returns
103 -------
104 result : `KeyedDataSchema` or None
105 The schema this action will produce when returning from call. This
106 will be unformatted if any templates are present. Should return
107 None if action does not return `KeyedData`.
108 """
109 return None
111 def getFormattedInputSchema(self, **kwargs) -> KeyedDataSchema:
112 """Return input schema, with keys formatted with any arguments supplied
113 by kwargs passed to this method.
115 Returns
116 -------
117 result : `KeyedDataSchema`
118 The schema this action requires to be present when calling this
119 action, formatted with any input arguments (e.g. band='i')
120 """
121 for key, typ in self.getInputSchema():
122 yield key.format_map(kwargs), typ
124 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None:
125 """Add the supplied inputSchema argument to the class such that it will
126 be returned along side any other arguments in a call to
127 ``getInputSchema``.
129 Parameters
130 ----------
131 inputSchema : `KeyedDataSchema`
132 A schema that is to be merged in with any existing schema when a
133 call to ``getInputSchema`` is made.
134 """
135 warnings.warn(
136 f"{type(self)} does not implement adding input schemas, call will do nothing, "
137 "this may be expected",
138 RuntimeWarning,
139 )
142class KeyedDataAction(AnalysisAction):
143 """A `KeyedDataAction` is an `AnalysisAction` that returns `KeyedData` when
144 called.
145 """
147 @abstractmethod
148 def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
149 raise NotImplementedError("This is not implemented on the base class")
152class VectorAction(AnalysisAction):
153 """A `VectorAction` is an `AnalysisAction` that returns a `Vector` when
154 called.
155 """
157 @abstractmethod
158 def __call__(self, data: KeyedData, **kwargs) -> Vector:
159 raise NotImplementedError("This is not implemented on the base class")
162class TensorAction(AnalysisAction):
163 """A `TensorAction` is an `AnalysisAction` that returns a `Tensor` when
164 called.
165 """
167 @abstractmethod
168 def __call__(self, data: KeyedData, **kwargs) -> Tensor:
169 raise NotImplementedError("This is not implemented on the base class")
172class ScalarAction(AnalysisAction):
173 """A `ScalarAction` is an `AnalysisAction` that returns a `Scalar` when
174 called.
175 """
177 @abstractmethod
178 def __call__(self, data: KeyedData, **kwargs) -> Scalar:
179 """Compute a scalar value from keyed data.
181 Parameters
182 ----------
183 data
184 Keyed data to compute a value from.
185 kwargs
186 Additional keyword arguments.
188 Returns
189 -------
190 A scalar value.
191 """
192 raise NotImplementedError("This is not implemented on the base class")
194 def getMask(self, **kwargs) -> Vector | slice:
195 """Extract a mask if one is passed as key word args, otherwise return
196 an empty slice object that can still be used in a getitem call.
198 Returns
199 -------
200 result : `Vector` or `slice`
201 The mask passed as a keyword, or a slice object that will return
202 a complete Vector when used in getitem.
203 """
204 if (mask := kwargs.get("mask")) is None:
205 mask = slice(None)
206 return mask
209class MetricAction(AnalysisAction):
210 """A `MetricAction` is an `AnalysisAction` that returns a `Measurement` or
211 a `Mapping` of `str` to `Measurement` when called.
212 """
214 @abstractmethod
215 def __call__(self, data: KeyedData, **kwargs) -> MetricResultType:
216 raise NotImplementedError("This is not implemented on the base class")
219class PlotAction(AnalysisAction):
220 """A `PlotAction` is an `AnalysisAction` that returns a `PlotType` or
221 a `Mapping` of `str` to `PlotType` when called.
222 """
224 def getOutputNames(self, config: pexConfig.Config | None = None) -> Iterable[str]:
225 """Returns a list of names that will be used as keys if this action's
226 call method returns a mapping. Otherwise return an empty Iterable.
228 Parameters
229 ----------
230 config : `lsst.pex.config.Config`, optional
231 Configuration of the task. This is only used if the output naming
232 needs to be config-aware.
234 Returns
235 -------
236 result : `Iterable` of `str`
237 If a `PlotAction` produces more than one plot, this should be the
238 keys the action will use in the returned `Mapping`.
239 """
240 return tuple()
242 @abstractmethod
243 def __call__(self, data: KeyedData, **kwargs) -> PlotResultType:
244 raise NotImplementedError("This is not implemented on the base class")
247class PlotElement(AnalysisAction):
248 """PlotElements are the most basic components of a plot. They can be
249 composed together within a `PlotAction` to create rich plots.
251 Plot elements may return metadata about creating their element by returning
252 `KeyedData` from their call method.
253 """
255 @abstractmethod
256 def __call__(self, data: KeyedData, ax: Axes, **kwargs) -> KeyedData:
257 raise NotImplementedError("This is not implemented on the base class")
260class NoPlot(PlotAction):
261 """This is a sentinel class to indicate that there is no plotting action"""
264class NoMetric(MetricAction):
265 """This is a sentinel class to indicate that there is no Metric action"""
268@dataclass
269class JointResults:
270 """The `JointResults` dataclass is a container for the results of a
271 `JointAction`.
272 """
274 plot: PlotResultType | None
275 metric: MetricResultType | None
278class JointAction(AnalysisAction):
279 """A `JointAction` is an `AnalysisAction` that is a composite of a
280 `PlotAction` and a `MetricAction`.
281 """
283 metric = ConfigurableActionField[MetricAction](doc="Action to run that will produce one or more metrics")
284 plot = ConfigurableActionField[PlotAction](doc="Action to run that will produce one or more plots")
286 def __call__(self, data: KeyedData, **kwargs) -> JointResults:
287 if isinstance(self.plot, NoPlot):
288 plots = None
289 else:
290 plots = self.plot(data, **kwargs)
291 if isinstance(self.metric, NoMetric):
292 metrics = None
293 else:
294 metrics = self.metric(data, **kwargs)
295 return JointResults(plots, metrics)
297 def getInputSchema(self) -> KeyedDataSchema:
298 yield from self.metric.getInputSchema()
299 yield from self.plot.getInputSchema()
301 def getOutputNames(self, config: pexConfig.Config | None = None) -> Iterable[str]:
302 """Returns a list of names that will be used as keys if this action's
303 call method returns a mapping. Otherwise return an empty Iterable.
305 Parameters
306 ----------
307 config : `lsst.pex.config.Config`, optional
308 Configuration of the task. This is only used if the output naming
309 needs to be config-aware.
311 Returns
312 -------
313 outNames : `Iterable` of `str`
314 If a `PlotAction` produces more than one plot, this should be the
315 keys the action will use in the returned `Mapping`.
316 """
317 if config is None:
318 # `dynamicOutputNames` is set to False.
319 outNames = self.plot.getOutputNames()
320 else:
321 # `dynamicOutputNames` is set to True.
322 outNames = self.plot.getOutputNames(config=config)
323 return outNames