Coverage for python/lsst/analysis/tools/actions/vector/ellipticity.py: 34%

87 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-22 09:56 +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/>. 

21from __future__ import annotations 

22 

23__all__ = ( 

24 "CalcE", 

25 "CalcEDiff", 

26 "CalcE1", 

27 "CalcE2", 

28) 

29 

30import numpy as np 

31from lsst.pex.config import ChoiceField, Field, FieldValidationError 

32from lsst.pipe.tasks.configurableActions import ConfigurableActionField 

33 

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

35 

36 

37class CalcE(VectorAction): 

38 """Calculate a complex value representation of the ellipticity. 

39 

40 The complex ellipticity is typically defined as 

41 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is 

42 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments. 

43 This is sometimes referred to as distortion, and denoted by e = (e1, e2) 

44 in GalSim and referred to as chi-type ellipticity following the notation 

45 in Eq. 4.4 of Bartelmann and Schneider (2001). The other definition differs 

46 in normalization. It is referred to as shear, and denoted by g = (g1, g2) 

47 in GalSim and referred to as epsilon-type ellipticity again following the 

48 notation in Eq. 4.10 of Bartelmann and Schneider (2001). It is defined as 

49 g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)). 

50 

51 The shear measure is unbiased in weak-lensing shear, but may exclude some 

52 objects in the presence of noisy moment estimates. The distortion measure 

53 is biased in weak-lensing distortion, but does not suffer from selection 

54 artifacts. 

55 

56 References 

57 ---------- 

58 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”, 

59 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001. 

60 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508 

61 

62 Notes 

63 ----- 

64 

65 1. This is a shape measurement used for doing QA on the ellipticity 

66 of the sources. 

67 

68 2. For plotting purposes we might want to plot |E|*exp(i*theta). 

69 If `halvePhaseAngle` config parameter is set to `True`, then 

70 the returned quantity therefore corresponds to |E|*exp(i*theta). 

71 

72 See Also 

73 -------- 

74 CalcE1 

75 CalcE2 

76 """ 

77 

78 colXx = Field[str]( 

79 doc="The column name to get the xx shape component from.", 

80 default="{band}_ixx", 

81 ) 

82 

83 colYy = Field[str]( 

84 doc="The column name to get the yy shape component from.", 

85 default="{band}_iyy", 

86 ) 

87 

88 colXy = Field[str]( 

89 doc="The column name to get the xy shape component from.", 

90 default="{band}_ixy", 

91 ) 

92 

93 ellipticityType = ChoiceField[str]( 

94 doc="The type of ellipticity to calculate", 

95 allowed={ 

96 "chi": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/" "(Ixx + Iyy)"), 

97 "epsilon": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"), 

98 }, 

99 default="chi", 

100 ) 

101 

102 halvePhaseAngle = Field[bool]( 

103 doc="Divide the phase angle by 2? Suitable for quiver plots.", 

104 default=False, 

105 ) 

106 

107 component = ChoiceField[str]( 

108 doc="Which component of the ellipticity to return. If `None`, return complex ellipticity values.", 

109 optional=True, 

110 allowed={ 

111 "1": "e1 or g1 (depending on `ellipticityType`)", 

112 "2": "e2 or g2 (depending on `ellipticityType`)", 

113 }, 

114 ) 

115 

116 def getInputSchema(self) -> KeyedDataSchema: 

117 return ((self.colXx, Vector), (self.colXy, Vector), (self.colYy, Vector)) 

118 

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

120 e = (data[self.colXx.format(**kwargs)] - data[self.colYy.format(**kwargs)]) + 1j * ( 

121 2 * data[self.colXy.format(**kwargs)] 

122 ) 

123 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)] 

124 

125 if self.ellipticityType == "epsilon": 

126 denom += 2 * np.sqrt( 

127 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)] 

128 - data[self.colXy.format(**kwargs)] ** 2 

129 ) 

130 

131 e /= denom 

132 

133 if self.halvePhaseAngle: 

134 # Ellipiticity is |e|*exp(i*2*theta), but we want to return 

135 # |e|*exp(i*theta). So we multiply by |e| and take its square root 

136 # instead of the more expensive trig calls. 

137 e *= np.abs(e) 

138 e = np.sqrt(e) 

139 

140 if self.component == "1": 

141 return np.real(e) 

142 elif self.component == "2": 

143 return np.imag(e) 

144 else: 

145 return e 

146 

147 

148class CalcEDiff(VectorAction): 

149 """Calculate the difference of two ellipticities as a complex quantity. 

150 

151 The complex ellipticity difference between e_A and e_B is defined as 

152 e_A - e_B = de = |de|exp(j*2*theta). 

153 

154 See Also 

155 -------- 

156 CalcE 

157 

158 Notes 

159 ----- 

160 

161 1. This is a shape measurement used for doing QA on the ellipticity 

162 of the sources. 

163 

164 2. For plotting purposes we might want to plot |de|*exp(j*theta). 

165 If `halvePhaseAngle` config parameter is set to `True`, then 

166 the returned quantity therefore corresponds to |e|*exp(j*theta). 

167 """ 

168 

169 colA = ConfigurableActionField( 

170 doc="Ellipticity to subtract from", 

171 dtype=VectorAction, 

172 default=CalcE, 

173 ) 

174 

175 colB = ConfigurableActionField( 

176 doc="Ellipticity to subtract", 

177 dtype=VectorAction, 

178 default=CalcE, 

179 ) 

180 

