Coverage for python/lsst/analysis/tools/actions/vector/ellipticity.py: 34%
90 statements
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-30 03:04 -0700
« prev ^ index » next coverage.py v7.2.4, created at 2023-04-30 03:04 -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 """Calculate a complex value representation of the ellipticity.
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)).
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.
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
63 Notes
64 -----
66 1. This is a shape measurement used for doing QA on the ellipticity
67 of the sources.
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).
73 See Also
74 --------
75 CalcE1
76 CalcE2
77 """
79 colXx = Field[str](
80 doc="The column name to get the xx shape component from.",
81 default="{band}_ixx",
82 )
84 colYy = Field[str](
85 doc="The column name to get the yy shape component from.",
86 default="{band}_iyy",
87 )
89 colXy = Field[str](
90 doc="The column name to get the xy shape component from.",
91 default="{band}_ixy",
92 )
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 )
103 halvePhaseAngle = Field[bool](
104 doc="Divide the phase angle by 2? Suitable for quiver plots.",
105 default=False,
106 )
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 )
117 def getInputSchema(self) -> KeyedDataSchema:
118 return ((self.colXx, Vector), (self.colXy, Vector), (self.colYy, Vector))
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)]
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)
134 e /= denom
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)
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
151class CalcEDiff(VectorAction):
152 """Calculate the difference of two ellipticities as a complex quantity.
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).
157 See Also
158 --------
159 CalcE
161 Notes
162 -----
164 1. This is a shape measurement used for doing QA on the ellipticity
165 of the sources.
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 """
172 colA = ConfigurableActionField[VectorAction](
173 doc="Ellipticity to subtract from",
174 default=CalcE,
175 )
177 colB = ConfigurableActionField[VectorAction](
178 doc="Ellipticity to subtract",
179 dtype=VectorAction,
180 default=CalcE,
181 )
183 halvePhaseAngle = Field[bool](
184 doc="Divide the phase angle by 2? Suitable for quiver plots.",
185 default=False,
186 )
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 )
197 def getInputSchema(self) -> KeyedDataSchema:
198 yield from self.colA.getInputSchema()
199 yield from self.colB.getInputSchema()
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)
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)
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
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)).
230 See Also
231 --------
232 CalcE
233 CalcE2
235 Note
236 ----
237 This is a shape measurement used for doing QA on the ellipticity
238 of the sources.
239 """
241 colXx = Field[str](
242 doc="The column name to get the xx shape component from.",
243 default="{band}_ixx",
244 )
246 colYy = Field[str](
247 doc="The column name to get the yy shape component from.",
248 default="{band}_iyy",
249 )
251 colXy = Field[str](
252 doc="The column name to get the xy shape component from.",
253 default="{band}_ixy",
254 optional=True,
255 )
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 )
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 )
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
288 return cast(Vector, e1)
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)
297class CalcE2(VectorAction):
298 """Calculate distortion-type e2 = 2Ixy/(Ixx+Iyy) or
299 shear-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
301 See Also
302 --------
303 CalcE
304 CalcE1
306 Note
307 ----
308 This is a shape measurement used for doing QA on the ellipticity
309 of the sources.
310 """
312 colXx = Field[str](
313 doc="The column name to get the xx shape component from.",
314 default="{band}_ixx",
315 )
317 colYy = Field[str](
318 doc="The column name to get the yy shape component from.",
319 default="{band}_iyy",
320 )
322 colXy = Field[str](
323 doc="The column name to get the xy shape component from.",
324 default="{band}_ixy",
325 )
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 )
336 def getInputSchema(self) -> KeyedDataSchema:
337 return (
338 (self.colXx, Vector),
339 (self.colYy, Vector),
340 (self.colXy, Vector),
341 )
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)