Coverage for python/lsst/analysis/tools/actions/vector/vectorActions.py: 50%

115 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-04 03:18 -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/>. 

21from __future__ import annotations 

22 

23import logging 

24from typing import cast 

25 

26import numpy as np 

27from astropy import units as u 

28from lsst.pex.config import DictField, Field 

29from lsst.pipe.tasks.configurableActions import ConfigurableActionField 

30 

31from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction 

32from .selectors import VectorSelector 

33 

34_LOG = logging.getLogger(__name__) 

35 

36 

37class DownselectVector(VectorAction): 

38 """Get a vector from KeyedData, apply specified selector, return the 

39 shorter Vector. 

40 """ 

41 

42 vectorKey = Field[str](doc="column key to load from KeyedData") 

43 

44 selector = ConfigurableActionField(doc="Action which returns a selection mask", default=VectorSelector) 

45 

46 def getInputSchema(self) -> KeyedDataSchema: 

47 yield (self.vectorKey, Vector) 

48 yield from cast(VectorAction, self.selector).getInputSchema() 

49 

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

51 mask = cast(VectorAction, self.selector)(data, **kwargs) 

52 return cast(Vector, data[self.vectorKey.format(**kwargs)])[mask] 

53 

54 

55class MagColumnNanoJansky(VectorAction): 

56 vectorKey = Field[str](doc="column key to use for this transformation") 

57 

58 def getInputSchema(self) -> KeyedDataSchema: 

59 return ((self.vectorKey, Vector),) 

60 

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

62 with np.warnings.catch_warnings(): # type: ignore 

63 np.warnings.filterwarnings("ignore", r"invalid value encountered") # type: ignore 

64 np.warnings.filterwarnings("ignore", r"divide by zero") # type: ignore 

65 vec = cast(Vector, data[self.vectorKey.format(**kwargs)]) 

66 return np.array(-2.5 * np.log10((vec * 1e-9) / 3631.0)) # type: ignore 

67 

68 

69class FractionalDifference(VectorAction): 

70 """Calculate (A-B)/B""" 

71 

72 actionA = ConfigurableActionField(doc="Action which supplies vector A", dtype=VectorAction) 

73 actionB = ConfigurableActionField(doc="Action which supplies vector B", dtype=VectorAction) 

74 

75 def getInputSchema(self) -> KeyedDataSchema: 

76 yield from self.actionA.getInputSchema() # type: ignore 

77 yield from self.actionB.getInputSchema() # type: ignore 

78 

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

80 vecA = self.actionA(data, **kwargs) # type: ignore 

81 vecB = self.actionB(data, **kwargs) # type: ignore 

82 return (vecA - vecB) / vecB 

83 

84 

85class LoadVector(VectorAction): 

86 """Load and return a Vector from KeyedData""" 

87 

88 vectorKey = Field[str](doc="Key of vector which should be loaded") 

89 

90 def getInputSchema(self) -> KeyedDataSchema: 

91 return ((self.vectorKey, Vector),) 

92 

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

94 return np.array(cast(Vector, data[self.vectorKey.format(**kwargs)])) 

95 

96 

97class MagDiff(VectorAction): 

98 """Calculate the difference between two magnitudes; 

99 each magnitude is derived from a flux column. 

100 Parameters 

101 ---------- 

102 TO DO: 

103 Returns 

104 ------- 

105 The magnitude difference in milli mags. 

106 Notes 

107 ----- 

108 The flux columns need to be in units (specifiable in 

109 the fluxUnits1 and 2 config options) that can be converted 

110 to janskies. This action doesn't have any calibration 

111 information and assumes that the fluxes are already 

112 calibrated. 

113 """ 

114 

115 col1 = Field[str](doc="Column to subtract from") 

116 fluxUnits1 = Field[str](doc="Units for col1", default="nanojansky") 

117 col2 = Field[str](doc="Column to subtract") 

118 fluxUnits2 = Field[str](doc="Units for col2", default="nanojansky") 

119 returnMillimags = Field[bool](doc="Use millimags or not?", default=True) 

120 

121 def getInputSchema(self) -> KeyedDataSchema: 

122 return ((self.col1, Vector), (self.col2, Vector)) 

123 

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

125 flux1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.fluxUnits1) 

126 mag1 = flux1.to(u.ABmag) 

127 

128 flux2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.fluxUnits2) 

129 mag2 = flux2.to(u.ABmag) 

130 

131 magDiff = mag1 - mag2 

132 

133 if self.returnMillimags: 

134 magDiff = magDiff.to(u.mmag) 

135 

136 return np.array(magDiff.value) 

137 

138 

139class SNCalculator(VectorAction): 

140 """Calculate the signal-to-noise.""" 

141 

142 fluxType = Field[str](doc="Flux type to calculate the S/N.", default="{band}_psfFlux") 

143 uncertaintySuffix = Field[str]( 

144 doc="Suffix to add to fluxType to specify the uncertainty column", default="Err" 

145 ) 

