Coverage for python/lsst/analysis/tools/interfaces/_actions.py: 60%
73 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 04:02 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-15 04:02 -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 "PlotAction",
33 "JointResults",
34 "JointAction",
35 "NoMetric",
36 "NoPlot",
37)
39import warnings
40from abc import abstractmethod
41from dataclasses import dataclass
42from typing import Iterable
44from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField
46from ..contexts import ContextApplier
47from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Vector
50class AnalysisAction(ConfigurableAction):
51 """Base class interface for the various actions used in analysis tools.
53 This extends the basic `ConfigurableAction` class to include interfaces for
54 defining what an action expects to consume, and what it expects to produce.
55 """
57 def __init_subclass__(cls, **kwargs):
58 if "getInputSchema" not in dir(cls): 58 ↛ 59line 58 didn't jump to line 59, because the condition on line 58 was never true
59 raise NotImplementedError(f"Class {cls} must implement method getInputSchema")
61 # This is a descriptor that functions like a function in most contexts
62 # and can be treated as such
63 applyContext = ContextApplier()
64 r"""Apply a `Context` to an `AnalysisAction` recursively.
66 Generally this method is called from within an `AnalysisTool` to
67 configure all `AnalysisAction`\ s at one time to make sure that they
68 all are consistently configured. However, it is permitted to call this
69 method if you are aware of the effects, or from within a specific
70 execution environment like a python shell or notebook.
72 Parameters
73 ----------
74 context : `Context`
75 The specific execution context, this may be a single context or
76 a joint context, see `Context` for more info.
77 """
79 @abstractmethod
80 def getInputSchema(self) -> KeyedDataSchema:
81 """Return the schema an `AnalysisAction` expects to be present in the
82 arguments supplied to the __call__ method.
84 Returns
85 -------
86 result : `KeyedDataSchema`
87 The schema this action requires to be present when calling this
88 action, keys are unformatted.
89 """
90 raise NotImplementedError("This is not implemented on the base class")
92 def getOutputSchema(self) -> KeyedDataSchema | None:
93 """Return the schema an `AnalysisAction` will produce, if the
94 ``__call__`` method returns `KeyedData`, otherwise this may return
95 None.
97 Returns
98 -------
99 result : `KeyedDataSchema` or None
100 The schema this action will produce when returning from call. This
101 will be unformatted if any templates are present. Should return
102 None if action does not return `KeyedData`.
103 """
104 return None
106 def getFormattedInputSchema(self, **kwargs) -> KeyedDataSchema:
107 """Return input schema, with keys formatted with any arguments supplied
108 by kwargs passed to this method.
110 Returns
111 -------
112 result : `KeyedDataSchema`
113 The schema this action requires to be present when calling this
114 action, formatted with any input arguments (e.g. band='i')
115 """
116 for key, typ in self.getInputSchema():
117 yield key.format_map(kwargs), typ
119 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None:
120 """Add the supplied inputSchema argument to the class such that it will
121 be returned along side any other arguments in a call to
122 ``getInputSchema``.
124 Parameters
125 ----------
126 inputSchema : `KeyedDataSchema`
127 A schema that is to be merged in with any existing schema when a
128 call to ``getInputSchema`` is made.
129 """
130 warnings.warn(
131 f"{type(self)} does not implement adding input schemas, call will do nothing, "
132 "this may be expected",
133 RuntimeWarning,
134 )
137class KeyedDataAction(AnalysisAction):
138 """A `KeyedDataAction` is an `AnalysisAction` that returns `KeyedData` when
139 called.
140 """
142 @abstractmethod
143 def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
144 raise NotImplementedError("This is not implemented on the base class")
147class VectorAction(AnalysisAction):
148 """A `VectorAction` is an `AnalysisAction` that returns a `Vector` when
149 called.
150 """
152 @abstractmethod
153 def __call__(self, data: KeyedData, **kwargs) -> Vector:
154 raise NotImplementedError("This is not implemented on the base class")
157class ScalarAction(AnalysisAction):
158 """A `ScalarAction` is an `AnalysisAction` that returns a `Scalar` when
159 called.
160 """
162 @abstractmethod
163 def __call__(self, data: KeyedData, **kwargs) -> Scalar:
164 raise NotImplementedError("This is not implemented on the base class")
166 def getMask(self, **kwargs) -> Vector | slice:
167 """Extract a mask if one is passed as key word args, otherwise return
168 an empty slice object that can still be used in a getitem call.
170 Returns
171 -------
172 result : `Vector` or `slice`
173 The mask passed as a keyword, or a slice object that will return
174 a complete Vector when used in getitem.
175 """
176 if (mask := kwargs.get("mask")) is None:
177 mask = slice(None)
178 return mask
181class MetricAction(AnalysisAction):
182 """A `MetricAction` is an `AnalysisAction` that returns a `Measurement` or
183 a `Mapping` of `str` to `Measurement` when called.
184 """
186 @abstractmethod
187 def __call__(self, data: KeyedData, **kwargs) -> MetricResultType:
188 raise NotImplementedError("This is not implemented on the base class")
191class PlotAction(AnalysisAction):
192 """A `PlotAction` is an `AnalysisAction` that returns a `PlotType` or
193 a `Mapping` of `str` to `PlotType` when called.
194 """
196 def getOutputNames(self) -> Iterable[str]:
197 """Returns a list of names that will be used as keys if this action's
198 call method returns a mapping. Otherwise return an empty Iterable
200 Returns
201 -------
202 result : `Iterable` of `str`
203 If a `PlotAction` produces more than one plot, this should be the
204 keys the action will use in the returned `Mapping`.
205 """
206 return tuple()
208 @abstractmethod
209 def __call__(self, data: KeyedData, **kwargs) -> PlotResultType:
210 raise NotImplementedError("This is not implemented on the base class")
213class NoPlot(PlotAction):
214 """This is a sentinel class to indicate that there is no plotting action"""
217class NoMetric(MetricAction):
218 """This is a sentinel class to indicate that there is no Metric action"""
221@dataclass
222class JointResults:
223 """The `JointResults` dataclass is a container for the results of a
224 `JointAction`.
225 """
227 plot: PlotResultType | None
228 metric: MetricResultType | None
231class JointAction(AnalysisAction):
232 """A `JointAction` is an `AnalysisAction` that is a composite of a
233 `PlotAction` and a `MetricAction`.
234 """
236 metric = ConfigurableActionField[MetricAction](doc="Action to run that will produce one or more metrics")
237 plot = ConfigurableActionField[PlotAction](doc="Action to run that will produce one or more plots")
239 def __call__(self, data: KeyedData, **kwargs) -> JointResults:
240 if isinstance(self.plot, NoPlot):
241 plots = None
242 else:
243 plots = self.plot(data, **kwargs)
244 if isinstance(self.metric, NoMetric):
245 metrics = None
246 else:
247 metrics = self.metric(data, **kwargs)
248 return JointResults(plots, metrics)
250 def getInputSchema(self) -> KeyedDataSchema:
251 yield from self.metric.getInputSchema()
252 yield from self.plot.getInputSchema()
254 def getOutputNames(self) -> Iterable[str]:
255 """Returns a list of names that will be used as keys if this action's
256 call method returns a mapping. Otherwise return an empty Iterable
258 Returns
259 -------
260 result : `Iterable` of `str`
261 If a `PlotAction` produces more than one plot, this should be the
262 keys the action will use in the returned `Mapping`.
263 """
264 return self.plot.getOutputNames()