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

90 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-02 11:55 -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 

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 r"""Calculate a complex value representation of the ellipticity. 

41 

42 The complex ellipticity is typically defined as 

43 .. math:: 

44 

45 e = |e|\exp{(j*2*theta)} 

46 e = \\frac{((Ixx - Iyy) + j*(2*Ixy))}{(Ixx + Iyy)}, 

47 

48 where j is the square root of -1 and Ixx, Iyy, Ixy are second-order 

49 central moments. This is sometimes referred to as distortion, and denoted 

50 by e = (e1, e2) in GalSim (see Eq. 4.4. of Bartelmann and Schneider, 2001). 

51 The other definition differs in normalization. 

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

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

54 as 

55 .. math:: 

56 

57 g = \\frac{((Ixx-Iyy)+j*(2*Ixy))}{(Ixx+Iyy+2\sqrt{(Ixx*Iyy-Ixy^{2})})}. 

58 

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

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

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

62 artifacts. 

63 

64 References 

65 ---------- 

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

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

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

69 

70 Notes 

71 ----- 

72 

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

74 of the sources. 

75 

76 2. For plotting purposes we might want to plot :math:`|E|*\exp{(i*theta)}`. 

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

78 the returned quantity therefore corresponds to :math:`|E|*\exp{(i*theta)}`. 

79 

80 See Also 

81 -------- 

82 CalcE1 

83 CalcE2 

84 """ 

85 

86 colXx = Field[str]( 

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

88 default="{band}_ixx", 

89 ) 

90 

91 colYy = Field[str]( 

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

93 default="{band}_iyy", 

94 ) 

95 

96 colXy = Field[str]( 

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

98 default="{band}_ixy", 

99 ) 

100 

101 ellipticityType = ChoiceField[str]( 

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

103 allowed={ 

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

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

106 }, 

107 default="distortion", 

108 ) 

109 

110 halvePhaseAngle = Field[bool]( 

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

112 default=False, 

113 ) 

114 

115 component = ChoiceField[str]( 

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

117 optional=True, 

118 allowed={ 

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

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

121 }, 

122 ) 

123 

124 def getInputSchema(self) -> KeyedDataSchema: 

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

126 

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

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

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

130 ) 

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

132 

133 if self.ellipticityType == "shear": 

134 denom += 2 * np.sqrt( 

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

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

137 ) 

138 e = cast(Vector, e) 

139 denom = cast(Vector, denom) 

140 

141 e /= denom 

142 

143 if self.halvePhaseAngle: 

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

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

146 # instead of the more expensive trig calls. 

147 e *= np.abs(e) 

148 e = np.sqrt(e) 

149 

150 if self.component == "1": 

151 return np.real(e) 

152 elif self.component == "2": 

153 return np.imag(e) 

154 else: 

155 return e 

156 

157 

158class CalcEDiff(VectorAction): 

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

160 

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

162 :math:`e_{A} - e_{B} = de = |de|\exp{(j*2*theta)}`. 

163 

164 See Also 

165 -------- 

166 CalcE 

167 

168 Notes 

169 ----- 

170 

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

172 of the sources. 

173 

174 2. For plotting purposes we might want to plot 

175 

176 .. math:: |de|*\exp{(j*theta)}. 

177 

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

179 the returned quantity therefore corresponds to :math:`|e|*\exp{(j*theta)}`. 

180 """ 

181 

182 colA = ConfigurableActionField[VectorAction]( 

183 doc="Ellipticity to subtract from", 

184 default=CalcE, 

185 ) 

186 

187 colB = ConfigurableActionField[VectorAction]( 

188 doc="Ellipticity to subtract", 

189 dtype=VectorAction, 

190 default=CalcE, 

191 ) 

192 

193 halvePhaseAngle = Field[bool]( 

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

195 default=False, 

196 ) 

197 

198 component = ChoiceField[str]( 

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

200 optional=True, 

201 allowed={ 

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

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

204 }, 

205 ) 

206 

207 def getInputSchema(self) -> KeyedDataSchema: 

208 yield from self.colA.getInputSchema() 

209 yield from self.colB.getInputSchema() 

210 

211 def validate(self): 

212 super().validate() 

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

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

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

216 

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

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

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

220 eDiff = eMeas - ePSF 

221 if self.halvePhaseAngle: 

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

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

224 # instead of the more expensive trig calls. 

225 eDiff *= np.abs(eDiff) 

226 eDiff = np.sqrt(eDiff) 

227 

228 if self.component == "1": 

229 return np.real(eDiff) 

230 elif self.component == "2": 

231 return np.imag(eDiff) 

232 else: 

233 return eDiff 

234 

235 

236class CalcE1(VectorAction): 

237 """Calculate distortion-type :math:`e1 = (Ixx - Iyy)/(Ixx + Iyy)` or 

238 shear-type :math:`g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy^{2}))`. 

239 

240 See Also 

241 -------- 

242 CalcE 

243 CalcE2 

244 

245 Note 

246 ---- 

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

248 of the sources. 

249 """ 

250 

251 colXx = Field[str]( 

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

253 default="{band}_ixx", 

254 ) 

255 

256 colYy = Field[str]( 

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

258 default="{band}_iyy", 

259 ) 

260 

261 colXy = Field[str]( 

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

263 default="{band}_ixy", 

264 optional=True, 

265 ) 

266 

267 ellipticityType = ChoiceField[str]( 

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

269 allowed={ 

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

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

272 }, 

273 default="distortion", 

274 ) 

275 

276 def getInputSchema(self) -> KeyedDataSchema: 

277 if self.ellipticityType == "distortion": 

278 return ( 

279 (self.colXx, Vector), 

280 (self.colYy, Vector), 

281 ) 

282 else: 

283 return ( 

284 (self.colXx, Vector), 

285 (self.colYy, Vector), 

286 (self.colXy, Vector), 

287 ) 

288 

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

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

291 if self.ellipticityType == "shear": 

292 denom += 2 * np.sqrt( 

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

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

295 ) 

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

297 

298 return cast(Vector, e1) 

299 

300 def validate(self): 

301 super().validate() 

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

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

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

305 

306 

307class CalcE2(VectorAction): 

308 r"""Calculate distortion-type :math:`e2 = 2Ixy/(Ixx+Iyy)` or 

309 shear-type :math:`g2 = 2Ixy/(Ixx+Iyy+2\sqrt(Ixx*Iyy - Ixy^{2}))`. 

310 

311 See Also 

312 -------- 

313 CalcE 

314 CalcE1 

315 

316 Note 

317 ---- 

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

319 of the sources. 

320 """ 

321 

322 colXx = Field[str]( 

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

324 default="{band}_ixx", 

325 ) 

326 

327 colYy = Field[str]( 

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

329 default="{band}_iyy", 

330 ) 

331 

332 colXy = Field[str]( 

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

334 default="{band}_ixy", 

335 ) 

336 

337 ellipticityType = ChoiceField[str]( 

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

339 allowed={ 

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

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

342 }, 

343 default="distortion", 

344 ) 

345 

346 def getInputSchema(self) -> KeyedDataSchema: 

347 return ( 

348 (self.colXx, Vector), 

349 (self.colYy, Vector), 

350 (self.colXy, Vector), 

351 ) 

352 

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

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

355 if self.ellipticityType == "shear": 

356 denom += 2 * np.sqrt( 

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

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

359 ) 

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

361 return cast(Vector, e2)