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

90 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-28 10:27 +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 

30from typing import cast 

31 

32import numpy as np 

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

34from lsst.pex.config.configurableActions import ConfigurableActionField 

35 

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

37 

38 

39class CalcE(VectorAction): 

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

41 

42 The complex ellipticity is typically defined as 

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

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

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

46 in GalSim (see Eq. 4.4. of Bartelmann and Schneider, 2001). 

47 The other definition differs in normalization. 

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

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

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

51 

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

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

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

55 artifacts. 

56 

57 References 

58 ---------- 

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

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

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

62 

63 Notes 

64 ----- 

65 

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

67 of the sources. 

68 

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

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

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

72 

73 See Also 

74 -------- 

75 CalcE1 

76 CalcE2 

77 """ 

78 

79 colXx = Field[str]( 

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

81 default="{band}_ixx", 

82 ) 

83 

84 colYy = Field[str]( 

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

86 default="{band}_iyy", 

87 ) 

88 

89 colXy = Field[str]( 

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

91 default="{band}_ixy", 

92 ) 

93 

94 ellipticityType = ChoiceField[str]( 

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

96 allowed={ 

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

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

99 }, 

100 default="distortion", 

101 ) 

102 

103 halvePhaseAngle = Field[bool]( 

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

105 default=False, 

106 ) 

107 

108 component = ChoiceField[str]( 

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

110 optional=True, 

111 allowed={ 

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

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

114 }, 

115 ) 

116 

117 def getInputSchema(self) -> KeyedDataSchema: 

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

119 

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

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

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

123 ) 

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

125 

126 if self.ellipticityType == "shear": 

127 denom += 2 * np.sqrt( 

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

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

130 ) 

131 e = cast(Vector, e) 

132 denom = cast(Vector, denom) 

133 

134 e /= denom 

135 

136 if self.halvePhaseAngle: 

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

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

139 # instead of the more expensive trig calls. 

140 e *= np.abs(e) 

141 e = np.sqrt(e) 

142 

143 if self.component == "1": 

144 return np.real(e) 

145 elif self.component == "2": 

146 return np.imag(e) 

147 else: 

148 return e 

149 

150 

151class CalcEDiff(VectorAction): 

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

153 

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

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

156 

157 See Also 

158 -------- 

159 CalcE 

160 

161 Notes 

162 ----- 

163 

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

165 of the sources. 

166 

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

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

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

170 """ 

171 

172 colA = ConfigurableActionField[VectorAction]( 

173 doc="Ellipticity to subtract from", 

174 default=CalcE, 

175 ) 

176 

177 colB = ConfigurableActionField[VectorAction]( 

178 doc="Ellipticity to subtract", 

179 dtype=VectorAction, 

180 default=CalcE, 

181 ) 

182 

183 halvePhaseAngle = Field[bool]( 

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

185 default=False, 

186 ) 

187 

188 component = ChoiceField[str]( 

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

190 optional=True, 

191 allowed={ 

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

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

194 }, 

195 ) 

196 

197 def getInputSchema(self) -> KeyedDataSchema: 

198 yield from self.colA.getInputSchema() 

199 yield from self.colB.getInputSchema() 

200 

201 def validate(self): 

202 super().validate() 

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

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

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

206 

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

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

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

210 eDiff = eMeas - ePSF 

211 if self.halvePhaseAngle: 

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

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

214 # instead of the more expensive trig calls. 

215 eDiff *= np.abs(eDiff) 

216 eDiff = np.sqrt(eDiff) 

217 

218 if self.component == "1": 

219 return np.real(eDiff) 

220 elif self.component == "2": 

221 return np.imag(eDiff) 

222 else: 

223 return eDiff 

224 

225 

226class CalcE1(VectorAction): 

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

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

229 

230 See Also 

231 -------- 

232 CalcE 

233 CalcE2 

234 

235 Note 

236 ---- 

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

238 of the sources. 

239 """ 

240 

241 colXx = Field[str]( 

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

243 default="{band}_ixx", 

244 ) 

245 

246 colYy = Field[str]( 

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

248 default="{band}_iyy", 

249 ) 

250 

251 colXy = Field[str]( 

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

253 default="{band}_ixy", 

254 optional=True, 

255 ) 

256 

257 ellipticityType = ChoiceField[str]( 

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

259 allowed={ 

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

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

262 }, 

263 default="distortion", 

264 ) 

265 

266 def getInputSchema(self) -> KeyedDataSchema: 

267 if self.ellipticityType == "distortion": 

268 return ( 

269 (self.colXx, Vector), 

270 (self.colYy, Vector), 

271 ) 

272 else: 

273 return ( 

274 (self.colXx, Vector), 

275 (self.colYy, Vector), 

276 (self.colXy, Vector), 

277 ) 

278 

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

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

281 if self.ellipticityType == "shear": 

282 denom += 2 * np.sqrt( 

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

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

285 ) 

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

287 

288 return cast(Vector, e1) 

289 

290 def validate(self): 

291 super().validate() 

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

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

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

295 

296 

297class CalcE2(VectorAction): 

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

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

300 

301 See Also 

302 -------- 

303 CalcE 

304 CalcE1 

305 

306 Note 

307 ---- 

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

309 of the sources. 

310 """ 

311 

312 colXx = Field[str]( 

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

314 default="{band}_ixx", 

315 ) 

316 

317 colYy = Field[str]( 

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

319 default="{band}_iyy", 

320 ) 

321 

322 colXy = Field[str]( 

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

324 default="{band}_ixy", 

325 ) 

326 

327 ellipticityType = ChoiceField[str]( 

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

329 allowed={ 

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

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

332 }, 

333 default="distortion", 

334 ) 

335 

336 def getInputSchema(self) -> KeyedDataSchema: 

337 return ( 

338 (self.colXx, Vector), 

339 (self.colYy, Vector), 

340 (self.colXy, Vector), 

341 ) 

342 

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

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

345 if self.ellipticityType == "shear": 

346 denom += 2 * np.sqrt( 

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

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

349 ) 

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

351 return cast(Vector, e2)