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

81 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-03 13:22 +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 "PlotAction", 

33 "JointResults", 

34 "JointAction", 

35 "NoMetric", 

36 "NoPlot", 

37) 

38 

39import warnings 

40from abc import abstractmethod 

41from dataclasses import dataclass 

42from typing import Iterable 

43 

44import lsst.pex.config as pexConfig 

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

46 

47from ..contexts import ContextApplier 

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

49 

50 

51class AnalysisAction(ConfigurableAction): 

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

53 

54 This extends the basic `ConfigurableAction` class to include interfaces for 

55 defining what an action expects to consume, and what it expects to produce. 

56 """ 

57 

58 def __init_subclass__(cls, **kwargs): 

59 if "getInputSchema" not in dir(cls): 59 ↛ 60line 59 didn't jump to line 60, because the condition on line 59 was never true

60 raise NotImplementedError(f"Class {cls} must implement method getInputSchema") 

61 

62 # This is a descriptor that functions like a function in most contexts 

63 # and can be treated as such 

64 applyContext = ContextApplier() 

65 r"""Apply a `Context` to an `AnalysisAction` recursively. 

66 

67 Generally this method is called from within an `AnalysisTool` to 

68 configure all `AnalysisAction`\ s at one time to make sure that they 

69 all are consistently configured. However, it is permitted to call this 

70 method if you are aware of the effects, or from within a specific 

71 execution environment like a python shell or notebook. 

72 

73 Parameters 

74 ---------- 

75 context : `Context` 

76 The specific execution context, this may be a single context or 

77 a joint context, see `Context` for more info. 

78 """ 

79 

80 @abstractmethod 

81 def getInputSchema(self) -> KeyedDataSchema: 

82 """Return the schema an `AnalysisAction` expects to be present in the 

83 arguments supplied to the __call__ method. 

84 

85 Returns 

86 ------- 

87 result : `KeyedDataSchema` 

88 The schema this action requires to be present when calling this 

89 action, keys are unformatted. 

90 """ 

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

92 

93 def getOutputSchema(self) -> KeyedDataSchema | None: 

94 """Return the schema an `AnalysisAction` will produce, if the 

95 ``__call__`` method returns `KeyedData`, otherwise this may return 

96 None. 

97 

98 Returns 

99 ------- 

100 result : `KeyedDataSchema` or None 

101 The schema this action will produce when returning from call. This 

102 will be unformatted if any templates are present. Should return 

103 None if action does not return `KeyedData`. 

104 """ 

105 return None 

106 

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

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

109 by kwargs passed to this method. 

110 

111 Returns 

112 ------- 

113 result : `KeyedDataSchema` 

114 The schema this action requires to be present when calling this 

115 action, formatted with any input arguments (e.g. band='i') 

116 """ 

117 for key, typ in self.getInputSchema(): 

118 yield key.format_map(kwargs), typ 

119 

120 def addInputSchema(self, inputSchema: KeyedDataSchema) -> None: 

121 """Add the supplied inputSchema argument to the class such that it will 

122 be returned along side any other arguments in a call to 

123 ``getInputSchema``. 

124 

125 Parameters 

126 ---------- 

127 inputSchema : `KeyedDataSchema` 

128 A schema that is to be merged in with any existing schema when a 

129 call to ``getInputSchema`` is made. 

130 """ 

131 warnings.warn( 

132 f"{type(self)} does not implement adding input schemas, call will do nothing, " 

133 "this may be expected", 

134 RuntimeWarning, 

135 ) 

136 

137 

138class KeyedDataAction(AnalysisAction): 

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

140 called. 

141 """ 

142 

143 @abstractmethod 

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

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

146 

147 

148class VectorAction(AnalysisAction): 

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

150 called. 

151 """ 

152 

153 @abstractmethod 

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

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

156 

157 

158class TensorAction(AnalysisAction): 

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

160 called. 

161 """ 

162 

163 @abstractmethod 

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

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

166 

167 

168class ScalarAction(AnalysisAction): 

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

170 called. 

171 """ 

172 

173 @abstractmethod 

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

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

176 

177 Parameters 

178 ---------- 

179 data 

180 Keyed data to compute a value from. 

181 kwargs 

182 Additional keyword arguments. 

183 

184 Returns 

185 ------- 

186 A scalar value. 

187 """ 

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

189 

190 def getMask(self, **kwargs) -> Vector | slice: 

191 """Extract a mask if one is passed as key word args, otherwise return 

192 an empty slice object that can still be used in a getitem call. 

193 

194 Returns 

195 ------- 

196 result : `Vector` or `slice` 

197 The mask passed as a keyword, or a slice object that will return 

198 a complete Vector when used in getitem. 

199 """ 

200 if (mask := kwargs.get("mask")) is None: 

201 mask = slice(None) 

202 return mask 

203 

204 

205class MetricAction(AnalysisAction): 

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

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

208 """ 

209 

210 @abstractmethod 

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

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

213 

214 

215class PlotAction(AnalysisAction): 

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

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

218 """ 

219 

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

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

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

223 

224 Parameters 

225 ---------- 

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

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

228 needs to be config-aware. 

229 

230 Returns 

231 ------- 

232 result : `Iterable` of `str` 

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

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

235 """ 

236 return tuple() 

237 

238 @abstractmethod 

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

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

241 

242 

243class NoPlot(PlotAction): 

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

245 

246 

247class NoMetric(MetricAction): 

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

249 

250 

251@dataclass 

252class JointResults: 

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

254 `JointAction`. 

255 """ 

256 

257 plot: PlotResultType | None 

258 metric: MetricResultType | None 

259 

260 

261class JointAction(AnalysisAction): 

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

263 `PlotAction` and a `MetricAction`. 

264 """ 

265 

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

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

268 

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

270 if isinstance(self.plot, NoPlot): 

271 plots = None 

272 else: 

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

274 if isinstance(self.metric, NoMetric): 

275 metrics = None 

276 else: 

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

278 return JointResults(plots, metrics) 

279 

280 def getInputSchema(self) -> KeyedDataSchema: 

281 yield from self.metric.getInputSchema() 

282 yield from self.plot.getInputSchema() 

283 

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

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

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

287 

288 Parameters 

289 ---------- 

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

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

292 needs to be config-aware. 

293 

294 Returns 

295 ------- 

296 outNames : `Iterable` of `str` 

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

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

299 """ 

300 if config is None: 

301 # `dynamicOutputNames` is set to False. 

302 outNames = self.plot.getOutputNames() 

303 else: 

304 # `dynamicOutputNames` is set to True. 

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

306 return outNames