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

73 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-08 07:15 -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 "NoMetric", 

36 "NoPlot", 

37) 

38 

39import warnings 

40from abc import abstractmethod 

41from dataclasses import dataclass 

42from typing import Iterable 

43 

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

45 

46from ..contexts import ContextApplier 

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

48 

49 

50class AnalysisAction(ConfigurableAction): 

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

52 

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

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

55 """ 

56 

57 def __init_subclass__(cls, **kwargs): 

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

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

60 

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

62 # and can be treated as such 

63 applyContext = ContextApplier() 

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

65 

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

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

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

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

70 execution environment like a python shell or notebook. 

71 

72 Parameters 

73 ---------- 

74 context : `Context` 

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

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

77 """ 

78 

79 @abstractmethod 

80 def getInputSchema(self) -> KeyedDataSchema: 

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

82 arguments supplied to the __call__ method. 

83 

84 Returns 

85 ------- 

86 result : `KeyedDataSchema` 

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

88 action, keys are unformatted. 

89 """ 

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

91 

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

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

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

95 None. 

96 

97 Returns 

98 ------- 

99 result : `KeyedDataSchema` or None 

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

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

102 None if action does not return `KeyedData`. 

103 """ 

104 return None 

105 

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

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

108 by kwargs passed to this method. 

109 

110 Returns 

111 ------- 

112 result : `KeyedDataSchema` 

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

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

115 """ 

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

117 yield key.format_map(kwargs), typ 

118 

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

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

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

122 ``getInputSchema``. 

123 

124 Parameters 

125 ---------- 

126 inputSchema : `KeyedDataSchema` 

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

128 call to ``getInputSchema`` is made. 

129 """ 

130 warnings.warn( 

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

132 "this may be expected", 

133 RuntimeWarning, 

134 ) 

135 

136 

137class KeyedDataAction(AnalysisAction): 

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

139 called. 

140 """ 

141 

142 @abstractmethod 

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

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

145 

146 

147class VectorAction(AnalysisAction): 

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

149 called. 

150 """ 

151 

152 @abstractmethod 

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

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

155 

156 

157class ScalarAction(AnalysisAction): 

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

159 called. 

160 """ 

161 

162 @abstractmethod 

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

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

165 

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

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

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

169 

170 Returns 

171 ------- 

172 result : `Vector` or `slice` 

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

174 a complete Vector when used in getitem. 

175 """ 

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

177 mask = slice(None) 

178 return mask 

179 

180 

181class MetricAction(AnalysisAction): 

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

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

184 """ 

185 

186 @abstractmethod 

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

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

189 

190 

191class PlotAction(AnalysisAction): 

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

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

194 """ 

195 

196 def getOutputNames(self) -> Iterable[str]: 

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

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

199 

200 Returns 

201 ------- 

202 result : `Iterable` of `str` 

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

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

205 """ 

206 return tuple() 

207 

208 @abstractmethod 

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

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

211 

212 

213class NoPlot(PlotAction): 

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

215 

216 

217class NoMetric(MetricAction): 

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

219 

220 

221@dataclass 

222class JointResults: 

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

224 `JointAction`. 

225 """ 

226 

227 plot: PlotResultType | None 

228 metric: MetricResultType | None 

229 

230 

231class JointAction(AnalysisAction): 

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

233 `PlotAction` and a `MetricAction`. 

234 """ 

235 

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

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

238 

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

240 if isinstance(self.plot, NoPlot): 

241 plots = None 

242 else: 

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

244 if isinstance(self.metric, NoMetric): 

245 metrics = None 

246 else: 

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

248 return JointResults(plots, metrics) 

249 

250 def getInputSchema(self) -> KeyedDataSchema: 

251 yield from self.metric.getInputSchema() 

252 yield from self.plot.getInputSchema() 

253 

254 def getOutputNames(self) -> Iterable[str]: 

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

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

257 

258 Returns 

259 ------- 

260 result : `Iterable` of `str` 

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

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

263 """ 

264 return self.plot.getOutputNames()