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-18 03:21 -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 

44 .. math:: 

45 e &= |e|\exp{(\mathrm{i}2\theta)} = e_1+\mathrm{i}e_2, \\ 

46 &= \frac{(I_{xx} - I_{yy}) + \mathrm{i}2I_{xy}}{I_{xx} + I_{yy}}, 

47 

48 where :math:`\mathrm{i}` is the square root of -1 and :math:`I_{xx}`, 

49 :math:`I_{yy}`, and :math:`I_{xy}` are second-order central moments. 

50 This is sometimes referred to as distortion, and denoted in GalSim by 

51 :math:`e=(e_1,e_2)` (see Eq. 4.4. of Bartelmann and Schneider, 2001 [1]_). 

52 The other definition differs in normalization. 

53 It is referred to as shear, and denoted by :math:`g=(g_{1},g_{2})` 

54 in GalSim (see Eq. 4.10 of Bartelmann and Schneider, 2001 [1]_). 

55 It is defined as 

56 

57 .. math:: 

58 

59 g = \frac{(I_{xx} - I_{yy}) + \mathrm{i}2I_{xy}} 

60 {I_{xx} + I_{yy} + 2\sqrt{(I_{xx}I_{yy}-I_{xy}^{2})}}. 

61 

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

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

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

65 artifacts. 

66 

67 References 

68 ---------- 

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

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

71 doi:10.1016/S0370-1573(00)00082-X; 

72 https://arxiv.org/abs/astro-ph/9912508 

73 

74 Notes 

75 ----- 

76 

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

78 of the sources. 

79 

80 2. For plotting purposes we might want to plot quivers whose lengths 

81 are proportional to :math:`|e|` and whose angles correspond to 

82 :math:`\theta`. 

83 If `halvePhaseAngle` config parameter is set to `True`, then the returned 

84 quantity therefore corresponds to the complex quantity 

85 :math:`|e|\exp{(\mathrm{i}\theta)}` or its real and imaginary parts 

86 (depending on the `component`). 

87 

88 See Also 

89 -------- 

90 CalcE1 

91 CalcE2 

92 """ 

93 

94 colXx = Field[str]( 

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

96 default="{band}_ixx", 

97 ) 

98 

99 colYy = Field[str]( 

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

101 default="{band}_iyy", 

102 ) 

103 

104 colXy = Field[str]( 

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

106 default="{band}_ixy", 

107 ) 

108 

109 ellipticityType = ChoiceField[str]( 

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

111 allowed={ 

112 "distortion": ( 

113 "Distortion, defined as " r":math:`(I_{xx}-I_{yy}+\mathrm{i}2I_{xy})/(I_{xx}+I_{yy})`" 

114 ), 

115 "shear": ( 

116 "Shear, defined as " 

117 r":math:`(I_{xx}-I_{yy}+\mathrm{i}2I_{xy})/(I_{xx}+I_{yy}+2\sqrt{I_{xx}I_{yy}-I_{xy}^2})`" 

118 ), 

119 }, 

120 default="distortion", 

121 optional=False, 

122 ) 

123 

124 halvePhaseAngle = Field[bool]( 

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

126 default=False, 

127 ) 

128 

129 component = ChoiceField[str]( 

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

131 allowed={ 

132 "1": r":math:`e_1` or :math:`g_1` (depending on `ellipticityType`)", 

133 "2": r":math:`e_2` or :math:`g_2` (depending on `ellipticityType`)", 

134 None: r":math:`e_1 + \mathrm{i}e_2` or :math:`g_1 + \mathrm{i}g_2`" 

135 " (depending on `ellipticityType`)", 

136 }, 

137 ) 

138 

139 def getInputSchema(self) -> KeyedDataSchema: 

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

141 

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

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

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

145 ) 

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

147 

148 if self.ellipticityType == "shear": 

149 denom += 2 * np.sqrt( 

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

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

152 ) 

153 e = cast(Vector, e) 

154 denom = cast(Vector, denom) 

155 

156 e /= denom 

157 

158 if self.halvePhaseAngle: 

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

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

161 # instead of the more expensive trig calls. 

162 e *= np.abs(e) 

163 e = np.sqrt(e) 

164 

165 if self.component == "1": 

166 return np.real(e) 

167 elif self.component == "2": 

168 return np.imag(e) 

169 else: 

170 return e 

171 

172 

173class CalcEDiff(VectorAction): 

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

175 

176 The complex ellipticity (for both distortion-type and shear-type) 

177 difference ( between :math:`e_A` and :math:`e_B` is defined as 

178 :math:`e_{A}-e_{B}=\delta e=|\delta e|\exp{(\mathrm{i}2\theta_{\delta})}` 

179 

180 

181 See Also 

182 -------- 

183 CalcE 

184 

185 Notes 

186 ----- 

187 

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

189 of the sources. 

190 

191 2. The `ellipticityType` of `colA` and `colB` have to be the same. 

192 

193 3. For plotting purposes we might want to plot quivers whose lengths 

194 are proportional to :math:`|\delta e|` and whose angles correspond to 

195 :math:`\theta_\delta`. 

196 If `halvePhaseAngle` config parameter is set to `True`, then the returned 

197 quantity therefore corresponds to the complex quantity 

198 :math:`|\delta e|\exp{(\mathrm{i}\theta_\delta)}`. 

199 """ 

200 

201 colA = ConfigurableActionField[VectorAction]( 

202 doc="Ellipticity to subtract from", 

203 default=CalcE, 

204 ) 

205 

