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
« 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
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
43 .. math::
45 e = |e|\exp{(j*2*theta)}
46 e = \\frac{((Ixx - Iyy) + j*(2*Ixy))}{(Ixx + Iyy)},
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::
57 g = \\frac{((Ixx-Iyy)+j*(2*Ixy))}{(Ixx+Iyy+2\sqrt{(Ixx*Iyy-Ixy^{2})})}.
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.
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
70 Notes
71 -----
73 1. This is a shape measurement used for doing QA on the ellipticity
74 of the sources.
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)}`.
80 See Also
81 --------
82 CalcE1
83 CalcE2
84 """
86 colXx = Field[str](
87 doc="The column name to get the xx shape component from.",
88 default="{band}_ixx",
89 )
91 colYy = Field[str](
92 doc="The column name to get the yy shape component from.",
93 default="{band}_iyy",
94 )
96 colXy = Field[str](
97 doc="The column name to get the xy shape component from.",
98 default="{band}_ixy",
99 )
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 )
110 halvePhaseAngle = Field[bool](
111 doc="Divide the phase angle by 2? Suitable for quiver plots.",
112 default=False,
113 )
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 )
124 def getInputSchema(self) -> KeyedDataSchema:
125 return ((self.colXx, Vector), (self.colXy, Vector), (self.colYy, Vector))
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)]
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)
141 e /= denom
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)
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
158class CalcEDiff(VectorAction):
159 r"""Calculate the difference of two ellipticities as a complex quantity.
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)}`.
164 See Also
165 --------
166 CalcE
168 Notes
169 -----
171 1. This is a shape measurement used for doing QA on the ellipticity
172 of the sources.
174 2. For plotting purposes we might want to plot
176 .. math:: |de|*\exp{(j*theta)}.
178 If `halvePhaseAngle` config parameter is set to `True`, then
179 the returned quantity therefore corresponds to :math:`|e|*\exp{(j*theta)}`.
180 """
182 colA = ConfigurableActionField[VectorAction](
183 doc="Ellipticity to subtract from",
184 default=CalcE,
185 )
187 colB = ConfigurableActionField[VectorAction](
188 doc="Ellipticity to subtract",
189 dtype=VectorAction,
190 default=CalcE,
191 )
193 halvePhaseAngle = Field[bool](
194 doc="Divide the phase angle by 2? Suitable for quiver plots.",
195 default=False,
196 )
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 )
207 def getInputSchema(self) -> KeyedDataSchema:
208 yield from self.colA.getInputSchema()
209 yield from self.colB.getInputSchema()
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)
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)
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
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}))`.
240 See Also
241 --------
242 CalcE
243 CalcE2
245 Note
246 ----
247 This is a shape measurement used for doing QA on the ellipticity
248 of the sources.
249 """
251 colXx = Field[str](
252 doc="The column name to get the xx shape component from.",
253 default="{band}_ixx",
254 )
256 colYy = Field[str](
257 doc="The column name to get the yy shape component from.",
258 default="{band}_iyy",
259 )
261 colXy = Field[str](
262 doc="The column name to get the xy shape component from.",
263 default="{band}_ixy",
264 optional=True,
265 )
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 )
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 )
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
298 return cast(Vector, e1)
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)
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}))`.
311 See Also
312 --------
313 CalcE
314 CalcE1
316 Note
317 ----
318 This is a shape measurement used for doing QA on the ellipticity
319 of the sources.
320 """
322 colXx = Field[str](
323 doc="The column name to get the xx shape component from.",
324 default="{band}_ixx",
325 )
327 colYy = Field[str](
328 doc="The column name to get the yy shape component from.",
329 default="{band}_iyy",
330 )
332 colXy = Field[str](
333 doc="The column name to get the xy shape component from.",
334 default="{band}_ixy",
335 )
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 )
346 def getInputSchema(self) -> KeyedDataSchema:
347 return (
348 (self.colXx, Vector),
349 (self.colYy, Vector),
350 (self.colXy, Vector),
351 )
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)