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

89 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 11:06 +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/>. 

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 def getPlotType(self) -> str: 

247 return type(self).__name__ 

248 

249 

250class PlotElement(AnalysisAction): 

251 """PlotElements are the most basic components of a plot. They can be 

252 composed together within a `PlotAction` to create rich plots. 

253 

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

255 `KeyedData` from their call method. 

256 """ 

257 

258 @abstractmethod 

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

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

261 

262 

263class NoPlot(PlotAction): 

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

265 

266 

267class NoMetric(MetricAction): 

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

269 

270 

271@dataclass 

272class JointResults: 

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

274 `JointAction`. 

275 """ 

276 

277 plot: PlotResultType | None 

278 metric: MetricResultType | None 

279 

280 

281class JointAction(AnalysisAction): 

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

283 `PlotAction` and a `MetricAction`. 

284 """ 

285 

286 metric = ConfigurableActionField[MetricAction](doc="Action to run that will produce one or more metrics") 

287 plot = ConfigurableActionField[PlotAction](doc="Action to run that will produce one or more plots") 

288 

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

290 if isinstance(self.plot, NoPlot): 

291 plots = None 

292 else: 

293 plots = self.plot(data, **kwargs) 

294 if isinstance(self.metric, NoMetric): 

295 metrics = None 

296 else: 

297 metrics = self.metric(data, **kwargs) 

298 return JointResults(plots, metrics) 

299 

300 def getInputSchema(self) -> KeyedDataSchema: 

301 yield from self.metric.getInputSchema() 

302 yield from self.plot.getInputSchema() 

303 

304 def getOutputNames(self, config: pexConfig.Config | None = None) -> Iterable[str]: 

305 """Returns a list of names that will be used as keys if this action's 

306 call method returns a mapping. Otherwise return an empty Iterable. 

307 

308 Parameters 

309 ---------- 

310 config : `lsst.pex.config.Config`, optional 

311 Configuration of the task. This is only used if the output naming 

312 needs to be config-aware. 

313 

314 Returns 

315 ------- 

316 outNames : `Iterable` of `str` 

317 If a `PlotAction` produces more than one plot, this should be the 

318 keys the action will use in the returned `Mapping`. 

319 """ 

320 if config is None: 

321 # `dynamicOutputNames` is set to False. 

322 outNames = self.plot.getOutputNames() 

323 else: 

324 # `dynamicOutputNames` is set to True. 

325 outNames = self.plot.getOutputNames(config=config) 

326 return outNames