Coverage for python/lsst/analysis/tools/interfaces/_actions.py: 60%
73 statements
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-30 03:04 -0700
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-30 03:04 -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)
37import warnings
38from abc import abstractmethod
39from dataclasses import dataclass
40from typing import Iterable
42from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField
44from ..contexts import ContextApplier
45from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Vector
48class AnalysisAction(ConfigurableAction):
49 """Base class interface for the various actions used in analysis tools.
51 This extends the basic `ConfigurableAction` class to include interfaces for
52 defining what an action expects to consume, and what it expects to produce.
53 """
55 def __init_subclass__(cls, **kwargs):
56 if "getInputSchema" not in dir(cls): 56 ↛ 57line 56 didn't jump to line 57, because the condition on line 56 was never true
57 raise NotImplementedError(f"Class {cls} must implement method getInputSchema")
59 # This is a descriptor that functions like a function in most contexts
60 # and can be treated as such
61 applyContext = ContextApplier()
62 r"""Apply a `Context` to an `AnalysisAction` recursively.
64 Generally this method is called from within an `AnalysisTool` to
65 configure all `AnalysisAction`\ s at one time to make sure that they
66 all are consistently configured. However, it is permitted to call this
67 method if you are aware of the effects, or from within a specific
68 execution environment like a python shell or notebook.
70 Parameters
71 ----------
72 context : `Context`
73 The specific execution context, this may be a single context or
74 a joint context, see `Context` for more info.
75 """
77 @abstractmethod
78 def getInputSchema(self) -> KeyedDataSchema:
79 """Return the schema an `AnalysisAction` expects to be present in the
80 arguments supplied to the __call__ method.
82 Returns
83 -------
84 result : `KeyedDataSchema`
85 The schema this action requires to be present when calling this
86 action, keys are unformatted.
87 """
88 raise NotImplementedError("This is not implemented on the base class")
90 def getOutputSchema(self) -> KeyedDataSchema | None:
91 """Return the schema an `AnalysisAction` will produce, if the
92 ``__call__`` method returns `KeyedData`, otherwise this may return
93 None.
95 Returns
96 -------
97 result : `KeyedDataSchema` or None
98 The schema this action will produce when returning from call. This
99 will be unformatted if any templates are present. Should return
100 None if action does not return `KeyedData`.
101 """
102 return None
104 def getFormattedInputSchema(self, **kwargs) -> KeyedDataSchema:
105 """Return input schema, with keys formatted with any arguments supplied
106 by kwargs passed to this method.
108 Returns
109 -------
110 result : `KeyedDataSchema`
111 The schema this action requires to be present when calling this
112 action, formatted with any input arguments (e.g. band='i')
113 """
114 for key, typ in self.getInputSchema():
115 yield key.format_map(kwargs), typ
117 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None:
118 """Add the supplied inputSchema argument to the class such that it will
119 be returned along side any other arguments in a call to
120 ``getInputSchema``.
122 Parameters
123 ----------
124 inputSchema : `KeyedDataSchema`
125 A schema that is to be merged in with any existing schema when a
126 call to ``getInputSchema`` is made.
127 """
128 warnings.warn(
129 f"{type(self)} does not implement adding input schemas, call will do nothing, "
130 "this may be expected",
131 RuntimeWarning,
132 )
135class KeyedDataAction(AnalysisAction):
136 """A `KeyedDataAction` is an `AnalysisAction` that returns `KeyedData` when
137 called.
138 """
140 @abstractmethod
141 def __call__(self, data: KeyedData, **kwargs) -> KeyedData:
142 raise NotImplementedError("This is not implemented on the base class")
145class VectorAction(AnalysisAction):
146 """A `VectorAction` is an `AnalysisAction` that returns a `Vector` when
147 called.
148 """
150 @abstractmethod
151 def __call__(self, data: KeyedData, **kwargs) -> Vector:
152 raise NotImplementedError("This is not implemented on the base class")
155class ScalarAction(AnalysisAction):
156 """A `ScalarAction` is an `AnalysisAction` that returns a `Scalar` when
157 called.
158 """
160 @abstractmethod
161 def __call__(self, data: KeyedData, **kwargs) -> Scalar:
162 raise NotImplementedError("This is not implemented on the base class")
164 def getMask(self, **kwargs) -> Vector | slice:
165 """Extract a mask if one is passed as key word args, otherwise return
166 an empty slice object that can still be used in a getitem call.
168 Returns
169 -------
170 result : `Vector` or `slice`
171 The mask passed as a keyword, or a slice object that will return
172 a complete Vector when used in getitem.
173 """
174 if (mask := kwargs.get("mask")) is None:
175 mask = slice(None)
176 return mask
179class MetricAction(AnalysisAction):
180 """A `MetricAction` is an `AnalysisAction` that returns a `Measurement` or
181 a `Mapping` of `str` to `Measurement` when called.
182 """
184 @abstractmethod
185 def __call__(self, data: KeyedData, **kwargs) -> MetricResultType:
186 raise NotImplementedError("This is not implemented on the base class")
189class PlotAction(AnalysisAction):
190 """A `PlotAction` is an `AnalysisAction` that returns a `PlotType` or
191 a `Mapping` of `str` to `PlotType` when called.
192 """
194 def getOutputNames(self) -> Iterable[str]:
195 """Returns a list of names that will be used as keys if this action's
196 call method returns a mapping. Otherwise return an empty Iterable
198 Returns
199 -------
200 result : `Iterable` of `str`
201 If a `PlotAction` produces more than one plot, this should be the
202 keys the action will use in the returned `Mapping`.
203 """
204 return tuple()
206 @abstractmethod
207 def __call__(self, data: KeyedData, **kwargs) -> PlotResultType:
208 raise NotImplementedError("This is not implemented on the base class")
211class NoPlot(PlotAction):
212 """This is a sentinel class to indicate that there is no plotting action"""
215class NoMetric(MetricAction):
216 """This is a sentinel class to indicate that there is no Metric action"""
219@dataclass
220class JointResults:
221 plot: PlotResultType | None
222 metric: MetricResultType | None
225class JointAction(AnalysisAction):
226 """A `JointAction` is an `AnalysisAction` that is a composite of a
227 `PlotAction` and a `MetricAction`
228 """
230 metric = ConfigurableActionField[MetricAction](doc="Action to run that will produce one or more metrics")
231 plot = ConfigurableActionField[PlotAction](doc="Action to run that will produce one or more plots")
233 def __call__(self, data: KeyedData, **kwargs) -> JointResults:
234 if isinstance(self.plot, NoPlot):
235 plots = None
236 else:
237 plots = self.plot(data, **kwargs)
238 if isinstance(self.metric, NoMetric):
239 metrics = None
240 else:
241 metrics = self.metric(data, **kwargs)
242 return JointResults(plots, metrics)
244 def getInputSchema(self) -> KeyedDataSchema:
245 yield from self.metric.getInputSchema()
246 yield from self.plot.getInputSchema()
248 def getOutputNames(self) -> Iterable[str]:
249 """Returns a list of names that will be used as keys if this action's
250 call method returns a mapping. Otherwise return an empty Iterable
252 Returns
253 -------
254 result : `Iterable` of `str`
255 If a `PlotAction` produces more than one plot, this should be the
256 keys the action will use in the returned `Mapping`.
257 """
258 return self.plot.getOutputNames()