Coverage for python/lsst/analysis/tools/actions/vector/ellipticity.py: 34%
87 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-25 12:23 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-25 12:23 +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.pipe.tasks.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(
169 doc="Ellipticity to subtract from",
170 dtype=VectorAction,
171 default=CalcE,
172 )
174 colB = ConfigurableActionField(
175 doc="Ellipticity to subtract",
176 dtype=VectorAction,
177 default=CalcE,
178 )
180 halvePhaseAngle = Field[bool](
181 doc="Divide the phase angle by 2? Suitable for quiver plots.",
182 default=False,
183 )
185 component = ChoiceField[str](
186 doc="Which component of the ellipticity to return. If `None`, return complex ellipticity values.",
187 optional=True,
188 allowed={
189 "1": "e1 or g1 (depending on the `ellipiticyType`)",
190 "2": "e2 or g2 (depending on the `ellipiticyType`)",
191 },
192 )
194 def getInputSchema(self) -> KeyedDataSchema:
195 yield from self.colA.getInputSchema()
196 yield from self.colB.getInputSchema()
198 def validate(self):
199 super().validate()
200 if self.colA.ellipticityType != self.colB.ellipticityType:
201 msg = "Both the ellipticities in CalcEDiff must have the same type."
202 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg)
204 def __call__(self, data: KeyedData, **kwargs) -> Vector:
205 eMeas = self.colA(data, **kwargs)
206 ePSF = self.colB(data, **kwargs)
207 eDiff = eMeas - ePSF
208 if self.halvePhaseAngle:
209 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
210 # |e|*exp(j*theta). So we multiply by |e| and take its square root
211 # instead of the more expensive trig calls.
212 eDiff *= np.abs(eDiff)
213 eDiff = np.sqrt(eDiff)
215 if self.component == "1":
216 return np.real(eDiff)
217 elif self.component == "2":
218 return np.imag(eDiff)
219 else:
220 return eDiff
223class CalcE1(VectorAction):
224 """Calculate distortion-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or
225 shear-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
227 See Also
228 --------
229 CalcE
230 CalcE2
232 Note
233 ----
234 This is a shape measurement used for doing QA on the ellipticity
235 of the sources.
236 """
238 colXx = Field[str](
239 doc="The column name to get the xx shape component from.",
240 default="{band}_ixx",
241 )
243 colYy = Field[str](
244 doc="The column name to get the yy shape component from.",
245 default="{band}_iyy",
246 )
248 colXy = Field[str](
249 doc="The column name to get the xy shape component from.",
250 default="{band}_ixy",
251 optional=True,
252 )
254 ellipticityType = ChoiceField[str](
255 doc="The type of ellipticity to calculate",
256 allowed={
257 "distortion": "Distortion, measured as (Ixx - Iyy)/(Ixx + Iyy)",
258 "shear": ("Shear, measured as (Ixx - Iyy)/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"),
259 },
260 default="distortion",
261 )
263 def getInputSchema(self) -> KeyedDataSchema:
264 if self.ellipticityType == "distortion":
265 return (
266 (self.colXx, Vector),
267 (self.colYy, Vector),
268 )
269 else:
270 return (
271 (self.colXx, Vector),
272 (self.colYy, Vector),
273 (self.colXy, Vector),
274 )
276 def __call__(self, data: KeyedData, **kwargs) -> Vector:
277 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)]
278 if self.ellipticityType == "shear":
279 denom += 2 * np.sqrt(
280 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)]
281 - data[self.colXy.format(**kwargs)] ** 2
282 )
283 e1 = (data[self.colXx.format(**kwargs)] - data[self.colYy.format(**kwargs)]) / denom
285 return e1
287 def validate(self):
288 super().validate()
289 if self.ellipticityType == "shear" and self.colXy is None:
290 msg = "colXy is required for shear-type shear ellipticity"
291 raise FieldValidationError(self.__class__.colXy, self, msg)
294class CalcE2(VectorAction):
295 """Calculate distortion-type e2 = 2Ixy/(Ixx+Iyy) or
296 shear-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
298 See Also
299 --------
300 CalcE
301 CalcE1
303 Note
304 ----
305 This is a shape measurement used for doing QA on the ellipticity
306 of the sources.
307 """
309 colXx = Field[str](
310 doc="The column name to get the xx shape component from.",
311 default="{band}_ixx",
312 )
314 colYy = Field[str](
315 doc="The column name to get the yy shape component from.",
316 default="{band}_iyy",
317 )
319 colXy = Field[str](
320 doc="The column name to get the xy shape component from.",
321 default="{band}_ixy",
322 )
324 ellipticityType = ChoiceField[str](
325 doc="The type of ellipticity to calculate",
326 allowed={
327 "distortion": "Distortion, defined as 2*Ixy/(Ixx + Iyy)",
328 "shear": ("Shear, defined as 2*Ixy/" "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"),
329 },
330 default="distortion",
331 )
333 def getInputSchema(self) -> KeyedDataSchema:
334 return (
335 (self.colXx, Vector),
336 (self.colYy, Vector),
337 (self.colXy, Vector),
338 )
340 def __call__(self, data: KeyedData, **kwargs) -> Vector:
341 denom = data[self.colXx.format(**kwargs)] + data[self.colYy.format(**kwargs)]
342 if self.ellipticityType == "shear":
343 denom += 2 * np.sqrt(
344 data[self.colXx.format(**kwargs)] * data[self.colYy.format(**kwargs)]
345 - data[self.colXy.format(**kwargs)] ** 2
346 )
347 e2 = 2 * data[self.colXy.format(**kwargs)] / denom
348 return e2