146 

147 def getInputSchema(self) -> KeyedDataSchema: 

148 yield self.fluxType, Vector 

149 yield f"{self.fluxType}{self.uncertaintySuffix}", Vector 

150 

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

152 signal = np.array(data[self.fluxType.format(**kwargs)]) 

153 noise = np.array(data[f"{self.fluxType}{self.uncertaintySuffix}".format(**kwargs)]) 

154 sn = signal / noise 

155 

156 return np.array(sn) 

157 

158 

159class ExtinctionCorrectedMagDiff(VectorAction): 

160 """Compute the difference between two magnitudes and correct for extinction 

161 By default bands are derived from the <band>_ prefix on flux columns, 

162 per the naming convention in the Object Table: 

163 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another 

164 convention, bands can alternatively be supplied via the band1 or band2 

165 config parameters. 

166 If band1 and band2 are supplied, the flux column names are ignored. 

167 """ 

168 

169 magDiff = ConfigurableActionField( 

170 doc="Action that returns a difference in magnitudes", default=MagDiff, dtype=VectorAction 

171 ) 

172 ebvCol = Field[str](doc="E(B-V) Column Name", default="ebv") 

173 band1 = Field[str]( 

174 doc="Optional band for magDiff.col1. Supercedes column name prefix", 

175 optional=True, 

176 default=None, 

177 ) 

178 band2 = Field[str]( 

179 doc="Optional band for magDiff.col2. Supercedes column name prefix", 

180 optional=True, 

181 default=None, 

182 ) 

183 extinctionCoeffs = DictField[str, float]( 

184 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band." 

185 "Key must be the band", 

186 optional=True, 

187 default=None, 

188 ) 

189 

190 def getInputSchema(self) -> KeyedDataSchema: 

191 return self.magDiff.getInputSchema() + ((self.ebvCol, Vector),) 

192 

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

194 diff = self.magDiff(data, **kwargs) 

195 if not self.extinctionCoeffs: 

196 _LOG.warning("No extinction Coefficients. Not applying extinction correction") 

197 return diff 

198 

199 col1Band = self.band1 if self.band1 else self.magDiff.col1.split("_")[0] 

200 col2Band = self.band2 if self.band2 else self.magDiff.col2.split("_")[0] 

201 

202 # Return plain MagDiff with warning if either coeff not found 

203 for band in (col1Band, col2Band): 

204 if band not in self.extinctionCoeffs: 

205 _LOG.warning( 

206 "%s band not found in coefficients dictionary: %s" " Not applying extinction correction", 

207 band, 

208 self.extinctionCoeffs, 

209 ) 

210 return diff 

211 

212 av1: float = self.extinctionCoeffs[col1Band] 

213 av2: float = self.extinctionCoeffs[col2Band] 

214 

215 ebv = data[self.ebvCol] 

216 # Ignore type until a more complete Vector protocol 

217 correction = np.array((av1 - av2) * ebv) * u.mag # type: ignore 

218 

219 if self.magDiff.returnMillimags: 

220 correction = correction.to(u.mmag) 

221 

222 return np.array(diff - correction.value) 

223 

224 

225class AstromDiff(VectorAction): 

226 """Calculate the difference between two columns, assuming their units 

227 are degrees, and convert the difference to arcseconds. 

228 Parameters 

229 ---------- 

230 df : `pandas.core.frame.DataFrame` 

231 The catalog to calculate the position difference from. 

232 Returns 

233 ------- 

234 angleDiffValue : `np.ndarray` 

235 The difference between two columns, either in the input units or in 

236 milliarcseconds. 

237 Notes 

238 ----- 

239 The columns need to be in units (specifiable in the radecUnits1 and 2 

240 config options) that can be converted to arcseconds. This action doesn't 

241 have any calibration information and assumes that the positions are already 

242 calibrated. 

243 """ 

244 

245 col1 = Field[str](doc="Column to subtract from", dtype=str) 

246 radecUnits1 = Field[str](doc="Units for col1", dtype=str, default="degree") 

247 col2 = Field[str](doc="Column to subtract", dtype=str) 

248 radecUnits2 = Field[str](doc="Units for col2", dtype=str, default="degree") 

249 returnMilliArcsecs = Field[bool](doc="Use marcseconds or not?", dtype=bool, default=True) 

250 

251 def getInputSchema(self) -> KeyedDataSchema: 

252 return ((self.col1, Vector), (self.col2, Vector)) 

253 

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

255 angle1 = np.array(data[self.col1.format(**kwargs)]) * u.Unit(self.radecUnits1) 

256 

257 angle2 = np.array(data[self.col2.format(**kwargs)]) * u.Unit(self.radecUnits2) 

258 

259 angleDiff = angle1 - angle2 

260 

261 if self.returnMilliArcsecs: 

262 angleDiffValue = angleDiff.to(u.arcsec).value * 1000 

263 else: 

264 angleDiffValue = angleDiff.value 

265 return angleDiffValue