181 halvePhaseAngle = Field[bool]( 

182 doc="Divide the phase angle by 2? Suitable for quiver plots.", 

183 default=False, 

184 ) 

185 

186 component = ChoiceField[str]( 

187 doc="Which component of the ellipticity to return. If `None`, return complex ellipticity values.", 

188 optional=True, 

189 allowed={ 

190 "1": "e1 or g1 (depending on the `ellipiticyType`)", 

191 "2": "e2 or g2 (depending on the `ellipiticyType`)", 

192 }, 

193 ) 

194 

195 def getInputSchema(self) -> KeyedDataSchema: 

196 yield from self.colA.getInputSchema() 

197 yield from self.colB.getInputSchema() 

198 

199 def validate(self): 

200 super().validate() 

201 if self.colA.ellipticityType != self.colB.ellipticityType: 

202 msg = "Both the ellipticities in CalcEDiff must have the same type." 

203 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg) 

204 

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

206 eMeas = self.colA(data, **kwargs) 

207 ePSF = self.colB(data, **kwargs) 

208 eDiff = eMeas - ePSF 

209 if self.halvePhaseAngle: 

210 # Ellipiticity is |e|*exp(i*2*theta), but we want to return 

211 # |e|*exp(j*theta). So we multiply by |e| and take its square root 

212 # instead of the more expensive trig calls. 

213 eDiff *= np.abs(eDiff) 

214 eDiff = np.sqrt(eDiff) 

215 

216 if self.component == "1": 

217 return np.real(eDiff) 

218 elif self.component == "2": 

219 return np.imag(eDiff) 

220 else: 

221 return eDiff 

222 

223 

224class CalcE1(VectorAction): 

225 """Calculate chi-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or 

226 epsilon-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)). 

227 

228 See Also 

229 -------- 

230 CalcE 

231 CalcE2 

232 

233 Note 

234 ---- 

235 This is a shape measurement used for doing QA on the ellipticity 

236 of the sources. 

237 """ 

238 

239 colXx = Field[str]( 

240 doc="The column name to get the xx shape component from.", 

241 default="{band}_ixx", 

242 ) 

243 

244 colYy = Field[str]( 

245 doc="The column name to get the yy shape component from.", 

246 default="{band}_iyy", 

247 ) 

248 

249 colXy = Field[str]( 

250 doc="The column name to get the xy shape component from.", 

251 default="{band}_ixy", 

252 optional=True, 

253 ) 

254 

255 ellipticityType = ChoiceField[str]( 

256 doc="The type of ellipticity to calculate", 

257 allowed={ 

258 "chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)", 

259 "epsilon": ("Shear, defined as (Ixx - Iyy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"), 

260 }, 

261 default="chi", 

262 ) 

263 

264 def getInputSchema(self) -> KeyedDataSchema: 

265 if self.ellipticityType == "chi": 

266 return ( 

267 (self.colXx, Vector), 

268 (self.colYy, Vector), 

269 ) 

270 else: 

271 return ( 

272 (self.colXx, Vector), 

273 (self.colYy, Vector), 

274 (self.colXy, Vector), 

275 ) 

276 

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

278 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)] 

279 if self.ellipticityType == "epsilon": 

280 denom += 2 * np.sqrt( 

281 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)] 

282 - data[self.colXy.format(**kwargs)] ** 2 

283 ) 

284 e1 = (data[self.colXx.format(**kwargs)] - data[self.colYy.format(**kwargs)]) / denom 

285 

286 return e1 

287 

288 def validate(self): 

289 super().validate() 

290 if self.ellipticityType == "epsilon" and self.colXy is None: 

291 msg = "colXy is required for epsilon-type shear ellipticity" 

292 raise FieldValidationError(self.__class__.colXy, self, msg) 

293 

294 

295class CalcE2(VectorAction): 

296 """Calculate chi-type e2 = 2Ixy/(Ixx+Iyy) or 

297 epsilon-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)). 

298 

299 See Also 

300 -------- 

301 CalcE 

302 CalcE1 

303 

304 Note 

305 ---- 

306 This is a shape measurement used for doing QA on the ellipticity 

307 of the sources. 

308 """ 

309 

310 colXx = Field[str]( 

311 doc="The column name to get the xx shape component from.", 

312 default="{band}_ixx", 

313 ) 

314 

315 colYy = Field[str]( 

316 doc="The column name to get the yy shape component from.", 

317 default="{band}_iyy", 

318 ) 

319 

320 colXy = Field[str]( 

321 doc="The column name to get the xy shape component from.", 

322 default="{band}_ixy", 

323 ) 

324 

325 ellipticityType = ChoiceField[str]( 

326 doc="The type of ellipticity to calculate", 

327 allowed={ 

328 "chi": "Distortion, defined as 2*Ixy/(Ixx + Iyy)", 

329 "epsilon": ("Shear, defined as 2*Ixy/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"), 

330 }, 

331 default="chi", 

332 ) 

333 

334 def getInputSchema(self) -> KeyedDataSchema: 

335 return ( 

336 (self.colXx, Vector), 

337 (self.colYy, Vector), 

338 (self.colXy, Vector), 

339 ) 

340 

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

342 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)] 

343 if self.ellipticityType == "epsilon": 

344 denom += 2 * np.sqrt( 

345 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)] 

346 - data[self.colXy.format(**kwargs)] ** 2 

347 ) 

348 e2 = 2 * data[self.colXy.format(**kwargs)] / denom 

349 return e2