Coverage for python/lsst/analysis/drp/calcFunctors.py: 37%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1__all__ = ["SNCalculator", "KronFluxDivPsfFlux", "MagDiff", "ColorDiff", "ColorDiffPull",
2 "ExtinctionCorrectedMagDiff", "CalcE", "CalcEDiff", "CalcE1", "CalcE2", "CalcShapeSize"]
4from lsst.pipe.tasks.configurableActions import ConfigurableActionField
5from lsst.pipe.tasks.dataFrameActions import DataFrameAction, DivideColumns, MultiColumnAction
6from lsst.pex.config import ChoiceField, DictField, Field
7from astropy import units as u
8import numpy as np
9import logging
11_LOG = logging.getLogger(__name__)
14class SNCalculator(DivideColumns):
15 """Calculate the signal to noise by default the i band PSF flux is used"""
17 def setDefaults(self):
18 super().setDefaults()
19 self.colA.column = "i_psfFlux"
20 self.colB.column = "i_psfFluxErr"
23class KronFluxDivPsfFlux(DivideColumns):
24 """Divide the Kron instFlux by the PSF instFlux"""
26 def setDefaults(self):
27 super().setDefaults()
28 self.colA.column = "i_kronFlux"
29 self.colB.column = "i_psfFlux"
32class MagDiff(MultiColumnAction):
33 """Calculate the difference between two magnitudes;
34 each magnitude is derived from a flux column.
36 Parameters
37 ----------
38 df : `pandas.core.frame.DataFrame`
39 The catalog to calculate the magnitude difference from.
41 Returns
42 -------
43 The magnitude difference in milli mags.
45 Notes
46 -----
47 The flux columns need to be in units (specifiable in
48 the fluxUnits1 and 2 config options) that can be converted
49 to janskies. This action doesn't have any calibration
50 information and assumes that the fluxes are already
51 calibrated.
52 """
54 col1 = Field(doc="Column to subtract from", dtype=str)
55 fluxUnits1 = Field(doc="Units for col1", dtype=str, default="nanojansky")
56 col2 = Field(doc="Column to subtract", dtype=str)
57 fluxUnits2 = Field(doc="Units for col2", dtype=str, default="nanojansky")
58 returnMillimags = Field(doc="Use millimags or not?", dtype=bool, default=True)
60 @property
61 def columns(self):
62 return (self.col1, self.col2)
64 def __call__(self, df):
65 flux1 = df[self.col1].values * u.Unit(self.fluxUnits1)
66 mag1 = flux1.to(u.ABmag)
68 flux2 = df[self.col2].values * u.Unit(self.fluxUnits2)
69 mag2 = flux2.to(u.ABmag)
71 magDiff = mag1 - mag2
73 if self.returnMillimags:
74 magDiff = magDiff.to(u.mmag)
76 return magDiff
79class ExtinctionCorrectedMagDiff(DataFrameAction):
80 """Compute the difference between two magnitudes and correct for extinction
82 By default bands are derived from the <band>_ prefix on flux columns,
83 per the naming convention in the Object Table:
84 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
85 convention, bands can alternatively be supplied via the band1 or band2
86 config parameters.
87 If band1 and band2 are supplied, the flux column names are ignored.
88 """
90 magDiff = ConfigurableActionField(doc="Action that returns a difference in magnitudes",
91 default=MagDiff, dtype=DataFrameAction)
92 ebvCol = Field(doc="E(B-V) Column Name", dtype=str, default="ebv")
93 band1 = Field(doc="Optional band for magDiff.col1. Supercedes column name prefix",
94 dtype=str, optional=True, default=None)
95 band2 = Field(doc="Optional band for magDiff.col2. Supercedes column name prefix",
96 dtype=str, optional=True, default=None)
97 extinctionCoeffs = DictField(
98 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
99 "Key must be the band",
100 keytype=str, itemtype=float, optional=True,
101 default=None)
103 @property
104 def columns(self):
105 return self.magDiff.columns + (self.ebvCol,)
107 def __call__(self, df):
108 diff = self.magDiff(df)
109 if not self.extinctionCoeffs:
110 _LOG.warning("No extinction Coefficients. Not applying extinction correction")
111 return diff
113 col1Band = self.band1 if self.band1 else self.magDiff.col1.split('_')[0]
114 col2Band = self.band2 if self.band2 else self.magDiff.col2.split('_')[0]
116 for band in (col1Band, col1Band):
117 if band not in self.extinctionCoeffs:
118 _LOG.warning("%s band not found in coefficients dictionary: %s"
119 " Not applying extinction correction", band, self.extinctionCoeffs)
120 return diff
122 av1 = self.extinctionCoeffs[col1Band]
123 av2 = self.extinctionCoeffs[col2Band]
125 ebv = df[self.ebvCol].values
126 correction = (av1 - av2) * ebv * u.mag
128 if self.magDiff.returnMillimags:
129 correction = correction.to(u.mmag)
131 return diff - correction
134class CalcE(MultiColumnAction):
135 """Calculate a complex value representation of the ellipticity.
137 The complex ellipticity is typically defined as
138 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is
139 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments.
140 This is sometimes referred to as distortion, and denoted by e = (e1, e2)
141 in GalSim and referred to as chi-type ellipticity following the notation
142 in Eq. 4.4 of Bartelmann and Schneider (2001). The other definition differs
143 in normalization. It is referred to as shear, and denoted by g = (g1, g2)
144 in GalSim and referred to as epsilon-type ellipticity again following the
145 notation in Eq. 4.10 of Bartelmann and Schneider (2001). It is defined as
146 g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
148 The shear measure is unbiased in weak-lensing shear, but may exclude some
149 objects in the presence of noisy moment estimates. The distortion measure
150 is biased in weak-lensing distortion, but does not suffer from selection
151 artifacts.
153 Reference
154 ---------
155 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”,
156 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001.
157 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508
159 Notes
160 -----
162 1. This is a shape measurement used for doing QA on the ellipticity
163 of the sources.
165 2. For plotting purposes we might want to plot |E|*exp(i*theta).
166 If `halvePhaseAngle` config parameter is set to `True`, then
167 the returned quantity therefore corresponds to |E|*exp(i*theta).
169 See Also
170 --------
171 CalcE1
172 CalcE2
173 """
175 colXx = Field(
176 doc="The column name to get the xx shape component from.",
177 dtype=str,
178 default="ixx",
179 )
181 colYy = Field(
182 doc="The column name to get the yy shape component from.",
183 dtype=str,
184 default="iyy",
185 )
187 colXy = Field(
188 doc="The column name to get the xy shape component from.",
189 dtype=str,
190 default="ixy",
191 )
193 ellipticityType = ChoiceField(
194 doc="The type of ellipticity to calculate",
195 dtype=str,
196 allowed={"chi": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/"
197 "(Ixx + Iyy)"
198 ),
199 "epsilon": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/"
200 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
201 ),
202 },
203 default="chi",
204 )
206 halvePhaseAngle = Field(
207 doc="Divide the phase angle by 2? Suitable for quiver plots.",
208 dtype=bool,
209 default=False,
210 )
212 @property
213 def columns(self):
214 return (self.colXx, self.colYy, self.colXy)
216 def __call__(self, df):
217 e = (df[self.colXx] - df[self.colYy]) + 1j*(2*df[self.colXy])
218 denom = (df[self.colXx] + df[self.colYy])
220 if self.ellipticityType == "epsilon":
221 denom += 2*np.sqrt(df[self.colXx]*df[self.colYy] - df[self.colXy]**2)
223 e /= denom
225 if self.halvePhaseAngle:
226 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
227 # |e|*exp(i*theta). So we multiply by |e| and take its square root
228 # instead of the more expensive trig calls.
229 e *= np.abs(e)
230 return np.sqrt(e)
231 else:
232 return e
235class CalcEDiff(DataFrameAction):
236 """Calculate the difference of two ellipticities as a complex quantity.
238 The complex ellipticity difference between e_A and e_B is defined as
239 e_A - e_B = de = |de|exp(j*2*theta).
241 See Also
242 --------
243 CalcE
245 Notes
246 -----
248 1. This is a shape measurement used for doing QA on the ellipticity
249 of the sources.
251 2. For plotting purposes we might want to plot |de|*exp(j*theta).
252 If `halvePhaseAngle` config parameter is set to `True`, then
253 the returned quantity therefore corresponds to |e|*exp(j*theta).
254 """
255 colA = ConfigurableActionField(
256 doc="Ellipticity to subtract from",
257 dtype=MultiColumnAction,
258 default=CalcE,
259 )
261 colB = ConfigurableActionField(
262 doc="Ellipticity to subtract",
263 dtype=MultiColumnAction,
264 default=CalcE,
265 )
267 halvePhaseAngle = Field(
268 doc="Divide the phase angle by 2? Suitable for quiver plots.",
269 dtype=bool,
270 default=False,
271 )
273 @property
274 def columns(self):
275 yield from self.colA.columns
276 yield from self.colB.columns
278 def __call__(self, df):
279 eMeas = self.colA(df)
280 ePSF = self.colB(df)
281 eDiff = eMeas - ePSF
282 if self.halvePhaseAngle:
283 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
284 # |e|*exp(j*theta). So we multiply by |e| and take its square root
285 # instead of the more expensive trig calls.
286 eDiff *= np.abs(eDiff)
287 return np.sqrt(eDiff)
288 else:
289 return eDiff
292class CalcE1(MultiColumnAction):
293 """Calculate chi-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or
294 epsilon-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
296 See Also
297 --------
298 CalcE
299 CalcE2
301 Note
302 ----
303 This is a shape measurement used for doing QA on the ellipticity
304 of the sources.
305 """
307 colXx = Field(
308 doc="The column name to get the xx shape component from.",
309 dtype=str,
310 default="ixx",
311 )
313 colYy = Field(
314 doc="The column name to get the yy shape component from.",
315 dtype=str,
316 default="iyy",
317 )
319 colXy = Field(
320 doc="The column name to get the xy shape component from.",
321 dtype=str,
322 default="ixy",
323 optional=True,
324 )
326 ellipticityType = ChoiceField(
327 doc="The type of ellipticity to calculate",
328 dtype=str,
329 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)",
330 "epsilon": ("Shear, defined as (Ixx - Iyy)/"
331 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
332 ),
333 },
334 default="chi",
335 )
337 @property
338 def columns(self):
339 if self.ellipticityType == "chi":
340 return (self.colXx, self.colYy)
341 else:
342 return (self.colXx, self.colYy, self.colXy)
344 def __call__(self, df):
345 denom = df[self.colXx] + df[self.colYy]
346 if self.ellipticityType == "epsilon":
347 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
348 e1 = (df[self.colXx] - df[self.colYy])/denom
350 return e1
352 def validate(self):
353 super().validate()
354 if self.ellipticityType == "epsilon" and self.colXy is None:
355 raise ValueError("colXy is required for epsilon-type shear ellipticity")
358class CalcE2(MultiColumnAction):
359 """Calculate chi-type e2 = 2Ixy/(Ixx+Iyy) or
360 epsilon-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
362 See Also
363 --------
364 CalcE
365 CalcE1
367 Note
368 ----
369 This is a shape measurement used for doing QA on the ellipticity
370 of the sources.
371 """
373 colXx = Field(
374 doc="The column name to get the xx shape component from.",
375 dtype=str,
376 default="ixx",
377 )
379 colYy = Field(
380 doc="The column name to get the yy shape component from.",
381 dtype=str,
382 default="iyy",
383 )
385 colXy = Field(
386 doc="The column name to get the xy shape component from.",
387 dtype=str,
388 default="ixy",
389 )
391 ellipticityType = ChoiceField(
392 doc="The type of ellipticity to calculate",
393 dtype=str,
394 allowed={"chi": "Distortion, defined as 2*Ixy/(Ixx + Iyy)",
395 "epsilon": ("Shear, defined as 2*Ixy/"
396 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
397 ),
398 },
399 default="chi",
400 )
402 @property
403 def columns(self):
404 return (self.colXx, self.colYy, self.colXy)
406 def __call__(self, df):
407 denom = df[self.colXx] + df[self.colYy]
408 if self.ellipticityType == "epsilon":
409 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
410 e2 = 2*df[self.colXy]/denom
411 return e2
414class CalcShapeSize(MultiColumnAction):
415 """Calculate a size: (Ixx*Iyy - Ixy**2)**0.25 OR (0.5*(Ixx + Iyy))**0.5
417 The square of size measure is typically expressed either as the arithmetic
418 mean of the eigenvalues of the moment matrix (trace radius) or as the
419 geometric mean of the eigenvalues (determinant radius), which can be
420 specified using the ``sizeType`` parameter. Both of these measures give the
421 `sigma^2` parameter for a 2D Gaussian.
423 Since lensing preserves surface brightness, the determinant radius relates
424 the magnification cleanly as it is derived from the area of isophotes, but
425 have a slightly higher chance of being NaNs for noisy moment estimates.
427 Note
428 ----
429 This is a size measurement used for doing QA on the ellipticity
430 of the sources.
431 """
433 colXx = Field(
434 doc="The column name to get the xx shape component from.",
435 dtype=str,
436 default="ixx",
437 )
439 colYy = Field(
440 doc="The column name to get the yy shape component from.",
441 dtype=str,
442 default="iyy",
443 )
445 colXy = Field(
446 doc="The column name to get the xy shape component from.",
447 dtype=str,
448 default="ixy",
449 optional=True,
450 )
452 sizeType = ChoiceField(
453 doc="The type of size to calculate",
454 dtype=str,
455 default="determinant",
456 allowed={"trace": "trace radius",
457 "determinant": "determinant radius",
458 },
459 )
461 @property
462 def columns(self):
463 if self.sizeType == "trace":
464 return (self.colXx, self.colYy,)
465 else:
466 return (self.colXx, self.colYy, self.colXy)
468 def __call__(self, df):
469 if self.sizeType == "trace":
470 size = np.power(0.5*(df[self.colXx] + df[self.colYy]), 0.5)
471 else:
472 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25)
474 return size
476 def validate(self):
477 super().validate()
478 if self.sizeType == "determinant" and self.colXy is None:
479 raise ValueError("colXy is required for determinant-type size")
482class ColorDiff(MultiColumnAction):
483 """Calculate the difference between two colors;
484 each color is derived from two flux columns.
486 The color difference is computed as (color1 - color2) with:
488 color1 = color1_mag1 - color1_mag2
489 color2 = color2_mag1 - color2_mag2
491 where color1_mag1 is the magnitude associated with color1_flux1, etc.
493 Parameters
494 ----------
495 df : `pandas.core.frame.DataFrame`
496 The catalog to calculate the color difference from.
498 Returns
499 -------
500 The color difference in millimags.
502 Notes
503 -----
504 The flux columns need to be in units that can be converted
505 to janskies. This action doesn't have any calibration
506 information and assumes that the fluxes are already
507 calibrated.
508 """
509 color1_flux1 = Field(doc="Column for flux1 to determine color1",
510 dtype=str)
511 color1_flux1_units = Field(doc="Units for color1_flux1",
512 dtype=str,
513 default="nanojansky")
514 color1_flux2 = Field(doc="Column for flux2 to determine color1",
515 dtype=str)
516 color1_flux2_units = Field(doc="Units for color1_flux2",
517 dtype=str,
518 default="nanojansky")
519 color2_flux1 = Field(doc="Column for flux1 to determine color2",
520 dtype=str)
521 color2_flux1_units = Field(doc="Units for color2_flux1",
522 dtype=str,
523 default="nanojansky")
524 color2_flux2 = Field(doc="Column for flux2 to determine color2",
525 dtype=str)
526 color2_flux2_units = Field(doc="Units for color2_flux2",
527 dtype=str,
528 default="nanojansky")
529 return_millimags = Field(doc="Use millimags or not?",
530 dtype=bool,
531 default=True)
533 @property
534 def columns(self):
535 return (self.color1_flux1,
536 self.color1_flux2,
537 self.color2_flux1,
538 self.color2_flux2)
540 def __call__(self, df):
541 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
542 color1_mag1 = color1_flux1.to(u.ABmag).value
544 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
545 color1_mag2 = color1_flux2.to(u.ABmag).value
547 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
548 color2_mag1 = color2_flux1.to(u.ABmag).value
550 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
551 color2_mag2 = color2_flux2.to(u.ABmag).value
553 color1 = color1_mag1 - color1_mag2
554 color2 = color2_mag1 - color2_mag2
556 color_diff = color1 - color2
558 if self.return_millimags:
559 color_diff = color_diff*1000
561 return color_diff
564class ColorDiffPull(ColorDiff):
565 """Calculate the difference between two colors, scaled by the color error;
566 Each color is derived from two flux columns.
568 The color difference is computed as (color1 - color2) with:
570 color1 = color1_mag1 - color1_mag2
571 color2 = color2_mag1 - color2_mag2
573 where color1_mag1 is the magnitude associated with color1_flux1, etc.
575 The color difference (color1 - color2) is then scaled by the error on
576 the color as computed from color1_flux1_err, color1_flux2_err,
577 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted
578 if the comparison is between an "observed" catalog and a "truth" catalog.
580 Parameters
581 ----------
582 df : `pandas.core.frame.DataFrame`
583 The catalog to calculate the color difference from.
585 Returns
586 -------
587 The color difference scaled by the error.
589 Notes
590 -----
591 The flux columns need to be in units that can be converted
592 to janskies. This action doesn't have any calibration
593 information and assumes that the fluxes are already
594 calibrated.
595 """
596 color1_flux1_err = Field(doc="Error column for flux1 for color1",
597 dtype=str,
598 default="")
599 color1_flux2_err = Field(doc="Error column for flux2 for color1",
600 dtype=str,
601 default="")
602 color2_flux1_err = Field(doc="Error column for flux1 for color2",
603 dtype=str,
604 default="")
605 color2_flux2_err = Field(doc="Error column for flux2 for color2",
606 dtype=str,
607 default="")
609 def validate(self):
610 super().validate()
612 color1_errors = False
613 color2_errors = False
615 if self.color1_flux1_err and self.color1_flux2_err:
616 color1_errors = True
617 elif ((self.color1_flux1_err and not self.color1_flux2_err)
618 or (not self.color1_flux1_err and self.color1_flux2_err)):
619 raise ValueError("Must set both color1_flux1_err and color1_flux2_err if either is set.")
620 if self.color2_flux1_err and self.color2_flux2_err:
621 color2_errors = True
622 elif ((self.color2_flux1_err and not self.color2_flux2_err)
623 or (not self.color2_flux1_err and self.color2_flux2_err)):
624 raise ValueError("Must set both color2_flux1_err and color2_flux2_err if either is set.")
626 if not color1_errors and not color2_errors:
627 raise ValueError("Must configure flux errors for at least color1 or color2.")
629 @property
630 def columns(self):
631 columns = (self.color1_flux1,
632 self.color1_flux2,
633 self.color2_flux1,
634 self.color2_flux2)
636 if self.color1_flux1_err:
637 # Config validation ensures if one is set, both are set.
638 columns = columns + (self.color1_flux1_err,
639 self.color1_flux2_err)
641 if self.color2_flux1_err:
642 # Config validation ensures if one is set, both are set.
643 columns = columns + (self.color2_flux1_err,
644 self.color2_flux2_err)
646 return columns
648 def __call__(self, df):
649 k = 2.5/np.log(10.)
651 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
652 color1_mag1 = color1_flux1.to(u.ABmag).value
653 if self.color1_flux1_err:
654 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values
655 else:
656 color1_mag1_err = 0.0
658 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
659 color1_mag2 = color1_flux2.to(u.ABmag).value
660 if self.color1_flux2_err:
661 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values
662 else:
663 color1_mag2_err = 0.0
665 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
666 color2_mag1 = color2_flux1.to(u.ABmag).value
667 if self.color2_flux1_err:
668 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values
669 else:
670 color2_mag1_err = 0.0
672 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
673 color2_mag2 = color2_flux2.to(u.ABmag).value
674 if self.color2_flux2_err:
675 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values
676 else:
677 color2_mag2_err = 0.0
679 color1 = color1_mag1 - color1_mag2
680 err1_sq = color1_mag1_err**2. + color1_mag2_err**2.
681 color2 = color2_mag1 - color2_mag2
682 err2_sq = color2_mag1_err**2. + color2_mag2_err**2.
684 color_diff = color1 - color2
686 pull = color_diff/np.sqrt(err1_sq + err2_sq)
688 return pull