Coverage for python/lsst/analysis/tools/interfaces/_actions.py: 62%
77 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-20 13:17 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-20 13:17 +0000
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 "PlotAction",
33 "JointResults",
34 "JointAction",
35 "NoMetric",
36 "NoPlot",
37)
39import warnings
40from abc import abstractmethod
41from dataclasses import dataclass
42from typing import Iterable
44import lsst.pex.config as pexConfig
45from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField
47from ..contexts import ContextApplier
48from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Vector
51class AnalysisAction(ConfigurableAction):
52 """Base class interface for the various actions used in analysis tools.
54 This extends the basic `ConfigurableAction` class to include interfaces for
55 defining what an action expects to consume, and what it expects to produce.
56 """
58 def __init_subclass__(cls, **kwargs):
59 if "getInputSchema" not in dir(cls): 59 ↛ 60line 59 didn't jump to line 60, because the condition on line 59 was never true
60 raise NotImplementedError(f"Class {cls} must implement method getInputSchema")
62 # This is a descriptor that functions like a function in most contexts
63 # and can be treated as such
64 applyContext = ContextApplier()
65 r"""Apply a `Context` to an `AnalysisAction` recursively.
67 Generally this method is called from within an `AnalysisTool` to
68 configure all `AnalysisAction`\ s at one time to make sure that they
69 all are consistently configured. However, it is permitted to call this
70 method if you are aware of the effects, or from within a specific
71 execution environment like a python shell or notebook.
73 Parameters
74 ----------
75 context : `Context`
76 The specific execution context, this may be a single context or
77 a joint context, see `Context` for more info.
78 """
80 @abstractmethod
81 def getInputSchema(self) -> KeyedDataSchema:
82 """Return the schema an `AnalysisAction` expects to be present in the
83 arguments supplied to the __call__ method.
85 Returns
86 -------
87 result : `KeyedDataSchema`
88 The schema this action requires to be present when calling this
89 action, keys are unformatted.
90 """
91 raise NotImplementedError("This is not implemented on the base class")
93 def getOutputSchema(self) -> KeyedDataSchema | None:
94 """Return the schema an `AnalysisAction` will produce, if the
95 ``__call__`` method returns `KeyedData`, otherwise this may return
96 None.
98 Returns
99 -------
100 result : `KeyedDataSchema` or None
101 The schema this action will produce when returning from call. This
102 will be unformatted if any templates are present. Should return
103 None if action does not return `KeyedData`.
104 """
105 return None
107 def getFormattedInputSchema(self, **kwargs) -> KeyedDataSchema:
108 """Return input schema, with keys formatted with any arguments supplied
109 by kwargs passed to this method.
111 Returns
112 -------
113 result : `KeyedDataSchema`
114 The schema this action requires to be present when calling this
115 action, formatted with any input arguments (e.g. band='i')
116 """
117 for key, typ in self.getInputSchema():
118 yield key.format_map(kwargs), typ
120 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None:
121 """Add the supplied inputSchema argument to the class such that it will
122 be returned along side any other arguments in a call to
123 ``getInputSchema``.
125 Parameters
126 ----------
127 inputSchema : `KeyedDataSchema`
128 A schema that is to be merged in with any existing schema when a
129 call to ``getInputSchema`` is made.
130 """
131 warnings.warn(
132 f"{type(self)} does not implement adding input schemas, call will do nothing, "
133 "this may be expected",
134 RuntimeWarning,
135 )
138class KeyedDataAction(AnalysisAction):
139 """A `KeyedDataAction` is an `AnalysisAction` that returns `KeyedData` when
140 called.
141 """
143 @abstractmethod
144 def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
145 raise NotImplementedError("This is not implemented on the base class")
148class VectorAction(AnalysisAction):
149 """A `VectorAction` is an `AnalysisAction` that returns a `Vector` when
150 called.
151 """
153 @abstractmethod
154 def __call__(self, data: KeyedData, **kwargs) -> Vector:
155 raise NotImplementedError("This is not implemented on the base class")
158class ScalarAction(AnalysisAction):
159 """A `ScalarAction` is an `AnalysisAction` that returns a `Scalar` when
160 called.
161 """
163 @abstractmethod
164 def __call__(self, data: KeyedData, **kwargs) -> Scalar:
165 """Compute a scalar value from keyed data.
167 Parameters
168 ----------
169 data
170 Keyed data to compute a value from.
171 kwargs
172 Additional keyword arguments.
174 Returns
175 -------
176 A scalar value.
177 """
178 raise NotImplementedError("This is not implemented on the base class")
180 def getMask(self, **kwargs) -> Vector | slice:
181 """Extract a mask if one is passed as key word args, otherwise return
182 an empty slice object that can still be used in a getitem call.
184 Returns
185 -------
186 result : `Vector` or `slice`
187 The mask passed as a keyword, or a slice object that will return
188 a complete Vector when used in getitem.
189 """
190 if (mask := kwargs.get("mask")) is None:
191 mask = slice(None)
192 return mask
195class MetricAction(AnalysisAction):
196 """A `MetricAction` is an `AnalysisAction` that returns a `Measurement` or
197 a `Mapping` of `str` to `Measurement` when called.
198 """
200 @abstractmethod
201 def __call__(self, data: KeyedData, **kwargs) -> MetricResultType:
202 raise NotImplementedError("This is not implemented on the base class")
205class PlotAction(AnalysisAction):
206 """A `PlotAction` is an `AnalysisAction` that returns a `PlotType` or
207 a `Mapping` of `str` to `PlotType` when called.
208 """
210 def getOutputNames(self, config: pexConfig.Config | None = None) -> Iterable[str]:
211 """Returns a list of names that will be used as keys if this action's
212 call method returns a mapping. Otherwise return an empty Iterable.
214 Parameters
215 ----------
216 config : `lsst.pex.config.Config`, optional
217 Configuration of the task. This is only used if the output naming
218 needs to be config-aware.
220 Returns
221 -------
222 result : `Iterable` of `str`
223 If a `PlotAction` produces more than one plot, this should be the
224 keys the action will use in the returned `Mapping`.
225 """
226 return tuple()
228 @abstractmethod
229 def __call__(self, data: KeyedData, **kwargs) -> PlotResultType:
230 raise NotImplementedError("This is not implemented on the base class")
233class NoPlot(PlotAction):
234 """This is a sentinel class to indicate that there is no plotting action"""
237class NoMetric(MetricAction):
238 """This is a sentinel class to indicate that there is no Metric action"""
241@dataclass
242class JointResults:
243 """The `JointResults` dataclass is a container for the results of a
244 `JointAction`.
245 """
247 plot: PlotResultType | None
248 metric: MetricResultType | None
251class JointAction(AnalysisAction):
252 """A `JointAction` is an `AnalysisAction` that is a composite of a
253 `PlotAction` and a `MetricAction`.
254 """
256 metric = ConfigurableActionField[MetricAction](doc="Action to run that will produce one or more metrics")
257 plot = ConfigurableActionField[PlotAction](doc="Action to run that will produce one or more plots")
259 def __call__(self, data: KeyedData, **kwargs) -> JointResults:
260 if isinstance(self.plot, NoPlot):
261 plots = None
262 else:
263 plots = self.plot(data, **kwargs)
264 if isinstance(self.metric, NoMetric):
265 metrics = None
266 else:
267 metrics = self.metric(data, **kwargs)
268 return JointResults(plots, metrics)
270 def getInputSchema(self) -> KeyedDataSchema:
271 yield from self.metric.getInputSchema()
272 yield from self.plot.getInputSchema()
274 def getOutputNames(self, config: pexConfig.Config | None = None) -> Iterable[str]:
275 """Returns a list of names that will be used as keys if this action's
276 call method returns a mapping. Otherwise return an empty Iterable.
278 Parameters
279 ----------
280 config : `lsst.pex.config.Config`, optional
281 Configuration of the task. This is only used if the output naming
282 needs to be config-aware.
284 Returns
285 -------
286 outNames : `Iterable` of `str`
287 If a `PlotAction` produces more than one plot, this should be the
288 keys the action will use in the returned `Mapping`.
289 """
290 if config is None:
291 # `dynamicOutputNames` is set to False.
292 outNames = self.plot.getOutputNames()
293 else:
294 # `dynamicOutputNames` is set to True.
295 outNames = self.plot.getOutputNames(config=config)
296 return outNames