206 colB = ConfigurableActionField[VectorAction]( 

207 doc="Ellipticity to subtract", 

208 dtype=VectorAction, 

209 default=CalcE, 

210 ) 

211 

212 halvePhaseAngle = Field[bool]( 

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

214 default=False, 

215 ) 

216 

217 component = ChoiceField[str]( 

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

219 allowed={ 

220 "1": r":math:`\delta e_1` or :math:`\delta g_1` (depending on the common `ellipiticityType`)", 

221 "2": r":math:`\delta e_2` or :math:`\delta g_2` (depending on the common `ellipiticityType`)", 

222 None: r":math:`\delta e_1+\mathrm{i}\delta e_2` or :math:`\delta g_1 \mathrm{i}\delta g_2`" 

223 " (depending on the common `ellipticityType`)", 

224 }, 

225 ) 

226 

227 def getInputSchema(self) -> KeyedDataSchema: 

228 yield from self.colA.getInputSchema() 

229 yield from self.colB.getInputSchema() 

230 

231 def validate(self): 

232 super().validate() 

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

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

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

236 

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

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

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

240 eDiff = eMeas - ePSF 

241 if self.halvePhaseAngle: 

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

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

244 # instead of the more expensive trig calls. 

245 eDiff *= np.abs(eDiff) 

246 eDiff = np.sqrt(eDiff) 

247 

248 if self.component == "1": 

249 return np.real(eDiff) 

250 elif self.component == "2": 

251 return np.imag(eDiff) 

252 else: 

253 return eDiff 

254 

255 

256class CalcE1(VectorAction): 

257 r"""Calculate :math:`e_1` (distortion-type) or :math:`g_1` (shear-type). 

258 

259 The definitions are as follows: 

260 

261 .. math:: 

262 e_1&=(I_{xx}-I_{yy})/(I_{xx}+I_{yy}) \\ 

263 g_1&=(I_{xx}-I_{yy})/(I_{xx}+I_{yy}+2\sqrt{I_{xx}I_{yy}-I_{xy}^{2}}). 

264 

265 See Also 

266 -------- 

267 CalcE 

268 CalcE2 

269 

270 Notes 

271 ----- 

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

273 of the sources. 

274 """ 

275 

276 colXx = Field[str]( 

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

278 default="{band}_ixx", 

279 ) 

280 

281 colYy = Field[str]( 

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

283 default="{band}_iyy", 

284 ) 

285 

286 colXy = Field[str]( 

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

288 default="{band}_ixy", 

289 optional=True, 

290 ) 

291 

292 ellipticityType = ChoiceField[str]( 

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

294 optional=False, 

295 allowed={ 

296 "distortion": ("Distortion, measured as " r":math:`(I_{xx}-I_{yy})/(I_{xx}+I_{yy})`"), 

297 "shear": ( 

298 "Shear, measured as " r":math:`(I_{xx}-I_{yy})/(I_{xx}+I_{yy}+2\sqrt{I_{xx}I_{yy}-I_{xy}^2})`" 

299 ), 

300 }, 

301 default="distortion", 

302 ) 

303 

304 def getInputSchema(self) -> KeyedDataSchema: 

305 if self.ellipticityType == "distortion": 

306 return ( 

307 (self.colXx, Vector), 

308 (self.colYy, Vector), 

309 ) 

310 else: 

311 return ( 

312 (self.colXx, Vector), 

313 (self.colYy, Vector), 

314 (self.colXy, Vector), 

315 ) 

316 

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

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

319 if self.ellipticityType == "shear": 

320 denom += 2 * np.sqrt( 

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

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

323 ) 

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

325 

326 return cast(Vector, e1) 

327 

328 def validate(self): 

329 super().validate() 

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

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

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

333 

334 

335class CalcE2(VectorAction): 

336 r"""Calculate :math:`e_2` (distortion-type) or :math:`g_2` (shear-type). 

337 

338 The definitions are as follows: 

339 

340 .. math:: 

341 e_2 &= 2I_{xy}/(I_{xx}+I_{yy}) \\ 

342 g_2 &= 2I_{xy}/(I_{xx}+I_{yy}+2\sqrt{(I_{xx}I_{yy}-I_{xy}^{2})}). 

343 

344 See Also 

345 -------- 

346 CalcE 

347 CalcE1 

348 

349 Notes 

350 ----- 

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

352 of the sources. 

353 """ 

354 

355 colXx = Field[str]( 

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

357 default="{band}_ixx", 

358 ) 

359 

360 colYy = Field[str]( 

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

362 default="{band}_iyy", 

363 ) 

364 

365 colXy = Field[str]( 

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

367 default="{band}_ixy", 

368 ) 

369 

370 ellipticityType = ChoiceField[str]( 

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

372 optional=False, 

373 allowed={ 

374 "distortion": ("Distortion, defined as " r":math:`2I_{xy}/(I_{xx}+I_{yy})`"), 

375 "shear": r"Shear, defined as :math:`2I_{xy}/(I_{xx}+I_{yy}+2\sqrt{I_{xx}I_{yy}-I_{xy}^2})`", 

376 }, 

377 default="distortion", 

378 ) 

379 

380 def getInputSchema(self) -> KeyedDataSchema: 

381 return ( 

382 (self.colXx, Vector), 

383 (self.colYy, Vector), 

384 (self.colXy, Vector), 

385 ) 

386 

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

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

389 if self.ellipticityType == "shear": 

390 denom += 2 * np.sqrt( 

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

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

393 ) 

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

395 return cast(Vector, e2)