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