Coverage for python/lsst/analysis/tools/actions/vector/ellipticity.py: 34%
90 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 04:48 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-02 04:48 -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
23__all__ = (
24 "CalcE",
25 "CalcEDiff",
26 "CalcE1",
27 "CalcE2",
28)
30from typing import cast
32import numpy as np
33from lsst.pex.config import ChoiceField, Field, FieldValidationError
34from lsst.pex.config.configurableActions import ConfigurableActionField
36from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction
39class CalcE(VectorAction):
40 r"""Calculate a complex value representation of the ellipticity.
42 The complex ellipticity is typically defined as:
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}},
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
57 .. math::
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})}}.
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.
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
74 Notes
75 -----
77 1. This is a shape measurement used for doing QA on the ellipticity
78 of the sources.
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`).
88 See Also
89 --------
90 CalcE1
91 CalcE2
92 """
94 colXx = Field[str](
95 doc="The column name to get the xx shape component from.",
96 default="{band}_ixx",
97 )
99 colYy = Field[str](
100 doc="The column name to get the yy shape component from.",
101 default="{band}_iyy",
102 )
104 colXy = Field[str](
105 doc="The column name to get the xy shape component from.",
106 default="{band}_ixy",
107 )
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 )
124 halvePhaseAngle = Field[bool](
125 doc="Divide the phase angle by 2? Suitable for quiver plots.",
126 default=False,
127 )
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 )
139 def getInputSchema(self) -> KeyedDataSchema:
140 return ((self.colXx, Vector), (self.colXy, Vector), (self.colYy, Vector))
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)]
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)
156 e /= denom
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)
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
173class CalcEDiff(VectorAction):
174 r"""Calculate the difference of two ellipticities as a complex quantity.
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})}`
181 See Also
182 --------
183 CalcE
185 Notes
186 -----
188 1. This is a shape measurement used for doing QA on the ellipticity
189 of the sources.
191 2. The `ellipticityType` of `colA` and `colB` have to be the same.
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 """
201 colA = ConfigurableActionField[VectorAction](
202 doc="Ellipticity to subtract from",
203 default=CalcE,
204 )
206 colB = ConfigurableActionField[VectorAction](
207 doc="Ellipticity to subtract",
208 dtype=VectorAction,
209 default=CalcE,
210 )
212 halvePhaseAngle = Field[bool](
213 doc="Divide the phase angle by 2? Suitable for quiver plots.",
214 default=False,
215 )
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 )
227 def getInputSchema(self) -> KeyedDataSchema:
228 yield from self.colA.getInputSchema()
229 yield from self.colB.getInputSchema()
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)
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)
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
256class CalcE1(VectorAction):
257 r"""Calculate :math:`e_1` (distortion-type) or :math:`g_1` (shear-type).
259 The definitions are as follows:
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}}).
265 See Also
266 --------
267 CalcE
268 CalcE2
270 Notes
271 -----
272 This is a shape measurement used for doing QA on the ellipticity
273 of the sources.
274 """
276 colXx = Field[str](
277 doc="The column name to get the xx shape component from.",
278 default="{band}_ixx",
279 )
281 colYy = Field[str](
282 doc="The column name to get the yy shape component from.",
283 default="{band}_iyy",
284 )
286 colXy = Field[str](
287 doc="The column name to get the xy shape component from.",
288 default="{band}_ixy",
289 optional=True,
290 )
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 )
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 )
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
326 return cast(Vector, e1)
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)
335class CalcE2(VectorAction):
336 r"""Calculate :math:`e_2` (distortion-type) or :math:`g_2` (shear-type).
338 The definitions are as follows:
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})}).
344 See Also
345 --------
346 CalcE
347 CalcE1
349 Notes
350 -----
351 This is a shape measurement used for doing QA on the ellipticity
352 of the sources.
353 """
355 colXx = Field[str](
356 doc="The column name to get the xx shape component from.",
357 default="{band}_ixx",
358 )
360 colYy = Field[str](
361 doc="The column name to get the yy shape component from.",
362 default="{band}_iyy",
363 )
365 colXy = Field[str](
366 doc="The column name to get the xy shape component from.",
367 default="{band}_ixy",
368 )
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 )
380 def getInputSchema(self) -> KeyedDataSchema:
381 return (
382 (self.colXx, Vector),
383 (self.colYy, Vector),
384 (self.colXy, Vector),
385 )
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)