Coverage for python/lsst/analysis/drp/calcFunctors.py: 32%
339 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-25 03:02 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-25 03:02 -0700
1# This file is part of analysis_drp.
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/>.
22__all__ = ["SNCalculator", "SnDiffCalculator", "SnPercentDiffCalculator", "KronFluxDivPsfFlux",
23 "MagDiff", "ColorDiff", "ColorDiffPull", "ExtinctionCorrectedMagDiff",
24 "CalcE", "CalcE1", "CalcE2", "CalcEDiff", "CalcShapeSize", "CalcRhoStatistics", ]
26import logging
28import numpy as np
29import treecorr
30from astropy import units as u
32from lsst.pex.config import ChoiceField, ConfigField, DictField, Field, FieldValidationError
33from lsst.pex.config.configurableActions import ConfigurableActionField
34from lsst.pipe.tasks.dataFrameActions import (CoordColumn, DataFrameAction, DivideColumns,
35 FractionalDifferenceColumns, MultiColumnAction,
36 SingleColumnAction, DiffOfDividedColumns,
37 PercentDiffOfDividedColumns)
39from ._treecorrConfig import BinnedCorr2Config
41_LOG = logging.getLogger(__name__)
44class SNCalculator(DivideColumns):
45 """Calculate the signal to noise by default the i band PSF flux is used"""
47 def setDefaults(self):
48 super().setDefaults()
49 self.colA.column = "i_psfFlux"
50 self.colB.column = "i_psfFluxErr"
53class SnDiffCalculator(DiffOfDividedColumns):
54 """Calculate the signal to noise difference between two measurements.
56 By default the i band flux is used and the S/N comparison is between PSF
57 and CModel fluxes.
58 """
60 def setDefaults(self):
61 super().setDefaults()
62 self.colA1.column = "i_psfFlux"
63 self.colB1.column = "i_psfFluxErr"
64 self.colA2.column = "i_cModelFlux"
65 self.colB2.column = "i_cModelFluxErr"
68class SnPercentDiffCalculator(PercentDiffOfDividedColumns):
69 """Calculate the signal to noise %difference between two measurements.
71 By default the i band flux is used and the S/N comparison is between PSF
72 and CModel fluxes.
73 """
75 def setDefaults(self):
76 super().setDefaults()
77 self.colA1.column = "i_psfFlux"
78 self.colB1.column = "i_psfFluxErr"
79 self.colA2.column = "i_cModelFlux"
80 self.colB2.column = "i_cModelFluxErr"
83class KronFluxDivPsfFlux(DivideColumns):
84 """Divide the Kron instFlux by the PSF instFlux"""
86 def setDefaults(self):
87 super().setDefaults()
88 self.colA.column = "i_kronFlux"
89 self.colB.column = "i_psfFlux"
92class MagDiff(MultiColumnAction):
93 """Calculate the difference between two magnitudes;
94 each magnitude is derived from a flux column.
96 Parameters
97 ----------
98 df : `pandas.core.frame.DataFrame`
99 The catalog to calculate the magnitude difference from.
101 Returns
102 -------
103 The magnitude difference in milli mags.
105 Notes
106 -----
107 The flux columns need to be in units (specifiable in
108 the fluxUnits1 and 2 config options) that can be converted
109 to janskies. This action doesn't have any calibration
110 information and assumes that the fluxes are already
111 calibrated.
112 """
114 col1 = Field(doc="Column to subtract from", dtype=str)
115 fluxUnits1 = Field(doc="Units for col1", dtype=str, default="nanojansky")
116 col2 = Field(doc="Column to subtract", dtype=str)
117 fluxUnits2 = Field(doc="Units for col2", dtype=str, default="nanojansky")
118 returnMillimags = Field(doc="Use millimags or not?", dtype=bool, default=True)
120 @property
121 def columns(self):
122 return (self.col1, self.col2)
124 def __call__(self, df):
125 flux1 = df[self.col1].values * u.Unit(self.fluxUnits1)
126 mag1 = flux1.to(u.ABmag)
128 flux2 = df[self.col2].values * u.Unit(self.fluxUnits2)
129 mag2 = flux2.to(u.ABmag)
131 magDiff = mag1 - mag2
133 if self.returnMillimags:
134 magDiff = magDiff.to(u.mmag)
136 return magDiff
139class ExtinctionCorrectedMagDiff(DataFrameAction):
140 """Compute the difference between two magnitudes and correct for extinction
142 By default bands are derived from the <band>_ prefix on flux columns,
143 per the naming convention in the Object Table:
144 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
145 convention, bands can alternatively be supplied via the band1 or band2
146 config parameters.
147 If band1 and band2 are supplied, the flux column names are ignored.
148 """
150 magDiff = ConfigurableActionField(doc="Action that returns a difference in magnitudes",
151 default=MagDiff, dtype=DataFrameAction)
152 ebvCol = Field(doc="E(B-V) Column Name", dtype=str, default="ebv")
153 band1 = Field(doc="Optional band for magDiff.col1. Supercedes column name prefix",
154 dtype=str, optional=True, default=None)
155 band2 = Field(doc="Optional band for magDiff.col2. Supercedes column name prefix",
156 dtype=str, optional=True, default=None)
157 extinctionCoeffs = DictField(
158 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
159 "Key must be the band",
160 keytype=str, itemtype=float, optional=True,
161 default=None)
163 @property
164 def columns(self):
165 return self.magDiff.columns + (self.ebvCol,)
167 def __call__(self, df):
168 diff = self.magDiff(df)
169 if not self.extinctionCoeffs:
170 _LOG.warning("No extinction Coefficients. Not applying extinction correction")
171 return diff
173 col1Band = self.band1 if self.band1 else self.magDiff.col1.split('_')[0]
174 col2Band = self.band2 if self.band2 else self.magDiff.col2.split('_')[0]
176 for band in (col1Band, col1Band):
177 if band not in self.extinctionCoeffs:
178 _LOG.warning("%s band not found in coefficients dictionary: %s"
179 " Not applying extinction correction", band, self.extinctionCoeffs)
180 return diff
182 av1 = self.extinctionCoeffs[col1Band]
183 av2 = self.extinctionCoeffs[col2Band]
185 ebv = df[self.ebvCol].values
186 correction = (av1 - av2) * ebv * u.mag
188 if self.magDiff.returnMillimags:
189 correction = correction.to(u.mmag)
191 return diff - correction
194class CalcE(MultiColumnAction):
195 """Calculate a complex value representation of the ellipticity.
197 The complex ellipticity is typically defined as
198 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is
199 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments.
200 This is sometimes referred to as distortion, and denoted by e = (e1, e2)
201 in GalSim and referred to as chi-type ellipticity following the notation
202 in Eq. 4.4 of Bartelmann and Schneider (2001). The other definition differs
203 in normalization. It is referred to as shear, and denoted by g = (g1, g2)
204 in GalSim and referred to as epsilon-type ellipticity again following the
205 notation in Eq. 4.10 of Bartelmann and Schneider (2001). It is defined as
206 g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
208 The shear measure is unbiased in weak-lensing shear, but may exclude some
209 objects in the presence of noisy moment estimates. The distortion measure
210 is biased in weak-lensing distortion, but does not suffer from selection
211 artifacts.
213 References
214 ----------
215 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”,
216 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001.
217 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508
219 Notes
220 -----
222 1. This is a shape measurement used for doing QA on the ellipticity
223 of the sources.
225 2. For plotting purposes we might want to plot |E|*exp(i*theta).
226 If `halvePhaseAngle` config parameter is set to `True`, then
227 the returned quantity therefore corresponds to |E|*exp(i*theta).
229 See Also
230 --------
231 CalcE1
232 CalcE2
233 """
235 colXx = Field(
236 doc="The column name to get the xx shape component from.",
237 dtype=str,
238 default="ixx",
239 )
241 colYy = Field(
242 doc="The column name to get the yy shape component from.",
243 dtype=str,
244 default="iyy",
245 )
247 colXy = Field(
248 doc="The column name to get the xy shape component from.",
249 dtype=str,
250 default="ixy",
251 )
253 ellipticityType = ChoiceField(
254 doc="The type of ellipticity to calculate",
255 dtype=str,
256 allowed={"chi": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/"
257 "(Ixx + Iyy)"
258 ),
259 "epsilon": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/"
260 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
261 ),
262 },
263 default="chi",
264 )
266 halvePhaseAngle = Field(
267 doc="Divide the phase angle by 2? Suitable for quiver plots.",
268 dtype=bool,
269 default=False,
270 )
272 @property
273 def columns(self):
274 return (self.colXx, self.colYy, self.colXy)
276 def __call__(self, df):
277 e = (df[self.colXx] - df[self.colYy]) + 1j*(2*df[self.colXy])
278 denom = (df[self.colXx] + df[self.colYy])
280 if self.ellipticityType == "epsilon":
281 denom += 2*np.sqrt(df[self.colXx]*df[self.colYy] - df[self.colXy]**2)
283 e /= denom
285 if self.halvePhaseAngle:
286 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
287 # |e|*exp(i*theta). So we multiply by |e| and take its square root
288 # instead of the more expensive trig calls.
289 e *= np.abs(e)
290 return np.sqrt(e)
291 else:
292 return e
295class CalcEDiff(DataFrameAction):
296 """Calculate the difference of two ellipticities as a complex quantity.
298 The complex ellipticity difference between e_A and e_B is defined as
299 e_A - e_B = de = |de|exp(j*2*theta).
301 See Also
302 --------
303 CalcE
305 Notes
306 -----
308 1. This is a shape measurement used for doing QA on the ellipticity
309 of the sources.
311 2. For plotting purposes we might want to plot |de|*exp(j*theta).
312 If `halvePhaseAngle` config parameter is set to `True`, then
313 the returned quantity therefore corresponds to |e|*exp(j*theta).
314 """
315 colA = ConfigurableActionField(
316 doc="Ellipticity to subtract from",
317 dtype=MultiColumnAction,
318 default=CalcE,
319 )
321 colB = ConfigurableActionField(
322 doc="Ellipticity to subtract",
323 dtype=MultiColumnAction,
324 default=CalcE,
325 )
327 halvePhaseAngle = Field(
328 doc="Divide the phase angle by 2? Suitable for quiver plots.",
329 dtype=bool,
330 default=False,
331 )
333 @property
334 def columns(self):
335 yield from self.colA.columns
336 yield from self.colB.columns
338 def validate(self):
339 super().validate()
340 if self.colA.ellipticityType != self.colB.ellipticityType:
341 msg = "Both the ellipticities in CalcEDiff must have the same type."
342 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg)
344 def __call__(self, df):
345 eMeas = self.colA(df)
346 ePSF = self.colB(df)
347 eDiff = eMeas - ePSF
348 if self.halvePhaseAngle:
349 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
350 # |e|*exp(j*theta). So we multiply by |e| and take its square root
351 # instead of the more expensive trig calls.
352 eDiff *= np.abs(eDiff)
353 return np.sqrt(eDiff)
354 else:
355 return eDiff
358class CalcE1(MultiColumnAction):
359 """Calculate chi-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or
360 epsilon-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
362 See Also
363 --------
364 CalcE
365 CalcE2
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 optional=True,
390 )
392 ellipticityType = ChoiceField(
393 doc="The type of ellipticity to calculate",
394 dtype=str,
395 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)",
396 "epsilon": ("Shear, defined as (Ixx - Iyy)/"
397 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
398 ),
399 },
400 default="chi",
401 )
403 @property
404 def columns(self):
405 if self.ellipticityType == "chi":
406 return (self.colXx, self.colYy)
407 else:
408 return (self.colXx, self.colYy, self.colXy)
410 def __call__(self, df):
411 denom = df[self.colXx] + df[self.colYy]
412 if self.ellipticityType == "epsilon":
413 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
414 e1 = (df[self.colXx] - df[self.colYy])/denom
416 return e1
418 def validate(self):
419 super().validate()
420 if self.ellipticityType == "epsilon" and self.colXy is None:
421 msg = "colXy is required for epsilon-type shear ellipticity"
422 raise FieldValidationError(self.__class__.colXy, self, msg)
425class CalcE2(MultiColumnAction):
426 """Calculate chi-type e2 = 2Ixy/(Ixx+Iyy) or
427 epsilon-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
429 See Also
430 --------
431 CalcE
432 CalcE1
434 Note
435 ----
436 This is a shape measurement used for doing QA on the ellipticity
437 of the sources.
438 """
440 colXx = Field(
441 doc="The column name to get the xx shape component from.",
442 dtype=str,
443 default="ixx",
444 )
446 colYy = Field(
447 doc="The column name to get the yy shape component from.",
448 dtype=str,
449 default="iyy",
450 )
452 colXy = Field(
453 doc="The column name to get the xy shape component from.",
454 dtype=str,
455 default="ixy",
456 )
458 ellipticityType = ChoiceField(
459 doc="The type of ellipticity to calculate",
460 dtype=str,
461 allowed={"chi": "Distortion, defined as 2*Ixy/(Ixx + Iyy)",
462 "epsilon": ("Shear, defined as 2*Ixy/"
463 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
464 ),
465 },
466 default="chi",
467 )
469 @property
470 def columns(self):
471 return (self.colXx, self.colYy, self.colXy)
473 def __call__(self, df):
474 denom = df[self.colXx] + df[self.colYy]
475 if self.ellipticityType == "epsilon":
476 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
477 e2 = 2*df[self.colXy]/denom
478 return e2
481class CalcShapeSize(MultiColumnAction):
482 """Calculate a size: (Ixx*Iyy - Ixy**2)**0.25 OR (0.5*(Ixx + Iyy))**0.5
484 The square of size measure is typically expressed either as the arithmetic
485 mean of the eigenvalues of the moment matrix (trace radius) or as the
486 geometric mean of the eigenvalues (determinant radius), which can be
487 specified using the ``sizeType`` parameter. Both of these measures give the
488 `sigma^2` parameter for a 2D Gaussian.
490 Since lensing preserves surface brightness, the determinant radius relates
491 the magnification cleanly as it is derived from the area of isophotes, but
492 have a slightly higher chance of being NaNs for noisy moment estimates.
494 Note
495 ----
496 This is a size measurement used for doing QA on the ellipticity
497 of the sources.
498 """
500 colXx = Field(
501 doc="The column name to get the xx shape component from.",
502 dtype=str,
503 default="ixx",
504 )
506 colYy = Field(
507 doc="The column name to get the yy shape component from.",
508 dtype=str,
509 default="iyy",
510 )
512 colXy = Field(
513 doc="The column name to get the xy shape component from.",
514 dtype=str,
515 default="ixy",
516 optional=True,
517 )
519 sizeType = ChoiceField(
520 doc="The type of size to calculate",
521 dtype=str,
522 default="determinant",
523 allowed={"trace": "trace radius",
524 "determinant": "determinant radius",
525 },
526 )
528 @property
529 def columns(self):
530 if self.sizeType == "trace":
531 return (self.colXx, self.colYy,)
532 else:
533 return (self.colXx, self.colYy, self.colXy)
535 def __call__(self, df):
536 if self.sizeType == "trace":
537 size = np.power(0.5*(df[self.colXx] + df[self.colYy]), 0.5)
538 else:
539 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25)
541 return size
543 def validate(self):
544 super().validate()
545 if self.sizeType == "determinant" and self.colXy is None:
546 msg = "colXy is required for determinant-type size"
547 raise FieldValidationError(self.__class__.colXy, self, msg)
550class CalcRhoStatistics(DataFrameAction):
551 r"""Calculate Rho statistics.
553 Rho statistics refer to a collection of correlation functions involving
554 PSF ellipticity and size residuals. They quantify the contribution from PSF
555 leakage due to errors in PSF modeling to the weak lensing shear correlation
556 functions. The standard rho statistics are indexed from 1 to 5, and
557 this action calculates a sixth rho statistic, indexed 0.
559 Notes
560 -----
561 The exact definitions of rho statistics as defined in [1]_ are given below.
562 In addition to these five, we also compute the auto-correlation function of
563 the fractional size residuals and call it as the :math:`\rho_0( \theta )`.
565 .. math::
567 \rho_0(\theta) &= \left\langle \frac{\delta T_{PSF}}{T_{PSF}}(x) \frac{\delta T_{PSF}}{T_{PSF}}(x+\theta) \right\rangle # noqa: E501, W505
569 \rho_1(\theta) &= \langle \delta e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle # noqa: W505
571 \rho_2(\theta) &= \langle e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle
573 \rho_3(\theta) &= \left\langle (e^*_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x)) \delta e_{PSF}(x+\theta) \right\rangle # noqa: E501, W505
575 \rho_4(\theta) &= \left\langle (\delta e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505
577 \rho_5(\theta) &= \left\langle (e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505
579 The definition of ellipticity used in [1]_ correspond to ``epsilon``-type ellipticity, which is typically
580 smaller by a factor of 4 than using ``chi``-type ellipticity.
582 References
583 ----------
584 .. [1] Jarvis, M., Sheldon, E., Zuntz, J., Kacprzak, T., Bridle, S. L., et. al (2016). # noqa: W501
585 The DES Science Verification weak lensing shear catalogues.
586 MNRAS, 460, 2245–2281.
587 https://doi.org/10.1093/mnras/stw990;
588 https://arxiv.org/abs/1507.05603
589 """
591 colRa = ConfigurableActionField(doc="RA column", dtype=SingleColumnAction, default=CoordColumn)
593 colDec = ConfigurableActionField(doc="Dec column", dtype=SingleColumnAction, default=CoordColumn)
595 colXx = Field(
596 doc="The column name to get the xx shape component from.",
597 dtype=str,
598 default="ixx"
599 )
601 colYy = Field(
602 doc="The column name to get the yy shape component from.",
603 dtype=str,
604 default="iyy"
605 )
607 colXy = Field(
608 doc="The column name to get the xy shape component from.",
609 dtype=str,
610 default="ixy"
611 )
613 colPsfXx = Field(
614 doc="The column name to get the PSF xx shape component from.",
615 dtype=str,
616 default="ixxPSF"
617 )
619 colPsfYy = Field(
620 doc="The column name to get the PSF yy shape component from.",
621 dtype=str,
622 default="iyyPSF"
623 )
625 colPsfXy = Field(
626 doc="The column name to get the PSF xy shape component from.",
627 dtype=str,
628 default="ixyPSF"
629 )
631 ellipticityType = ChoiceField(
632 doc="The type of ellipticity to calculate",
633 dtype=str,
634 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)",
635 "epsilon": ("Shear, defined as (Ixx - Iyy)/"
636 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
637 ),
638 },
639 default="chi",
640 )
642 sizeType = ChoiceField(
643 doc="The type of size to calculate",
644 dtype=str,
645 default="trace",
646 allowed={"trace": "trace radius",
647 "determinant": "determinant radius",
648 },
649 )
651 treecorr = ConfigField(
652 doc="TreeCorr configuration",
653 dtype=BinnedCorr2Config,
654 )
656 def setDefaults(self):
657 super().setDefaults()
658 self.treecorr = BinnedCorr2Config()
659 self.treecorr.sep_units = "arcmin"
660 self.treecorr.metric = "Arc"
661 # Note: self.treecorr is not configured completely at this point.
662 # Exactly three of nbins, bin_size, min_sep, max_sep need to be set.
663 # These are expected to be set in the tasks that use this action.
665 @property
666 def columns(self):
667 return (
668 self.colXx,
669 self.colYy,
670 self.colXy,
671 self.colPsfXx,
672 self.colPsfYy,
673 self.colPsfXy,
674 self.colRa.column,
675 self.colDec.column,
676 )
678 def __call__(self, df):
679 # Create instances of various actions.
680 calcEMeas = CalcE(
681 colXx=self.colXx,
682 colYy=self.colYy,
683 colXy=self.colXy,
684 ellipticityType=self.ellipticityType,
685 )
686 calcEpsf = CalcE(
687 colXx=self.colPsfXx,
688 colYy=self.colPsfYy,
689 colXy=self.colPsfXy,
690 ellipticityType=self.ellipticityType,
691 )
693 calcEDiff = CalcEDiff(colA=calcEMeas, colB=calcEpsf)
695 calcSizeResiduals = FractionalDifferenceColumns(
696 colA=CalcShapeSize(
697 colXx=self.colXx,
698 colYy=self.colYy,
699 colXy=self.colXy,
700 sizeType=self.sizeType,
701 ),
702 colB=CalcShapeSize(
703 colXx=self.colPsfXx,
704 colYy=self.colPsfYy,
705 colXy=self.colPsfXy,
706 sizeType=self.sizeType,
707 ),
708 )
710 # Call the actions on the dataframe.
711 eMEAS = calcEMeas(df)
712 e1, e2 = np.real(eMEAS), np.imag(eMEAS)
713 eRes = calcEDiff(df)
714 e1Res, e2Res = np.real(eRes), np.imag(eRes)
715 sizeRes = calcSizeResiduals(df)
717 # Scale the sizeRes by ellipticities
718 e1SizeRes = e1*sizeRes
719 e2SizeRes = e2*sizeRes
721 # Package the arguments to capture auto-/cross-correlations for the
722 # Rho statistics.
723 args = {
724 0: (sizeRes, None),
725 1: (e1Res, e2Res, None, None),
726 2: (e1, e2, e1Res, e2Res),
727 3: (e1SizeRes, e2SizeRes, None, None),
728 4: (e1Res, e2Res, e1SizeRes, e2SizeRes),
729 5: (e1, e2, e1SizeRes, e2SizeRes),
730 }
732 ra = self.colRa(df)
733 dec = self.colDec(df)
735 # If RA and DEC are not in radians, they are assumed to be in degrees.
736 if self.colRa.inRadians:
737 ra *= 180.0/np.pi
738 if self.colDec.inRadians:
739 dec *= 180.0/np.pi
741 # Convert the self.treecorr Config to a kwarg dict.
742 treecorrKwargs = self.treecorr.toDict()
744 # Pass the appropriate arguments to the correlator and build a dict
745 rhoStats = {
746 rhoIndex: self._corrSpin2(ra, dec, *(args[rhoIndex]), **treecorrKwargs)
747 for rhoIndex in range(1, 6)
748 }
749 rhoStats[0] = self._corrSpin0(ra, dec, *(args[0]), **treecorrKwargs)
751 return rhoStats
753 @classmethod
754 def _corrSpin0(cls, ra, dec, k1, k2=None, raUnits="degrees", decUnits="degrees", **treecorrKwargs):
755 """Function to compute correlations between at most two scalar fields.
757 This is used to compute Rho0 statistics, given the appropriate spin-0
758 (scalar) fields, usually fractional size residuals.
760 Parameters
761 ----------
762 ra : `numpy.array`
763 The right ascension values of entries in the catalog.
764 dec : `numpy.array`
765 The declination values of entries in the catalog.
766 k1 : `numpy.array`
767 The primary scalar field.
768 k2 : `numpy.array`, optional
769 The secondary scalar field.
770 Autocorrelation of the primary field is computed if `None`.
771 raUnits : `str`, optional
772 Unit of the right ascension values. Valid options are
773 "degrees", "arcmin", "arcsec", "hours" or "radians".
774 decUnits : `str`, optional
775 Unit of the declination values. Valid options are
776 "degrees", "arcmin", "arcsec", "hours" or "radians".
777 **treecorrKwargs
778 Keyword arguments to be passed to `treecorr.KKCorrelation`.
780 Returns
781 -------
782 xy : `treecorr.KKCorrelation`
783 A `treecorr.KKCorrelation` object containing the correlation
784 function.
785 """
787 xy = treecorr.KKCorrelation(**treecorrKwargs)
788 catA = treecorr.Catalog(ra=ra, dec=dec, k=k1, ra_units=raUnits,
789 dec_units=decUnits)
790 if k2 is None:
791 # Calculate the auto-correlation
792 xy.process(catA)
793 else:
794 catB = treecorr.Catalog(ra=ra, dec=dec, k=k2, ra_units=raUnits,
795 dec_units=decUnits)
796 # Calculate the cross-correlation
797 xy.process(catA, catB)
799 return xy
801 @classmethod
802 def _corrSpin2(cls, ra, dec, g1a, g2a, g1b=None, g2b=None,
803 raUnits="degrees", decUnits="degrees", **treecorrKwargs):
804 """Function to compute correlations between shear-like fields.
806 This is used to compute Rho statistics, given the appropriate spin-2
807 (shear-like) fields.
809 Parameters
810 ----------
811 ra : `numpy.array`
812 The right ascension values of entries in the catalog.
813 dec : `numpy.array`
814 The declination values of entries in the catalog.
815 g1a : `numpy.array`
816 The first component of the primary shear-like field.
817 g2a : `numpy.array`
818 The second component of the primary shear-like field.
819 g1b : `numpy.array`, optional
820 The first component of the secondary shear-like field.
821 Autocorrelation of the primary field is computed if `None`.
822 g2b : `numpy.array`, optional
823 The second component of the secondary shear-like field.
824 Autocorrelation of the primary field is computed if `None`.
825 raUnits : `str`, optional
826 Unit of the right ascension values. Valid options are
827 "degrees", "arcmin", "arcsec", "hours" or "radians".
828 decUnits : `str`, optional
829 Unit of the declination values. Valid options are
830 "degrees", "arcmin", "arcsec", "hours" or "radians".
831 **treecorrKwargs
832 Keyword arguments to be passed to `treecorr.GGCorrelation`.
834 Returns
835 -------
836 xy : `treecorr.GGCorrelation`
837 A `treecorr.GGCorrelation` object containing the correlation
838 function.
839 """
840 xy = treecorr.GGCorrelation(**treecorrKwargs)
841 catA = treecorr.Catalog(ra=ra, dec=dec, g1=g1a, g2=g2a, ra_units=raUnits, dec_units=decUnits)
842 if g1b is None or g2b is None:
843 # Calculate the auto-correlation
844 xy.process(catA)
845 else:
846 catB = treecorr.Catalog(ra=ra, dec=dec, g1=g1b, g2=g2b, ra_units=raUnits, dec_units=decUnits)
847 # Calculate the cross-correlation
848 xy.process(catA, catB)
850 return xy
853class ColorDiff(MultiColumnAction):
854 """Calculate the difference between two colors;
855 each color is derived from two flux columns.
857 The color difference is computed as (color1 - color2) with:
859 color1 = color1_mag1 - color1_mag2
860 color2 = color2_mag1 - color2_mag2
862 where color1_mag1 is the magnitude associated with color1_flux1, etc.
864 Parameters
865 ----------
866 df : `pandas.core.frame.DataFrame`
867 The catalog to calculate the color difference from.
869 Returns
870 -------
871 The color difference in millimags.
873 Notes
874 -----
875 The flux columns need to be in units that can be converted
876 to janskies. This action doesn't have any calibration
877 information and assumes that the fluxes are already
878 calibrated.
879 """
880 color1_flux1 = Field(doc="Column for flux1 to determine color1",
881 dtype=str)
882 color1_flux1_units = Field(doc="Units for color1_flux1",
883 dtype=str,
884 default="nanojansky")
885 color1_flux2 = Field(doc="Column for flux2 to determine color1",
886 dtype=str)
887 color1_flux2_units = Field(doc="Units for color1_flux2",
888 dtype=str,
889 default="nanojansky")
890 color2_flux1 = Field(doc="Column for flux1 to determine color2",
891 dtype=str)
892 color2_flux1_units = Field(doc="Units for color2_flux1",
893 dtype=str,
894 default="nanojansky")
895 color2_flux2 = Field(doc="Column for flux2 to determine color2",
896 dtype=str)
897 color2_flux2_units = Field(doc="Units for color2_flux2",
898 dtype=str,
899 default="nanojansky")
900 return_millimags = Field(doc="Use millimags or not?",
901 dtype=bool,
902 default=True)
904 @property
905 def columns(self):
906 return (self.color1_flux1,
907 self.color1_flux2,
908 self.color2_flux1,
909 self.color2_flux2)
911 def __call__(self, df):
912 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
913 color1_mag1 = color1_flux1.to(u.ABmag).value
915 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
916 color1_mag2 = color1_flux2.to(u.ABmag).value
918 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
919 color2_mag1 = color2_flux1.to(u.ABmag).value
921 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
922 color2_mag2 = color2_flux2.to(u.ABmag).value
924 color1 = color1_mag1 - color1_mag2
925 color2 = color2_mag1 - color2_mag2
927 color_diff = color1 - color2
929 if self.return_millimags:
930 color_diff = color_diff*1000
932 return color_diff
935class ColorDiffPull(ColorDiff):
936 """Calculate the difference between two colors, scaled by the color error;
937 Each color is derived from two flux columns.
939 The color difference is computed as (color1 - color2) with:
941 color1 = color1_mag1 - color1_mag2
942 color2 = color2_mag1 - color2_mag2
944 where color1_mag1 is the magnitude associated with color1_flux1, etc.
946 The color difference (color1 - color2) is then scaled by the error on
947 the color as computed from color1_flux1_err, color1_flux2_err,
948 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted
949 if the comparison is between an "observed" catalog and a "truth" catalog.
951 Parameters
952 ----------
953 df : `pandas.core.frame.DataFrame`
954 The catalog to calculate the color difference from.
956 Returns
957 -------
958 The color difference scaled by the error.
960 Notes
961 -----
962 The flux columns need to be in units that can be converted
963 to janskies. This action doesn't have any calibration
964 information and assumes that the fluxes are already
965 calibrated.
966 """
967 color1_flux1_err = Field(doc="Error column for flux1 for color1",
968 dtype=str,
969 default="")
970 color1_flux2_err = Field(doc="Error column for flux2 for color1",
971 dtype=str,
972 default="")
973 color2_flux1_err = Field(doc="Error column for flux1 for color2",
974 dtype=str,
975 default="")
976 color2_flux2_err = Field(doc="Error column for flux2 for color2",
977 dtype=str,
978 default="")
980 def validate(self):
981 super().validate()
983 color1_errors = False
984 color2_errors = False
986 if self.color1_flux1_err and self.color1_flux2_err:
987 color1_errors = True
988 elif ((self.color1_flux1_err and not self.color1_flux2_err)
989 or (not self.color1_flux1_err and self.color1_flux2_err)):
990 msg = "Must set both color1_flux1_err and color1_flux2_err if either is set."
991 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg)
992 if self.color2_flux1_err and self.color2_flux2_err:
993 color2_errors = True
994 elif ((self.color2_flux1_err and not self.color2_flux2_err)
995 or (not self.color2_flux1_err and self.color2_flux2_err)):
996 msg = "Must set both color2_flux1_err and color2_flux2_err if either is set."
997 raise FieldValidationError(self.__class__.color2_flux1_err, self, msg)
999 if not color1_errors and not color2_errors:
1000 msg = "Must configure flux errors for at least color1 or color2."
1001 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg)
1003 @property
1004 def columns(self):
1005 columns = (self.color1_flux1,
1006 self.color1_flux2,
1007 self.color2_flux1,
1008 self.color2_flux2)
1010 if self.color1_flux1_err:
1011 # Config validation ensures if one is set, both are set.
1012 columns = columns + (self.color1_flux1_err,
1013 self.color1_flux2_err)
1015 if self.color2_flux1_err:
1016 # Config validation ensures if one is set, both are set.
1017 columns = columns + (self.color2_flux1_err,
1018 self.color2_flux2_err)
1020 return columns
1022 def __call__(self, df):
1023 k = 2.5/np.log(10.)
1025 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
1026 color1_mag1 = color1_flux1.to(u.ABmag).value
1027 if self.color1_flux1_err:
1028 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values
1029 else:
1030 color1_mag1_err = 0.0
1032 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
1033 color1_mag2 = color1_flux2.to(u.ABmag).value
1034 if self.color1_flux2_err:
1035 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values
1036 else:
1037 color1_mag2_err = 0.0
1039 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
1040 color2_mag1 = color2_flux1.to(u.ABmag).value
1041 if self.color2_flux1_err:
1042 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values
1043 else:
1044 color2_mag1_err = 0.0
1046 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
1047 color2_mag2 = color2_flux2.to(u.ABmag).value
1048 if self.color2_flux2_err:
1049 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values
1050 else:
1051 color2_mag2_err = 0.0
1053 color1 = color1_mag1 - color1_mag2
1054 err1_sq = color1_mag1_err**2. + color1_mag2_err**2.
1055 color2 = color2_mag1 - color2_mag2
1056 err2_sq = color2_mag1_err**2. + color2_mag2_err**2.
1058 color_diff = color1 - color2
1060 pull = color_diff/np.sqrt(err1_sq + err2_sq)
1062 return pull
1065class AstromDiff(MultiColumnAction):
1066 """Calculate the difference between two columns, assuming their units
1067 are degrees, and convert the difference to arcseconds.
1069 Parameters
1070 ----------
1071 df : `pandas.core.frame.DataFrame`
1072 The catalog to calculate the position difference from.
1074 Returns
1075 -------
1076 The difference.
1078 Notes
1079 -----
1080 The columns need to be in units (specifiable in
1081 the radecUnits1 and 2 config options) that can be converted
1082 to arcseconds. This action doesn't have any calibration
1083 information and assumes that the positions are already
1084 calibrated.
1085 """
1087 col1 = Field(doc="Column to subtract from", dtype=str)
1088 radecUnits1 = Field(doc="Units for col1", dtype=str, default="degree")
1089 col2 = Field(doc="Column to subtract", dtype=str)
1090 radecUnits2 = Field(doc="Units for col2", dtype=str, default="degree")
1091 returnMilliArcsecs = Field(doc="Use marcseconds or not?", dtype=bool, default=True)
1093 @property
1094 def columns(self):
1095 return (self.col1, self.col2)
1097 def __call__(self, df):
1098 angle1 = df[self.col1].values * u.Unit(self.radecUnits1)
1100 angle2 = df[self.col2].values * u.Unit(self.radecUnits2)
1102 angleDiff = angle1 - angle2
1104 if self.returnMilliArcsecs:
1105 angleDiffValue = angleDiff.to(u.arcsec) * 1000
1106 else:
1107 angleDiffValue = angleDiff.value
1108 return angleDiffValue