Coverage for python/lsst/analysis/tools/actions/vector/ellipticity.py: 34%
87 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-21 10:05 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-21 10:05 +0000
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)
30import numpy as np
31from lsst.pex.config import ChoiceField, Field, FieldValidationError
32from lsst.pex.config.configurableActions import ConfigurableActionField
34from ...interfaces import KeyedData, KeyedDataSchema, Vector, VectorAction
37class CalcE(VectorAction):
38 """Calculate a complex value representation of the ellipticity.
40 The complex ellipticity is typically defined as
41 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is
42 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments.
43 This is sometimes referred to as distortion, and denoted by e = (e1, e2)
44 in GalSim (see Eq. 4.4. of Bartelmann and Schneider, 2001).
45 The other definition differs in normalization.
46 It is referred to as shear, and denoted by g = (g1, g2)
47 in GalSim (see Eq. 4.10 of Bartelmann and Schneider (2001). It is defined
48 as g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
50 The shear measure is unbiased in weak-lensing shear, but may exclude some
51 objects in the presence of noisy moment estimates. The distortion measure
52 is biased in weak-lensing distortion, but does not suffer from selection
53 artifacts.
55 References
56 ----------
57 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”,
58 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001.
59 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508
61 Notes
62 -----
64 1. This is a shape measurement used for doing QA on the ellipticity
65 of the sources.
67 2. For plotting purposes we might want to plot |E|*exp(i*theta).
68 If `halvePhaseAngle` config parameter is set to `True`, then
69 the returned quantity therefore corresponds to |E|*exp(i*theta).
71 See Also
72 --------
73 CalcE1
74 CalcE2
75 """
77 colXx = Field[str](
78 doc="The column name to get the xx shape component from.",
79 default="{band}_ixx",
80 )
82 colYy = Field[str](
83 doc="The column name to get the yy shape component from.",
84 default="{band}_iyy",
85 )
87 colXy = Field[str](
88 doc="The column name to get the xy shape component from.",
89 default="{band}_ixy",
90 )
92 ellipticityType = ChoiceField[str](
93 doc="The type of ellipticity to calculate",
94 allowed={
95 "distortion": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/" "(Ixx + Iyy)"),
96 "shear": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"),
97 },
98 default="distortion",
99 )
101 halvePhaseAngle = Field[bool](
102 doc="Divide the phase angle by 2? Suitable for quiver plots.",
103 default=False,
104 )
106 component = ChoiceField[str](
107 doc="Which component of the ellipticity to return. If `None`, return complex ellipticity values.",
108 optional=True,
109 allowed={
110 "1": "e1 or g1 (depending on `ellipticityType`)",
111 "2": "e2 or g2 (depending on `ellipticityType`)",
112 },
113 )
115 def getInputSchema(self) -> KeyedDataSchema:
116 return ((self.colXx, Vector), (self.colXy, Vector), (self.colYy, Vector))
118 def __call__(self, data: KeyedData, **kwargs) -> Vector:
119 e = (data[self.colXx.format(**kwargs)] - data[self.colYy.format(**kwargs)]) + 1j * (
120 2 * data[self.colXy.format(**kwargs)]
121 )
122 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)]
124 if self.ellipticityType == "shear":
125 denom += 2 * np.sqrt(
126 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)]
127 - data[self.colXy.format(**kwargs)] ** 2
128 )
130 e /= denom
132 if self.halvePhaseAngle:
133 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
134 # |e|*exp(i*theta). So we multiply by |e| and take its square root
135 # instead of the more expensive trig calls.
136 e *= np.abs(e)
137 e = np.sqrt(e)
139 if self.component == "1":
140 return np.real(e)
141 elif self.component == "2":
142 return np.imag(e)
143 else:
144 return e
147class CalcEDiff(VectorAction):
148 """Calculate the difference of two ellipticities as a complex quantity.
150 The complex ellipticity difference between e_A and e_B is defined as
151 e_A - e_B = de = |de|exp(j*2*theta).
153 See Also
154 --------
155 CalcE
157 Notes
158 -----
160 1. This is a shape measurement used for doing QA on the ellipticity
161 of the sources.
163 2. For plotting purposes we might want to plot |de|*exp(j*theta).
164 If `halvePhaseAngle` config parameter is set to `True`, then
165 the returned quantity therefore corresponds to |e|*exp(j*theta).
166 """
168 colA = ConfigurableActionField[VectorAction](
169 doc="Ellipticity to subtract from",
170 default=CalcE,
171 )
173 colB = ConfigurableActionField[VectorAction](
174 doc="Ellipticity to subtract",
175 dtype=VectorAction,
176 default=CalcE,
177 )
179 halvePhaseAngle = Field[bool](
180 doc="Divide the phase angle by 2? Suitable for quiver plots.",
181 default=False,
182 )
184 component = ChoiceField[str](
185 doc="Which component of the ellipticity to return. If `None`, return complex ellipticity values.",
186 optional=True,
187 allowed={
188 "1": "e1 or g1 (depending on the `ellipiticyType`)",
189 "2": "e2 or g2 (depending on the `ellipiticyType`)",
190 },
191 )
193 def getInputSchema(self) -> KeyedDataSchema:
194 yield from self.colA.getInputSchema()
195 yield from self.colB.getInputSchema()
197 def validate(self):
198 super().validate()
199 if self.colA.ellipticityType != self.colB.ellipticityType:
200 msg = "Both the ellipticities in CalcEDiff must have the same type."
201 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg)
203 def __call__(self, data: KeyedData, **kwargs) -> Vector:
204 eMeas = self.colA(data, **kwargs)
205 ePSF = self.colB(data, **kwargs)
206 eDiff = eMeas - ePSF
207 if self.halvePhaseAngle:
208 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
209 # |e|*exp(j*theta). So we multiply by |e| and take its square root
210 # instead of the more expensive trig calls.
211 eDiff *= np.abs(eDiff)
212 eDiff = np.sqrt(eDiff)
214 if self.component == "1":
215 return np.real(eDiff)
216 elif self.component == "2":
217 return np.imag(eDiff)
218 else:
219 return eDiff
222class CalcE1(VectorAction):
223 """Calculate distortion-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or
224 shear-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
226 See Also
227 --------
228 CalcE
229 CalcE2
231 Note
232 ----
233 This is a shape measurement used for doing QA on the ellipticity
234 of the sources.
235 """
237 colXx = Field[str](
238 doc="The column name to get the xx shape component from.",
239 default="{band}_ixx",
240 )
242 colYy = Field[str](
243 doc="The column name to get the yy shape component from.",
244 default="{band}_iyy",
245 )
247 colXy = Field[str](
248 doc="The column name to get the xy shape component from.",
249 default="{band}_ixy",
250 optional=True,
251 )
253 ellipticityType = ChoiceField[str](
254 doc="The type of ellipticity to calculate",
255 allowed={
256 "distortion": "Distortion, measured as (Ixx - Iyy)/(Ixx + Iyy)",
257 "shear": ("Shear, measured as (Ixx - Iyy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"),
258 },
259 default="distortion",
260 )
262 def getInputSchema(self) -> KeyedDataSchema:
263 if self.ellipticityType == "distortion":
264 return (
265 (self.colXx, Vector),
266 (self.colYy, Vector),
267 )
268 else:
269 return (
270 (self.colXx, Vector),
271 (self.colYy, Vector),
272 (self.colXy, Vector),
273 )
275 def __call__(self, data: KeyedData, **kwargs) -> Vector:
276 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)]
277 if self.ellipticityType == "shear":
278 denom += 2 * np.sqrt(
279 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)]
280 - data[self.colXy.format(**kwargs)] ** 2
281 )
282 e1 = (data[self.colXx.format(**kwargs)] - data[self.colYy.format(**kwargs)]) / denom
284 return e1
286 def validate(self):
287 super().validate()
288 if self.ellipticityType == "shear" and self.colXy is None:
289 msg = "colXy is required for shear-type shear ellipticity"
290 raise FieldValidationError(self.__class__.colXy, self, msg)
293class CalcE2(VectorAction):
294 """Calculate distortion-type e2 = 2Ixy/(Ixx+Iyy) or
295 shear-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
297 See Also
298 --------
299 CalcE
300 CalcE1
302 Note
303 ----
304 This is a shape measurement used for doing QA on the ellipticity
305 of the sources.
306 """
308 colXx = Field[str](
309 doc="The column name to get the xx shape component from.",
310 default="{band}_ixx",
311 )
313 colYy = Field[str](
314 doc="The column name to get the yy shape component from.",
315 default="{band}_iyy",
316 )
318 colXy = Field[str](
319 doc="The column name to get the xy shape component from.",
320 default="{band}_ixy",
321 )
323 ellipticityType = ChoiceField[str](
324 doc="The type of ellipticity to calculate",
325 allowed={
326 "distortion": "Distortion, defined as 2*Ixy/(Ixx + Iyy)",
327 "shear": ("Shear, defined as 2*Ixy/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"),
328 },
329 default="distortion",
330 )
332 def getInputSchema(self) -> KeyedDataSchema:
333 return (
334 (self.colXx, Vector),
335 (self.colYy, Vector),
336 (self.colXy, Vector),
337 )
339 def __call__(self, data: KeyedData, **kwargs) -> Vector:
340 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)]
341 if self.ellipticityType == "shear":
342 denom += 2 * np.sqrt(
343 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)]
344 - data[self.colXy.format(**kwargs)] ** 2
345 )
346 e2 = 2 * data[self.colXy.format(**kwargs)] / denom
347 return e2