Coverage for python/lsst/analysis/drp/calcFunctors.py: 41%
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"]
4from lsst.pipe.tasks.configurableActions import ConfigurableActionField
5from lsst.pipe.tasks.dataFrameActions import DataFrameAction, DivideColumns, MultiColumnAction
6from lsst.pex.config import Field, DictField
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 This is a shape measurement used for doing QA on the ellipticity
138 of the sources.
140 The complex ellipticity is typically defined as
141 E = ((ixx - iyy) + 1j*(2*ixy))/(ixx + iyy) = |E|exp(i*2*theta).
143 For plotting purposes we might want to plot |E|*exp(i*theta).
144 If `halvePhaseAngle` config parameter is set to `True`, then
145 the returned quantity therefore corresponds to |E|*exp(i*theta)
146 """
148 colXx = Field(doc="The column name to get the xx shape component from.",
149 dtype=str,
150 default="ixx")
152 colYy = Field(doc="The column name to get the yy shape component from.",
153 dtype=str,
154 default="iyy")
156 colXy = Field(doc="The column name to get the xy shape component from.",
157 dtype=str,
158 default="ixy")
160 halvePhaseAngle = Field(doc=("Divide the phase angle by 2? "
161 "Suitable for quiver plots."),
162 dtype=bool,
163 default=False)
165 @property
166 def columns(self):
167 return (self.colXx, self.colYy, self.colXy)
169 def __call__(self, df):
170 e = (df[self.colXx] - df[self.colYy]) + 1j*(2*df[self.colXy])
171 e /= (df[self.colXx] + df[self.colYy])
172 if self.halvePhaseAngle:
173 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
174 # |e|*exp(i*theta). So we multiply by |e| and take its square root
175 # instead of the more expensive trig calls.
176 e *= np.abs(e)
177 return np.sqrt(e)
178 else:
179 return e
182class CalcEDiff(DataFrameAction):
183 """Calculate the difference of two ellipticities as a complex quantity.
185 This is a shape measurement used for doing QA on the ellipticity
186 of the sources.
188 The complex ellipticity difference between E_A and E_B is efined as
189 dE = |dE|exp(i*2*theta).
191 For plotting purposes we might want to plot |dE|*exp(i*theta).
192 If `halvePhaseAngle` config parameter is set to `True`, then
193 the returned quantity therefore corresponds to |E|*exp(i*theta)
194 """
195 colA = ConfigurableActionField(doc="Ellipticity to subtract from",
196 dtype=MultiColumnAction,
197 default=CalcE)
199 colB = ConfigurableActionField(doc="Ellipticity to subtract",
200 dtype=MultiColumnAction,
201 default=CalcE)
203 halvePhaseAngle = Field(doc=("Divide the phase angle by 2? "
204 "Suitable for quiver plots."),
205 dtype=bool,
206 default=False)
208 @property
209 def columns(self):
210 yield from self.colA.columns
211 yield from self.colB.columns
213 def __call__(self, df):
214 eMeas = self.colA(df)
215 ePSF = self.colB(df)
216 eDiff = eMeas - ePSF
217 if self.halvePhaseAngle:
218 # Ellipiticity is |e|*exp(i*2*theta), but we want to return
219 # |e|*exp(i*theta). So we multiply by |e| and take its square root
220 # instead of the more expensive trig calls.
221 eDiff *= np.abs(eDiff)
222 return np.sqrt(eDiff)
223 else:
224 return eDiff
227class CalcE1(MultiColumnAction):
228 """Calculate E1: (ixx - iyy)/(ixx + iyy)
229 This is a shape measurement used for doing QA on the ellipticity
230 of the sources."""
232 colXx = Field(doc="The column name to get the xx shape component from.",
233 dtype=str,
234 default="ixx")
236 colYy = Field(doc="The column name to get the yy shape component from.",
237 dtype=str,
238 default="iyy")
240 @property
241 def columns(self):
242 return (self.colXx, self.colYy)
244 def __call__(self, df):
245 e1 = (df[self.colXx] - df[self.colYy])/(df[self.colXx] + df[self.colYy])
247 return e1
250class CalcE2(MultiColumnAction):
251 """Calculate E2: 2ixy/(ixx+iyy)
252 This is a shape measurement used for doing QA on the ellipticity
253 of the sources."""
255 colXx = Field(doc="The column name to get the xx shape component from.",
256 dtype=str,
257 default="ixx")
259 colYy = Field(doc="The column name to get the yy shape component from.",
260 dtype=str,
261 default="iyy")
263 colXy = Field(doc="The column name to get the xy shape component from.",
264 dtype=str,
265 default="ixy")
267 @property
268 def columns(self):
269 return (self.colXx, self.colYy, self.colXy)
271 def __call__(self, df):
272 e2 = 2*df[self.colXy]/(df[self.colXx] + df[self.colYy])
273 return e2
276class CalcShapeSize(MultiColumnAction):
277 """Calculate a size: (ixx*iyy - ixy**2)**0.25
279 The square of size measure is typically expressed either as the arithmetic
280 mean of the eigenvalues of the moment matrix (trace radius) or as the
281 geometric mean of the eigenvalues (determinant radius, computed here).
282 Both of these measures give the `sigma^2` parameter for a 2D Gaussian.
283 The determinant radius computed here is consistent with the measure
284 computed in GalSim:
285 http://github.com/GalSim-developers/GalSim/blob/ece3bd32c1ae6ed771f2b489c5ab1b25729e0ea4/galsim/hsm.py#L42
287 This is a size measurement used for doing QA on the ellipticity
288 of the sources."""
290 colXx = Field(doc="The column name to get the xx shape component from.",
291 dtype=str,
292 default="ixx")
294 colYy = Field(doc="The column name to get the yy shape component from.",
295 dtype=str,
296 default="iyy")
298 colXy = Field(doc="The column name to get the xy shape component from.",
299 dtype=str,
300 default="ixy")
302 @property
303 def columns(self):
304 return (self.colXx, self.colYy, self.colXy)
306 def __call__(self, df):
307 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25)
308 return size
311class ColorDiff(MultiColumnAction):
312 """Calculate the difference between two colors;
313 each color is derived from two flux columns.
315 The color difference is computed as (color1 - color2) with:
317 color1 = color1_mag1 - color1_mag2
318 color2 = color2_mag1 - color2_mag2
320 where color1_mag1 is the magnitude associated with color1_flux1, etc.
322 Parameters
323 ----------
324 df : `pandas.core.frame.DataFrame`
325 The catalog to calculate the color difference from.
327 Returns
328 -------
329 The color difference in millimags.
331 Notes
332 -----
333 The flux columns need to be in units that can be converted
334 to janskies. This action doesn't have any calibration
335 information and assumes that the fluxes are already
336 calibrated.
337 """
338 color1_flux1 = Field(doc="Column for flux1 to determine color1",
339 dtype=str)
340 color1_flux1_units = Field(doc="Units for color1_flux1",
341 dtype=str,
342 default="nanojansky")
343 color1_flux2 = Field(doc="Column for flux2 to determine color1",
344 dtype=str)
345 color1_flux2_units = Field(doc="Units for color1_flux2",
346 dtype=str,
347 default="nanojansky")
348 color2_flux1 = Field(doc="Column for flux1 to determine color2",
349 dtype=str)
350 color2_flux1_units = Field(doc="Units for color2_flux1",
351 dtype=str,
352 default="nanojansky")
353 color2_flux2 = Field(doc="Column for flux2 to determine color2",
354 dtype=str)
355 color2_flux2_units = Field(doc="Units for color2_flux2",
356 dtype=str,
357 default="nanojansky")
358 return_millimags = Field(doc="Use millimags or not?",
359 dtype=bool,
360 default=True)
362 @property
363 def columns(self):
364 return (self.color1_flux1,
365 self.color1_flux2,
366 self.color2_flux1,
367 self.color2_flux2)
369 def __call__(self, df):
370 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
371 color1_mag1 = color1_flux1.to(u.ABmag).value
373 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
374 color1_mag2 = color1_flux2.to(u.ABmag).value
376 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
377 color2_mag1 = color2_flux1.to(u.ABmag).value
379 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
380 color2_mag2 = color2_flux2.to(u.ABmag).value
382 color1 = color1_mag1 - color1_mag2
383 color2 = color2_mag1 - color2_mag2
385 color_diff = color1 - color2
387 if self.return_millimags:
388 color_diff = color_diff*1000
390 return color_diff
393class ColorDiffPull(ColorDiff):
394 """Calculate the difference between two colors, scaled by the color error;
395 Each color is derived from two flux columns.
397 The color difference is computed as (color1 - color2) with:
399 color1 = color1_mag1 - color1_mag2
400 color2 = color2_mag1 - color2_mag2
402 where color1_mag1 is the magnitude associated with color1_flux1, etc.
404 The color difference (color1 - color2) is then scaled by the error on
405 the color as computed from color1_flux1_err, color1_flux2_err,
406 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted
407 if the comparison is between an "observed" catalog and a "truth" catalog.
409 Parameters
410 ----------
411 df : `pandas.core.frame.DataFrame`
412 The catalog to calculate the color difference from.
414 Returns
415 -------
416 The color difference scaled by the error.
418 Notes
419 -----
420 The flux columns need to be in units that can be converted
421 to janskies. This action doesn't have any calibration
422 information and assumes that the fluxes are already
423 calibrated.
424 """
425 color1_flux1_err = Field(doc="Error column for flux1 for color1",
426 dtype=str,
427 default="")
428 color1_flux2_err = Field(doc="Error column for flux2 for color1",
429 dtype=str,
430 default="")
431 color2_flux1_err = Field(doc="Error column for flux1 for color2",
432 dtype=str,
433 default="")
434 color2_flux2_err = Field(doc="Error column for flux2 for color2",
435 dtype=str,
436 default="")
438 def validate(self):
439 super().validate()
441 color1_errors = False
442 color2_errors = False
444 if self.color1_flux1_err and self.color1_flux2_err:
445 color1_errors = True
446 elif ((self.color1_flux1_err and not self.color1_flux2_err)
447 or (not self.color1_flux1_err and self.color1_flux2_err)):
448 raise ValueError("Must set both color1_flux1_err and color1_flux2_err if either is set.")
449 if self.color2_flux1_err and self.color2_flux2_err:
450 color2_errors = True
451 elif ((self.color2_flux1_err and not self.color2_flux2_err)
452 or (not self.color2_flux1_err and self.color2_flux2_err)):
453 raise ValueError("Must set both color2_flux1_err and color2_flux2_err if either is set.")
455 if not color1_errors and not color2_errors:
456 raise ValueError("Must configure flux errors for at least color1 or color2.")
458 @property
459 def columns(self):
460 columns = (self.color1_flux1,
461 self.color1_flux2,
462 self.color2_flux1,
463 self.color2_flux2)
465 if self.color1_flux1_err:
466 # Config validation ensures if one is set, both are set.
467 columns = columns + (self.color1_flux1_err,
468 self.color1_flux2_err)
470 if self.color2_flux1_err:
471 # Config validation ensures if one is set, both are set.
472 columns = columns + (self.color2_flux1_err,
473 self.color2_flux2_err)
475 return columns
477 def __call__(self, df):
478 k = 2.5/np.log(10.)
480 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
481 color1_mag1 = color1_flux1.to(u.ABmag).value
482 if self.color1_flux1_err:
483 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values
484 else:
485 color1_mag1_err = 0.0
487 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
488 color1_mag2 = color1_flux2.to(u.ABmag).value
489 if self.color1_flux2_err:
490 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values
491 else:
492 color1_mag2_err = 0.0
494 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
495 color2_mag1 = color2_flux1.to(u.ABmag).value
496 if self.color2_flux1_err:
497 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values
498 else:
499 color2_mag1_err = 0.0
501 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
502 color2_mag2 = color2_flux2.to(u.ABmag).value
503 if self.color2_flux2_err:
504 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values
505 else:
506 color2_mag2_err = 0.0
508 color1 = color1_mag1 - color1_mag2
509 err1_sq = color1_mag1_err**2. + color1_mag2_err**2.
510 color2 = color2_mag1 - color2_mag2
511 err2_sq = color2_mag1_err**2. + color2_mag2_err**2.
513 color_diff = color1 - color2
515 pull = color_diff/np.sqrt(err1_sq + err2_sq)
517 return pull