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

87 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-25 12:23 +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 (see Eq. 4.4. of Bartelmann and Schneider, 2001). 

45 The other definition differs in normalization. 

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

47 in GalSim (see Eq. 4.10 of Bartelmann and Schneider (2001). It is defined 

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

49 

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

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

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

53 artifacts. 

54 

55 References 

56 ---------- 

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

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

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

60 

61 Notes 

62 ----- 

63 

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

65 of the sources. 

66 

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

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

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

70 

71 See Also 

72 -------- 

73 CalcE1 

74 CalcE2 

75 """ 

76 

77 colXx = Field[str]( 

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

79 default="{band}_ixx", 

80 ) 

81 

82 colYy = Field[str]( 

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

84 default="{band}_iyy", 

85 ) 

86 

87 colXy = Field[str]( 

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

89 default="{band}_ixy", 

90 ) 

91 

92 ellipticityType = ChoiceField[str]( 

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

94 allowed={ 

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

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

97 }, 

98 default="distortion", 

99 ) 

100 

101 halvePhaseAngle = Field[bool]( 

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

103 default=False, 

104 ) 

105 

106 component = ChoiceField[str]( 

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

108 optional=True, 

109 allowed={ 

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

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

112 }, 

113 ) 

114 

115 def getInputSchema(self) -> KeyedDataSchema: 

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

117 

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

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

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

121 ) 

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

123 

124 if self.ellipticityType == "shear": 

125 denom += 2 * np.sqrt( 

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

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

128 ) 

129 

130 e /= denom 

131 

132 if self.halvePhaseAngle: 

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

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

135 # instead of the more expensive trig calls. 

136 e *= np.abs(e) 

137 e = np.sqrt(e) 

138 

139 if self.component == "1": 

140 return np.real(e) 

141 elif self.component == "2": 

142 return np.imag(e) 

143 else: 

144 return e 

145 

146 

147class CalcEDiff(VectorAction): 

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

149 

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

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

152 

153 See Also 

154 -------- 

155 CalcE 

156 

157 Notes 

158 ----- 

159 

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

161 of the sources. 

162 

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

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

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

166 """ 

167 

168 colA = ConfigurableActionField( 

169 doc="Ellipticity to subtract from", 

170 dtype=VectorAction, 

171 default=CalcE, 

172 ) 

173 

174 colB = ConfigurableActionField( 

175 doc="Ellipticity to subtract", 

176 dtype=VectorAction, 

177 default=CalcE, 

178 ) 

179 

180 halvePhaseAngle = Field[bool]( 

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

182 default=False, 

183 ) 

184 

185 component = ChoiceField[str]( 

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

187 optional=True, 

188 allowed={ 

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

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

191 }, 

192 ) 

193 

194 def getInputSchema(self) -> KeyedDataSchema: 

195 yield from self.colA.getInputSchema() 

196 yield from self.colB.getInputSchema() 

197 

198 def validate(self): 

199 super().validate() 

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

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

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

203 

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

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

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

207 eDiff = eMeas - ePSF 

208 if self.halvePhaseAngle: 

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

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

211 # instead of the more expensive trig calls. 

212 eDiff *= np.abs(eDiff) 

213 eDiff = np.sqrt(eDiff) 

214 

215 if self.component == "1": 

216 return np.real(eDiff) 

217 elif self.component == "2": 

218 return np.imag(eDiff) 

219 else: 

220 return eDiff 

221 

222 

223class CalcE1(VectorAction): 

224 """Calculate distortion-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or 

225 shear-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)). 

226 

227 See Also 

228 -------- 

229 CalcE 

230 CalcE2 

231 

232 Note 

233 ---- 

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

235 of the sources. 

236 """ 

237 

238 colXx = Field[str]( 

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

240 default="{band}_ixx", 

241 ) 

242 

243 colYy = Field[str]( 

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

245 default="{band}_iyy", 

246 ) 

247 

248 colXy = Field[str]( 

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

250 default="{band}_ixy", 

251 optional=True, 

252 ) 

253 

254 ellipticityType = ChoiceField[str]( 

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

256 allowed={ 

257 "distortion": "Distortion, measured as (Ixx - Iyy)/(Ixx + Iyy)", 

258 "shear": ("Shear, measured as (Ixx - Iyy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"), 

259 }, 

260 default="distortion", 

261 ) 

262 

263 def getInputSchema(self) -> KeyedDataSchema: 

264 if self.ellipticityType == "distortion": 

265 return ( 

266 (self.colXx, Vector), 

267 (self.colYy, Vector), 

268 ) 

269 else: 

270 return ( 

271 (self.colXx, Vector), 

272 (self.colYy, Vector), 

273 (self.colXy, Vector), 

274 ) 

275 

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

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

278 if self.ellipticityType == "shear": 

279 denom += 2 * np.sqrt( 

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

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

282 ) 

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

284 

285 return e1 

286 

287 def validate(self): 

288 super().validate() 

289 if self.ellipticityType == "shear" and self.colXy is None: 

290 msg = "colXy is required for shear-type shear ellipticity" 

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

292 

293 

294class CalcE2(VectorAction): 

295 """Calculate distortion-type e2 = 2Ixy/(Ixx+Iyy) or 

296 shear-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)). 

297 

298 See Also 

299 -------- 

300 CalcE 

301 CalcE1 

302 

303 Note 

304 ---- 

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

306 of the sources. 

307 """ 

308 

309 colXx = Field[str]( 

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

311 default="{band}_ixx", 

312 ) 

313 

314 colYy = Field[str]( 

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

316 default="{band}_iyy", 

317 ) 

318 

319 colXy = Field[str]( 

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

321 default="{band}_ixy", 

322 ) 

323 

324 ellipticityType = ChoiceField[str]( 

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

326 allowed={ 

327 "distortion": "Distortion, defined as 2*Ixy/(Ixx + Iyy)", 

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

329 }, 

330 default="distortion", 

331 ) 

332 

333 def getInputSchema(self) -> KeyedDataSchema: 

334 return ( 

335 (self.colXx, Vector), 

336 (self.colYy, Vector), 

337 (self.colXy, Vector), 

338 ) 

339 

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

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

342 if self.ellipticityType == "shear": 

343 denom += 2 * np.sqrt( 

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

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

346 ) 

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

348 return e2