Coverage for python/lsst/analysis/tools/interfaces/_actions.py: 64%

87 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-26 04:09 -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/>. 

21 

22from __future__ import annotations 

23 

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) 

39 

40import warnings 

41from abc import abstractmethod 

42from dataclasses import dataclass 

43from typing import TYPE_CHECKING, Iterable 

44 

45import lsst.pex.config as pexConfig 

46from lsst.pex.config.configurableActions import ConfigurableAction, ConfigurableActionField 

47 

48from ..contexts import ContextApplier 

49from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Tensor, Vector 

50 

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 

53 

54 

55class AnalysisAction(ConfigurableAction): 

56 """Base class interface for the various actions used in analysis tools. 

57 

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 """ 

61 

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") 

65 

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. 

70 

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. 

76 

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 """ 

83 

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. 

88 

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") 

96 

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. 

101 

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 

110 

111 def getFormattedInputSchema(self, **kwargs) -> KeyedDataSchema: 

112 """Return input schema, with keys formatted with any arguments supplied 

113 by kwargs passed to this method. 

114 

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 

123 

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``. 

128 

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 ) 

140 

141 

142class KeyedDataAction(AnalysisAction): 

143 """A `KeyedDataAction` is an `AnalysisAction` that returns `KeyedData` when 

144 called. 

145 """ 

146 

147 @abstractmethod 

148 def __call__(self, data: KeyedData, **kwargs) -> KeyedData: 

149 raise NotImplementedError("This is not implemented on the base class") 

150 

151 

152class VectorAction(AnalysisAction): 

153 """A `VectorAction` is an `AnalysisAction` that returns a `Vector` when 

154 called. 

155 """ 

156 

157 @abstractmethod 

158 def __call__(self, data: KeyedData, **kwargs) -> Vector: 

159 raise NotImplementedError("This is not implemented on the base class") 

160 

161 

162class TensorAction(AnalysisAction): 

163 """A `TensorAction` is an `AnalysisAction` that returns a `Tensor` when 

164 called. 

165 """ 

166 

167 @abstractmethod 

168 def __call__(self, data: KeyedData, **kwargs) -> Tensor: 

169 raise NotImplementedError("This is not implemented on the base class") 

170 

171 

172class ScalarAction(AnalysisAction): 

173 """A `ScalarAction` is an `AnalysisAction` that returns a `Scalar` when 

174 called. 

175 """ 

176 

177 @abstractmethod 

178 def __call__(self, data: KeyedData, **kwargs) -> Scalar: 

179 """Compute a scalar value from keyed data. 

180 

181 Parameters 

182 ---------- 

183 data 

184 Keyed data to compute a value from. 

185 kwargs 

186 Additional keyword arguments. 

187 

188 Returns 

189 ------- 

190 A scalar value. 

191 """ 

192 raise NotImplementedError("This is not implemented on the base class") 

193 

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. 

197 

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 

207 

208 

209class MetricAction(AnalysisAction): 

210 """A `MetricAction` is an `AnalysisAction` that returns a `Measurement` or 

211 a `Mapping` of `str` to `Measurement` when called. 

212 """ 

213 

214 @abstractmethod 

215 def __call__(self, data: KeyedData, **kwargs) -> MetricResultType: 

216 raise NotImplementedError("This is not implemented on the base class") 

217 

218 

219class PlotAction(AnalysisAction): 

220 """A `PlotAction` is an `AnalysisAction` that returns a `PlotType` or 

221 a `Mapping` of `str` to `PlotType` when called. 

222 """ 

223 

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. 

227 

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. 

233 

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() 

241 

242 @abstractmethod 

243 def __call__(self, data: KeyedData, **kwargs) -> PlotResultType: 

244 raise NotImplementedError("This is not implemented on the base class") 

245 

246 

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. 

250 

251 Plot elements may return metadata about creating their element by returning 

252 `KeyedData` from their call method. 

253 """ 

254 

255 @abstractmethod 

256 def __call__(self, data: KeyedData, ax: Axes, **kwargs) -> KeyedData: 

257 raise NotImplementedError("This is not implemented on the base class") 

258 

259 

260class NoPlot(PlotAction): 

261 """This is a sentinel class to indicate that there is no plotting action""" 

262 

263 

264class NoMetric(MetricAction): 

265 """This is a sentinel class to indicate that there is no Metric action""" 

266 

267 

268@dataclass 

269class JointResults: 

270 """The `JointResults` dataclass is a container for the results of a 

271 `JointAction`. 

272 """ 

273 

274 plot: PlotResultType | None 

275 metric: MetricResultType | None 

276 

277 

278class JointAction(AnalysisAction): 

279 """A `JointAction` is an `AnalysisAction` that is a composite of a 

280 `PlotAction` and a `MetricAction`. 

281 """ 

282 

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") 

285 

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) 

296 

297 def getInputSchema(self) -> KeyedDataSchema: 

298 yield from self.metric.getInputSchema() 

299 yield from self.plot.getInputSchema() 

300 

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. 

304 

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. 

310 

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