Coverage for python/lsst/analysis/drp/calcFunctors.py: 32%
325 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 21:50 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-06 21:50 +0000
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", "KronFluxDivPsfFlux", "MagDiff", "ColorDiff", "ColorDiffPull",
23 "ExtinctionCorrectedMagDiff", "CalcE", "CalcE1", "CalcE2", "CalcEDiff", "CalcShapeSize",
24 "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.pipe.tasks.configurableActions import ConfigurableActionField
34from lsst.pipe.tasks.dataFrameActions import (CoordColumn, DataFrameAction, DivideColumns,
35 FractionalDifferenceColumns, MultiColumnAction,
36 SingleColumnAction,)
38from ._treecorrConfig import BinnedCorr2Config
40_LOG = logging.getLogger(__name__)
43class SNCalculator(DivideColumns):
44 """Calculate the signal to noise by default the i band PSF flux is used"""
46 def setDefaults(self):
47 super().setDefaults()
48 self.colA.column = "i_psfFlux"
49 self.colB.column = "i_psfFluxErr"
52class KronFluxDivPsfFlux(DivideColumns):
53 """Divide the Kron instFlux by the PSF instFlux"""
55 def setDefaults(self):
56 super().setDefaults()
57 self.colA.column = "i_kronFlux"
58 self.colB.column = "i_psfFlux"
61class MagDiff(MultiColumnAction):
62 """Calculate the difference between two magnitudes;
63 each magnitude is derived from a flux column.
65 Parameters
66 ----------
67 df : `pandas.core.frame.DataFrame`
68 The catalog to calculate the magnitude difference from.
70 Returns
71 -------
72 The magnitude difference in milli mags.
74 Notes
75 -----
76 The flux columns need to be in units (specifiable in
77 the fluxUnits1 and 2 config options) that can be converted
78 to janskies. This action doesn't have any calibration
79 information and assumes that the fluxes are already
80 calibrated.
81 """
83 col1 = Field(doc="Column to subtract from", dtype=str)
84 fluxUnits1 = Field(doc="Units for col1", dtype=str, default="nanojansky")
85 col2 = Field(doc="Column to subtract", dtype=str)
86 fluxUnits2 = Field(doc="Units for col2", dtype=str, default="nanojansky")
87 returnMillimags = Field(doc="Use millimags or not?", dtype=bool, default=True)
89 @property
90 def columns(self):
91 return (self.col1, self.col2)
93 def __call__(self, df):
94 flux1 = df[self.col1].values * u.Unit(self.fluxUnits1)
95 mag1 = flux1.to(u.ABmag)
97 flux2 = df[self.col2].values * u.Unit(self.fluxUnits2)
98 mag2 = flux2.to(u.ABmag)
100 magDiff = mag1 - mag2
102 if self.returnMillimags:
103 magDiff = magDiff.to(u.mmag)
105 return magDiff
108class ExtinctionCorrectedMagDiff(DataFrameAction):
109 """Compute the difference between two magnitudes and correct for extinction
111 By default bands are derived from the <band>_ prefix on flux columns,
112 per the naming convention in the Object Table:
113 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another
114 convention, bands can alternatively be supplied via the band1 or band2
115 config parameters.
116 If band1 and band2 are supplied, the flux column names are ignored.
117 """
119 magDiff = ConfigurableActionField(doc="Action that returns a difference in magnitudes",
120 default=MagDiff, dtype=DataFrameAction)
121 ebvCol = Field(doc="E(B-V) Column Name", dtype=str, default="ebv")
122 band1 = Field(doc="Optional band for magDiff.col1. Supercedes column name prefix",
123 dtype=str, optional=True, default=None)
124 band2 = Field(doc="Optional band for magDiff.col2. Supercedes column name prefix",
125 dtype=str, optional=True, default=None)
126 extinctionCoeffs = DictField(
127 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band."
128 "Key must be the band",
129 keytype=str, itemtype=float, optional=True,
130 default=None)
132 @property
133 def columns(self):
134 return self.magDiff.columns + (self.ebvCol,)
136 def __call__(self, df):
137 diff = self.magDiff(df)
138 if not self.extinctionCoeffs:
139 _LOG.warning("No extinction Coefficients. Not applying extinction correction")
140 return diff
142 col1Band = self.band1 if self.band1 else self.magDiff.col1.split('_')[0]
143 col2Band = self.band2 if self.band2 else self.magDiff.col2.split('_')[0]
145 for band in (col1Band, col1Band):
146 if band not in self.extinctionCoeffs:
147 _LOG.warning("%s band not found in coefficients dictionary: %s"
148 " Not applying extinction correction", band, self.extinctionCoeffs)
149 return diff
151 av1 = self.extinctionCoeffs[col1Band]
152 av2 = self.extinctionCoeffs[col2Band]
154 ebv = df[self.ebvCol].values
155 correction = (av1 - av2) * ebv * u.mag
157 if self.magDiff.returnMillimags:
158 correction = correction.to(u.mmag)
160 return diff - correction
163class CalcE(MultiColumnAction):
164 """Calculate a complex value representation of the ellipticity.
166 The complex ellipticity is typically defined as
167 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is
168 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments.
169 This is sometimes referred to as distortion, and denoted by e = (e1, e2)
170 in GalSim and referred to as chi-type ellipticity following the notation
171 in Eq. 4.4 of Bartelmann and Schneider (2001). The other definition differs
172 in normalization. It is referred to as shear, and denoted by g = (g1, g2)
173 in GalSim and referred to as epsilon-type ellipticity again following the
174 notation in Eq. 4.10 of Bartelmann and Schneider (2001). It is defined as
175 g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
177 The shear measure is unbiased in weak-lensing shear, but may exclude some
178 objects in the presence of noisy moment estimates. The distortion measure
179 is biased in weak-lensing distortion, but does not suffer from selection
180 artifacts.
182 References
183 ----------
184 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”,
185 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001.
186 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508
188 Notes
189 -----
191 1. This is a shape measurement used for doing QA on the ellipticity
192 of the sources.
194 2. For plotting purposes we might want to plot |E|*exp(i*theta).
195 If `halvePhaseAngle` config parameter is set to `True`, then
196 the returned quantity therefore corresponds to |E|*exp(i*theta).
198 See Also
199 --------
200 CalcE1
201 CalcE2
202 """
204 colXx = Field(
205 doc="The column name to get the xx shape component from.",
206 dtype=str,
207 default="ixx",
208 )
210 colYy = Field(
211 doc="The column name to get the yy shape component from.",
212 dtype=str,
213 default="iyy",
214 )
216 colXy = Field(
217 doc="The column name to get the xy shape component from.",
218 dtype=str,
219 default="ixy",
220 )
222 ellipticityType = ChoiceField(
223 doc="The type of ellipticity to calculate",
224 dtype=str,
225 allowed={"chi": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/"
226 "(Ixx + Iyy)"
227 ),
228 "epsilon": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/"
229 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
230 ),
231 },
232 default="chi",
233 )
235 halvePhaseAngle = Field(
236 doc="Divide the phase angle by 2? Suitable for quiver plots.",
237 dtype=bool,
238 default=False,
239 )
241 @property
242 def columns(self):
243 return (self.colXx, self.colYy, self.colXy)
245 def __call__(self, df):
246 e = (df[self.colXx] - df[self.colYy]) + 1j*(2*df[self.colXy])
247 denom = (df[self.colXx] + df[self.colYy])
249 if self.ellipticityType == "epsilon":
250 denom += 2*np.sqrt(df[self.colXx]*df[self.colYy] - df[self.colXy]**2)
252 e /= denom
254 if self.halvePhaseAngle:
255 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
256 # |e|*exp(i*theta). So we multiply by |e| and take its square root
257 # instead of the more expensive trig calls.
258 e *= np.abs(e)
259 return np.sqrt(e)
260 else:
261 return e
264class CalcEDiff(DataFrameAction):
265 """Calculate the difference of two ellipticities as a complex quantity.
267 The complex ellipticity difference between e_A and e_B is defined as
268 e_A - e_B = de = |de|exp(j*2*theta).
270 See Also
271 --------
272 CalcE
274 Notes
275 -----
277 1. This is a shape measurement used for doing QA on the ellipticity
278 of the sources.
280 2. For plotting purposes we might want to plot |de|*exp(j*theta).
281 If `halvePhaseAngle` config parameter is set to `True`, then
282 the returned quantity therefore corresponds to |e|*exp(j*theta).
283 """
284 colA = ConfigurableActionField(
285 doc="Ellipticity to subtract from",
286 dtype=MultiColumnAction,
287 default=CalcE,
288 )
290 colB = ConfigurableActionField(
291 doc="Ellipticity to subtract",
292 dtype=MultiColumnAction,
293 default=CalcE,
294 )
296 halvePhaseAngle = Field(
297 doc="Divide the phase angle by 2? Suitable for quiver plots.",
298 dtype=bool,
299 default=False,
300 )
302 @property
303 def columns(self):
304 yield from self.colA.columns
305 yield from self.colB.columns
307 def validate(self):
308 super().validate()
309 if self.colA.ellipticityType != self.colB.ellipticityType:
310 msg = "Both the ellipticities in CalcEDiff must have the same type."
311 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg)
313 def __call__(self, df):
314 eMeas = self.colA(df)
315 ePSF = self.colB(df)
316 eDiff = eMeas - ePSF
317 if self.halvePhaseAngle:
318 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
319 # |e|*exp(j*theta). So we multiply by |e| and take its square root
320 # instead of the more expensive trig calls.
321 eDiff *= np.abs(eDiff)
322 return np.sqrt(eDiff)
323 else:
324 return eDiff
327class CalcE1(MultiColumnAction):
328 """Calculate chi-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or
329 epsilon-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)).
331 See Also
332 --------
333 CalcE
334 CalcE2
336 Note
337 ----
338 This is a shape measurement used for doing QA on the ellipticity
339 of the sources.
340 """
342 colXx = Field(
343 doc="The column name to get the xx shape component from.",
344 dtype=str,
345 default="ixx",
346 )
348 colYy = Field(
349 doc="The column name to get the yy shape component from.",
350 dtype=str,
351 default="iyy",
352 )
354 colXy = Field(
355 doc="The column name to get the xy shape component from.",
356 dtype=str,
357 default="ixy",
358 optional=True,
359 )
361 ellipticityType = ChoiceField(
362 doc="The type of ellipticity to calculate",
363 dtype=str,
364 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)",
365 "epsilon": ("Shear, defined as (Ixx - Iyy)/"
366 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
367 ),
368 },
369 default="chi",
370 )
372 @property
373 def columns(self):
374 if self.ellipticityType == "chi":
375 return (self.colXx, self.colYy)
376 else:
377 return (self.colXx, self.colYy, self.colXy)
379 def __call__(self, df):
380 denom = df[self.colXx] + df[self.colYy]
381 if self.ellipticityType == "epsilon":
382 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
383 e1 = (df[self.colXx] - df[self.colYy])/denom
385 return e1
387 def validate(self):
388 super().validate()
389 if self.ellipticityType == "epsilon" and self.colXy is None:
390 msg = "colXy is required for epsilon-type shear ellipticity"
391 raise FieldValidationError(self.__class__.colXy, self, msg)
394class CalcE2(MultiColumnAction):
395 """Calculate chi-type e2 = 2Ixy/(Ixx+Iyy) or
396 epsilon-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)).
398 See Also
399 --------
400 CalcE
401 CalcE1
403 Note
404 ----
405 This is a shape measurement used for doing QA on the ellipticity
406 of the sources.
407 """
409 colXx = Field(
410 doc="The column name to get the xx shape component from.",
411 dtype=str,
412 default="ixx",
413 )
415 colYy = Field(
416 doc="The column name to get the yy shape component from.",
417 dtype=str,
418 default="iyy",
419 )
421 colXy = Field(
422 doc="The column name to get the xy shape component from.",
423 dtype=str,
424 default="ixy",
425 )
427 ellipticityType = ChoiceField(
428 doc="The type of ellipticity to calculate",
429 dtype=str,
430 allowed={"chi": "Distortion, defined as 2*Ixy/(Ixx + Iyy)",
431 "epsilon": ("Shear, defined as 2*Ixy/"
432 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
433 ),
434 },
435 default="chi",
436 )
438 @property
439 def columns(self):
440 return (self.colXx, self.colYy, self.colXy)
442 def __call__(self, df):
443 denom = df[self.colXx] + df[self.colYy]
444 if self.ellipticityType == "epsilon":
445 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2)
446 e2 = 2*df[self.colXy]/denom
447 return e2
450class CalcShapeSize(MultiColumnAction):
451 """Calculate a size: (Ixx*Iyy - Ixy**2)**0.25 OR (0.5*(Ixx + Iyy))**0.5
453 The square of size measure is typically expressed either as the arithmetic
454 mean of the eigenvalues of the moment matrix (trace radius) or as the
455 geometric mean of the eigenvalues (determinant radius), which can be
456 specified using the ``sizeType`` parameter. Both of these measures give the
457 `sigma^2` parameter for a 2D Gaussian.
459 Since lensing preserves surface brightness, the determinant radius relates
460 the magnification cleanly as it is derived from the area of isophotes, but
461 have a slightly higher chance of being NaNs for noisy moment estimates.
463 Note
464 ----
465 This is a size measurement used for doing QA on the ellipticity
466 of the sources.
467 """
469 colXx = Field(
470 doc="The column name to get the xx shape component from.",
471 dtype=str,
472 default="ixx",
473 )
475 colYy = Field(
476 doc="The column name to get the yy shape component from.",
477 dtype=str,
478 default="iyy",
479 )
481 colXy = Field(
482 doc="The column name to get the xy shape component from.",
483 dtype=str,
484 default="ixy",
485 optional=True,
486 )
488 sizeType = ChoiceField(
489 doc="The type of size to calculate",
490 dtype=str,
491 default="determinant",
492 allowed={"trace": "trace radius",
493 "determinant": "determinant radius",
494 },
495 )
497 @property
498 def columns(self):
499 if self.sizeType == "trace":
500 return (self.colXx, self.colYy,)
501 else:
502 return (self.colXx, self.colYy, self.colXy)
504 def __call__(self, df):
505 if self.sizeType == "trace":
506 size = np.power(0.5*(df[self.colXx] + df[self.colYy]), 0.5)
507 else:
508 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25)
510 return size
512 def validate(self):
513 super().validate()
514 if self.sizeType == "determinant" and self.colXy is None:
515 msg = "colXy is required for determinant-type size"
516 raise FieldValidationError(self.__class__.colXy, self, msg)
519class CalcRhoStatistics(DataFrameAction):
520 r"""Calculate Rho statistics.
522 Rho statistics refer to a collection of correlation functions involving
523 PSF ellipticity and size residuals. They quantify the contribution from PSF
524 leakage due to errors in PSF modeling to the weak lensing shear correlation
525 functions. The standard rho statistics are indexed from 1 to 5, and
526 this action calculates a sixth rho statistic, indexed 0.
528 Notes
529 -----
530 The exact definitions of rho statistics as defined in [1]_ are given below.
531 In addition to these five, we also compute the auto-correlation function of
532 the fractional size residuals and call it as the :math:`\rho_0( \theta )`.
534 .. math::
536 \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
538 \rho_1(\theta) &= \langle \delta e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle # noqa: W505
540 \rho_2(\theta) &= \langle e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle
542 \rho_3(\theta) &= \left\langle (e^*_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x)) \delta e_{PSF}(x+\theta) \right\rangle # noqa: E501, W505
544 \rho_4(\theta) &= \left\langle (\delta e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505
546 \rho_5(\theta) &= \left\langle (e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505
548 The definition of ellipticity used in [1]_ correspond to ``epsilon``-type ellipticity, which is typically
549 smaller by a factor of 4 than using ``chi``-type ellipticity.
551 References
552 ----------
553 .. [1] Jarvis, M., Sheldon, E., Zuntz, J., Kacprzak, T., Bridle, S. L., et. al (2016). # noqa: W501
554 The DES Science Verification weak lensing shear catalogues.
555 MNRAS, 460, 2245–2281.
556 https://doi.org/10.1093/mnras/stw990;
557 https://arxiv.org/abs/1507.05603
558 """
560 colRa = ConfigurableActionField(doc="RA column", dtype=SingleColumnAction, default=CoordColumn)
562 colDec = ConfigurableActionField(doc="Dec column", dtype=SingleColumnAction, default=CoordColumn)
564 colXx = Field(
565 doc="The column name to get the xx shape component from.",
566 dtype=str,
567 default="ixx"
568 )
570 colYy = Field(
571 doc="The column name to get the yy shape component from.",
572 dtype=str,
573 default="iyy"
574 )
576 colXy = Field(
577 doc="The column name to get the xy shape component from.",
578 dtype=str,
579 default="ixy"
580 )
582 colPsfXx = Field(
583 doc="The column name to get the PSF xx shape component from.",
584 dtype=str,
585 default="ixxPSF"
586 )
588 colPsfYy = Field(
589 doc="The column name to get the PSF yy shape component from.",
590 dtype=str,
591 default="iyyPSF"
592 )
594 colPsfXy = Field(
595 doc="The column name to get the PSF xy shape component from.",
596 dtype=str,
597 default="ixyPSF"
598 )
600 ellipticityType = ChoiceField(
601 doc="The type of ellipticity to calculate",
602 dtype=str,
603 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)",
604 "epsilon": ("Shear, defined as (Ixx - Iyy)/"
605 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))"
606 ),
607 },
608 default="chi",
609 )
611 sizeType = ChoiceField(
612 doc="The type of size to calculate",
613 dtype=str,
614 default="trace",
615 allowed={"trace": "trace radius",
616 "determinant": "determinant radius",
617 },
618 )
620 treecorr = ConfigField(
621 doc="TreeCorr configuration",
622 dtype=BinnedCorr2Config,
623 )
625 def setDefaults(self):
626 super().setDefaults()
627 self.treecorr = BinnedCorr2Config()
628 self.treecorr.sep_units = "arcmin"
629 self.treecorr.metric = "Arc"
630 # Note: self.treecorr is not configured completely at this point.
631 # Exactly three of nbins, bin_size, min_sep, max_sep need to be set.
632 # These are expected to be set in the tasks that use this action.
634 @property
635 def columns(self):
636 return (
637 self.colXx,
638 self.colYy,
639 self.colXy,
640 self.colPsfXx,
641 self.colPsfYy,
642 self.colPsfXy,
643 self.colRa.column,
644 self.colDec.column,
645 )
647 def __call__(self, df):
648 # Create instances of various actions.
649 calcEMeas = CalcE(
650 colXx=self.colXx,
651 colYy=self.colYy,
652 colXy=self.colXy,
653 ellipticityType=self.ellipticityType,
654 )
655 calcEpsf = CalcE(
656 colXx=self.colPsfXx,
657 colYy=self.colPsfYy,
658 colXy=self.colPsfXy,
659 ellipticityType=self.ellipticityType,
660 )
662 calcEDiff = CalcEDiff(colA=calcEMeas, colB=calcEpsf)
664 calcSizeResiduals = FractionalDifferenceColumns(
665 colA=CalcShapeSize(
666 colXx=self.colXx,
667 colYy=self.colYy,
668 colXy=self.colXy,
669 sizeType=self.sizeType,
670 ),
671 colB=CalcShapeSize(
672 colXx=self.colPsfXx,
673 colYy=self.colPsfYy,
674 colXy=self.colPsfXy,
675 sizeType=self.sizeType,
676 ),
677 )
679 # Call the actions on the dataframe.
680 eMEAS = calcEMeas(df)
681 e1, e2 = np.real(eMEAS), np.imag(eMEAS)
682 eRes = calcEDiff(df)
683 e1Res, e2Res = np.real(eRes), np.imag(eRes)
684 sizeRes = calcSizeResiduals(df)
686 # Scale the sizeRes by ellipticities
687 e1SizeRes = e1*sizeRes
688 e2SizeRes = e2*sizeRes
690 # Package the arguments to capture auto-/cross-correlations for the
691 # Rho statistics.
692 args = {
693 0: (sizeRes, None),
694 1: (e1Res, e2Res, None, None),
695 2: (e1, e2, e1Res, e2Res),
696 3: (e1SizeRes, e2SizeRes, None, None),
697 4: (e1Res, e2Res, e1SizeRes, e2SizeRes),
698 5: (e1, e2, e1SizeRes, e2SizeRes),
699 }
701 ra = self.colRa(df)
702 dec = self.colDec(df)
704 # If RA and DEC are not in radians, they are assumed to be in degrees.
705 if self.colRa.inRadians:
706 ra *= 180.0/np.pi
707 if self.colDec.inRadians:
708 dec *= 180.0/np.pi
710 # Convert the self.treecorr Config to a kwarg dict.
711 treecorrKwargs = self.treecorr.toDict()
713 # Pass the appropriate arguments to the correlator and build a dict
714 rhoStats = {
715 rhoIndex: self._corrSpin2(ra, dec, *(args[rhoIndex]), **treecorrKwargs)
716 for rhoIndex in range(1, 6)
717 }
718 rhoStats[0] = self._corrSpin0(ra, dec, *(args[0]), **treecorrKwargs)
720 return rhoStats
722 @classmethod
723 def _corrSpin0(cls, ra, dec, k1, k2=None, raUnits="degrees", decUnits="degrees", **treecorrKwargs):
724 """Function to compute correlations between at most two scalar fields.
726 This is used to compute Rho0 statistics, given the appropriate spin-0
727 (scalar) fields, usually fractional size residuals.
729 Parameters
730 ----------
731 ra : `numpy.array`
732 The right ascension values of entries in the catalog.
733 dec : `numpy.array`
734 The declination values of entries in the catalog.
735 k1 : `numpy.array`
736 The primary scalar field.
737 k2 : `numpy.array`, optional
738 The secondary scalar field.
739 Autocorrelation of the primary field is computed if `None`.
740 raUnits : `str`, optional
741 Unit of the right ascension values. Valid options are
742 "degrees", "arcmin", "arcsec", "hours" or "radians".
743 decUnits : `str`, optional
744 Unit of the declination values. Valid options are
745 "degrees", "arcmin", "arcsec", "hours" or "radians".
746 **treecorrKwargs
747 Keyword arguments to be passed to `treecorr.KKCorrelation`.
749 Returns
750 -------
751 xy : `treecorr.KKCorrelation`
752 A `treecorr.KKCorrelation` object containing the correlation
753 function.
754 """
756 xy = treecorr.KKCorrelation(**treecorrKwargs)
757 catA = treecorr.Catalog(ra=ra, dec=dec, k=k1, ra_units=raUnits,
758 dec_units=decUnits)
759 if k2 is None:
760 # Calculate the auto-correlation
761 xy.process(catA)
762 else:
763 catB = treecorr.Catalog(ra=ra, dec=dec, k=k2, ra_units=raUnits,
764 dec_units=decUnits)
765 # Calculate the cross-correlation
766 xy.process(catA, catB)
768 return xy
770 @classmethod
771 def _corrSpin2(cls, ra, dec, g1a, g2a, g1b=None, g2b=None,
772 raUnits="degrees", decUnits="degrees", **treecorrKwargs):
773 """Function to compute correlations between shear-like fields.
775 This is used to compute Rho statistics, given the appropriate spin-2
776 (shear-like) fields.
778 Parameters
779 ----------
780 ra : `numpy.array`
781 The right ascension values of entries in the catalog.
782 dec : `numpy.array`
783 The declination values of entries in the catalog.
784 g1a : `numpy.array`
785 The first component of the primary shear-like field.
786 g2a : `numpy.array`
787 The second component of the primary shear-like field.
788 g1b : `numpy.array`, optional
789 The first component of the secondary shear-like field.
790 Autocorrelation of the primary field is computed if `None`.
791 g2b : `numpy.array`, optional
792 The second component of the secondary shear-like field.
793 Autocorrelation of the primary field is computed if `None`.
794 raUnits : `str`, optional
795 Unit of the right ascension values. Valid options are
796 "degrees", "arcmin", "arcsec", "hours" or "radians".
797 decUnits : `str`, optional
798 Unit of the declination values. Valid options are
799 "degrees", "arcmin", "arcsec", "hours" or "radians".
800 **treecorrKwargs
801 Keyword arguments to be passed to `treecorr.GGCorrelation`.
803 Returns
804 -------
805 xy : `treecorr.GGCorrelation`
806 A `treecorr.GGCorrelation` object containing the correlation
807 function.
808 """
809 xy = treecorr.GGCorrelation(**treecorrKwargs)
810 catA = treecorr.Catalog(ra=ra, dec=dec, g1=g1a, g2=g2a, ra_units=raUnits, dec_units=decUnits)
811 if g1b is None or g2b is None:
812 # Calculate the auto-correlation
813 xy.process(catA)
814 else:
815 catB = treecorr.Catalog(ra=ra, dec=dec, g1=g1b, g2=g2b, ra_units=raUnits, dec_units=decUnits)
816 # Calculate the cross-correlation
817 xy.process(catA, catB)
819 return xy
822class ColorDiff(MultiColumnAction):
823 """Calculate the difference between two colors;
824 each color is derived from two flux columns.
826 The color difference is computed as (color1 - color2) with:
828 color1 = color1_mag1 - color1_mag2
829 color2 = color2_mag1 - color2_mag2
831 where color1_mag1 is the magnitude associated with color1_flux1, etc.
833 Parameters
834 ----------
835 df : `pandas.core.frame.DataFrame`
836 The catalog to calculate the color difference from.
838 Returns
839 -------
840 The color difference in millimags.
842 Notes
843 -----
844 The flux columns need to be in units that can be converted
845 to janskies. This action doesn't have any calibration
846 information and assumes that the fluxes are already
847 calibrated.
848 """
849 color1_flux1 = Field(doc="Column for flux1 to determine color1",
850 dtype=str)
851 color1_flux1_units = Field(doc="Units for color1_flux1",
852 dtype=str,
853 default="nanojansky")
854 color1_flux2 = Field(doc="Column for flux2 to determine color1",
855 dtype=str)
856 color1_flux2_units = Field(doc="Units for color1_flux2",
857 dtype=str,
858 default="nanojansky")
859 color2_flux1 = Field(doc="Column for flux1 to determine color2",
860 dtype=str)
861 color2_flux1_units = Field(doc="Units for color2_flux1",
862 dtype=str,
863 default="nanojansky")
864 color2_flux2 = Field(doc="Column for flux2 to determine color2",
865 dtype=str)
866 color2_flux2_units = Field(doc="Units for color2_flux2",
867 dtype=str,
868 default="nanojansky")
869 return_millimags = Field(doc="Use millimags or not?",
870 dtype=bool,
871 default=True)
873 @property
874 def columns(self):
875 return (self.color1_flux1,
876 self.color1_flux2,
877 self.color2_flux1,
878 self.color2_flux2)
880 def __call__(self, df):
881 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
882 color1_mag1 = color1_flux1.to(u.ABmag).value
884 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
885 color1_mag2 = color1_flux2.to(u.ABmag).value
887 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
888 color2_mag1 = color2_flux1.to(u.ABmag).value
890 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
891 color2_mag2 = color2_flux2.to(u.ABmag).value
893 color1 = color1_mag1 - color1_mag2
894 color2 = color2_mag1 - color2_mag2
896 color_diff = color1 - color2
898 if self.return_millimags:
899 color_diff = color_diff*1000
901 return color_diff
904class ColorDiffPull(ColorDiff):
905 """Calculate the difference between two colors, scaled by the color error;
906 Each color is derived from two flux columns.
908 The color difference is computed as (color1 - color2) with:
910 color1 = color1_mag1 - color1_mag2
911 color2 = color2_mag1 - color2_mag2
913 where color1_mag1 is the magnitude associated with color1_flux1, etc.
915 The color difference (color1 - color2) is then scaled by the error on
916 the color as computed from color1_flux1_err, color1_flux2_err,
917 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted
918 if the comparison is between an "observed" catalog and a "truth" catalog.
920 Parameters
921 ----------
922 df : `pandas.core.frame.DataFrame`
923 The catalog to calculate the color difference from.
925 Returns
926 -------
927 The color difference scaled by the error.
929 Notes
930 -----
931 The flux columns need to be in units that can be converted
932 to janskies. This action doesn't have any calibration
933 information and assumes that the fluxes are already
934 calibrated.
935 """
936 color1_flux1_err = Field(doc="Error column for flux1 for color1",
937 dtype=str,
938 default="")
939 color1_flux2_err = Field(doc="Error column for flux2 for color1",
940 dtype=str,
941 default="")
942 color2_flux1_err = Field(doc="Error column for flux1 for color2",
943 dtype=str,
944 default="")
945 color2_flux2_err = Field(doc="Error column for flux2 for color2",
946 dtype=str,
947 default="")
949 def validate(self):
950 super().validate()
952 color1_errors = False
953 color2_errors = False
955 if self.color1_flux1_err and self.color1_flux2_err:
956 color1_errors = True
957 elif ((self.color1_flux1_err and not self.color1_flux2_err)
958 or (not self.color1_flux1_err and self.color1_flux2_err)):
959 msg = "Must set both color1_flux1_err and color1_flux2_err if either is set."
960 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg)
961 if self.color2_flux1_err and self.color2_flux2_err:
962 color2_errors = True
963 elif ((self.color2_flux1_err and not self.color2_flux2_err)
964 or (not self.color2_flux1_err and self.color2_flux2_err)):
965 msg = "Must set both color2_flux1_err and color2_flux2_err if either is set."
966 raise FieldValidationError(self.__class__.color2_flux1_err, self, msg)
968 if not color1_errors and not color2_errors:
969 msg = "Must configure flux errors for at least color1 or color2."
970 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg)
972 @property
973 def columns(self):
974 columns = (self.color1_flux1,
975 self.color1_flux2,
976 self.color2_flux1,
977 self.color2_flux2)
979 if self.color1_flux1_err:
980 # Config validation ensures if one is set, both are set.
981 columns = columns + (self.color1_flux1_err,
982 self.color1_flux2_err)
984 if self.color2_flux1_err:
985 # Config validation ensures if one is set, both are set.
986 columns = columns + (self.color2_flux1_err,
987 self.color2_flux2_err)
989 return columns
991 def __call__(self, df):
992 k = 2.5/np.log(10.)
994 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
995 color1_mag1 = color1_flux1.to(u.ABmag).value
996 if self.color1_flux1_err:
997 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values
998 else:
999 color1_mag1_err = 0.0
1001 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
1002 color1_mag2 = color1_flux2.to(u.ABmag).value
1003 if self.color1_flux2_err:
1004 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values
1005 else:
1006 color1_mag2_err = 0.0
1008 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
1009 color2_mag1 = color2_flux1.to(u.ABmag).value
1010 if self.color2_flux1_err:
1011 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values
1012 else:
1013 color2_mag1_err = 0.0
1015 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
1016 color2_mag2 = color2_flux2.to(u.ABmag).value
1017 if self.color2_flux2_err:
1018 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values
1019 else:
1020 color2_mag2_err = 0.0
1022 color1 = color1_mag1 - color1_mag2
1023 err1_sq = color1_mag1_err**2. + color1_mag2_err**2.
1024 color2 = color2_mag1 - color2_mag2
1025 err2_sq = color2_mag1_err**2. + color2_mag2_err**2.
1027 color_diff = color1 - color2
1029 pull = color_diff/np.sqrt(err1_sq + err2_sq)
1031 return pull
1034class AstromDiff(MultiColumnAction):
1035 """Calculate the difference between two columns, assuming their units
1036 are degrees, and convert the difference to arcseconds.
1038 Parameters
1039 ----------
1040 df : `pandas.core.frame.DataFrame`
1041 The catalog to calculate the position difference from.
1043 Returns
1044 -------
1045 The difference.
1047 Notes
1048 -----
1049 The columns need to be in units (specifiable in
1050 the radecUnits1 and 2 config options) that can be converted
1051 to arcseconds. This action doesn't have any calibration
1052 information and assumes that the positions are already
1053 calibrated.
1054 """
1056 col1 = Field(doc="Column to subtract from", dtype=str)
1057 radecUnits1 = Field(doc="Units for col1", dtype=str, default="degree")
1058 col2 = Field(doc="Column to subtract", dtype=str)
1059 radecUnits2 = Field(doc="Units for col2", dtype=str, default="degree")
1060 returnMilliArcsecs = Field(doc="Use marcseconds or not?", dtype=bool, default=True)
1062 @property
1063 def columns(self):
1064 return (self.col1, self.col2)
1066 def __call__(self, df):
1067 angle1 = df[self.col1].values * u.Unit(self.radecUnits1)
1069 angle2 = df[self.col2].values * u.Unit(self.radecUnits2)
1071 angleDiff = angle1 - angle2
1073 if self.returnMilliArcsecs:
1074 angleDiffValue = angleDiff.to(u.arcsec) * 1000
1075 else:
1076 angleDiffValue = angleDiff.value
1077 return angleDiffValue