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

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) 

36 

37import warnings 

38from abc import abstractmethod 

39from dataclasses import dataclass 

40from typing import Iterable 

41 

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

43 

44from ..contexts import ContextApplier 

45from ._interfaces import KeyedData, KeyedDataSchema, MetricResultType, PlotResultType, Scalar, Vector 

46 

47 

48class AnalysisAction(ConfigurableAction): 

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

50 

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

54 

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

58 

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. 

63 

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. 

69 

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

76 

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. 

81 

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

89 

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. 

94 

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 

103 

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

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

106 by kwargs passed to this method. 

107 

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 

116 

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

121 

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 ) 

133 

134 

135class KeyedDataAction(AnalysisAction): 

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

137 called. 

138 """ 

139 

140 @abstractmethod 

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

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

143 

144 

145class VectorAction(AnalysisAction): 

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

147 called. 

148 """ 

149 

150 @abstractmethod 

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

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

153 

154 

155class ScalarAction(AnalysisAction): 

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

157 called. 

158 """ 

159 

160 @abstractmethod 

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

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

163 

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. 

167 

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 

177 

178 

179class MetricAction(AnalysisAction): 

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

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

182 """ 

183 

184 @abstractmethod 

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

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

187 

188 

189class PlotAction(AnalysisAction): 

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

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

192 """ 

193 

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 

197 

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

205 

206 @abstractmethod 

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

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

209 

210 

211class NoPlot(PlotAction): 

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

213 

214 

215class NoMetric(MetricAction): 

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

217 

218 

219@dataclass 

220class JointResults: 

221 plot: PlotResultType | None 

222 metric: MetricResultType | None 

223 

224 

225class JointAction(AnalysisAction): 

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

227 `PlotAction` and a `MetricAction` 

228 """ 

229 

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

232 

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) 

243 

244 def getInputSchema(self) -> KeyedDataSchema: 

245 yield from self.metric.getInputSchema() 

246 yield from self.plot.getInputSchema() 

247 

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 

251 

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