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
278 This is a size measurement used for doing QA on the ellipticity
279 of the sources."""
281 colXx = Field(doc="The column name to get the xx shape component from.",
282 dtype=str,
283 default="ixx")
285 colYy = Field(doc="The column name to get the yy shape component from.",
286 dtype=str,
287 default="iyy")
289 colXy = Field(doc="The column name to get the xy shape component from.",
290 dtype=str,
291 default="ixy")
293 @property
294 def columns(self):
295 return (self.colXx, self.colYy, self.colXy)
297 def __call__(self, df):
298 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25)
299 return size
302class ColorDiff(MultiColumnAction):
303 """Calculate the difference between two colors;
304 each color is derived from two flux columns.
306 The color difference is computed as (color1 - color2) with:
308 color1 = color1_mag1 - color1_mag2
309 color2 = color2_mag1 - color2_mag2
311 where color1_mag1 is the magnitude associated with color1_flux1, etc.
313 Parameters
314 ----------
315 df : `pandas.core.frame.DataFrame`
316 The catalog to calculate the color difference from.
318 Returns
319 -------
320 The color difference in millimags.
322 Notes
323 -----
324 The flux columns need to be in units that can be converted
325 to janskies. This action doesn't have any calibration
326 information and assumes that the fluxes are already
327 calibrated.
328 """
329 color1_flux1 = Field(doc="Column for flux1 to determine color1",
330 dtype=str)
331 color1_flux1_units = Field(doc="Units for color1_flux1",
332 dtype=str,
333 default="nanojansky")
334 color1_flux2 = Field(doc="Column for flux2 to determine color1",
335 dtype=str)
336 color1_flux2_units = Field(doc="Units for color1_flux2",
337 dtype=str,
338 default="nanojansky")
339 color2_flux1 = Field(doc="Column for flux1 to determine color2",
340 dtype=str)
341 color2_flux1_units = Field(doc="Units for color2_flux1",
342 dtype=str,
343 default="nanojansky")
344 color2_flux2 = Field(doc="Column for flux2 to determine color2",
345 dtype=str)
346 color2_flux2_units = Field(doc="Units for color2_flux2",
347 dtype=str,
348 default="nanojansky")
349 return_millimags = Field(doc="Use millimags or not?",
350 dtype=bool,
351 default=True)
353 @property
354 def columns(self):
355 return (self.color1_flux1,
356 self.color1_flux2,
357 self.color2_flux1,
358 self.color2_flux2)
360 def __call__(self, df):
361 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
362 color1_mag1 = color1_flux1.to(u.ABmag).value
364 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
365 color1_mag2 = color1_flux2.to(u.ABmag).value
367 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
368 color2_mag1 = color2_flux1.to(u.ABmag).value
370 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
371 color2_mag2 = color2_flux2.to(u.ABmag).value
373 color1 = color1_mag1 - color1_mag2
374 color2 = color2_mag1 - color2_mag2
376 color_diff = color1 - color2
378 if self.return_millimags:
379 color_diff = color_diff*1000
381 return color_diff
384class ColorDiffPull(ColorDiff):
385 """Calculate the difference between two colors, scaled by the color error;
386 Each color is derived from two flux columns.
388 The color difference is computed as (color1 - color2) with:
390 color1 = color1_mag1 - color1_mag2
391 color2 = color2_mag1 - color2_mag2
393 where color1_mag1 is the magnitude associated with color1_flux1, etc.
395 The color difference (color1 - color2) is then scaled by the error on
396 the color as computed from color1_flux1_err, color1_flux2_err,
397 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted
398 if the comparison is between an "observed" catalog and a "truth" catalog.
400 Parameters
401 ----------
402 df : `pandas.core.frame.DataFrame`
403 The catalog to calculate the color difference from.
405 Returns
406 -------
407 The color difference scaled by the error.
409 Notes
410 -----
411 The flux columns need to be in units that can be converted
412 to janskies. This action doesn't have any calibration
413 information and assumes that the fluxes are already
414 calibrated.
415 """
416 color1_flux1_err = Field(doc="Error column for flux1 for color1",
417 dtype=str,
418 default="")
419 color1_flux2_err = Field(doc="Error column for flux2 for color1",
420 dtype=str,
421 default="")
422 color2_flux1_err = Field(doc="Error column for flux1 for color2",
423 dtype=str,
424 default="")
425 color2_flux2_err = Field(doc="Error column for flux2 for color2",
426 dtype=str,
427 default="")
429 def validate(self):
430 super().validate()
432 color1_errors = False
433 color2_errors = False
435 if self.color1_flux1_err and self.color1_flux2_err:
436 color1_errors = True
437 elif ((self.color1_flux1_err and not self.color1_flux2_err)
438 or (not self.color1_flux1_err and self.color1_flux2_err)):
439 raise ValueError("Must set both color1_flux1_err and color1_flux2_err if either is set.")
440 if self.color2_flux1_err and self.color2_flux2_err:
441 color2_errors = True
442 elif ((self.color2_flux1_err and not self.color2_flux2_err)
443 or (not self.color2_flux1_err and self.color2_flux2_err)):
444 raise ValueError("Must set both color2_flux1_err and color2_flux2_err if either is set.")
446 if not color1_errors and not color2_errors:
447 raise ValueError("Must configure flux errors for at least color1 or color2.")
449 @property
450 def columns(self):
451 columns = (self.color1_flux1,
452 self.color1_flux2,
453 self.color2_flux1,
454 self.color2_flux2)
456 if self.color1_flux1_err:
457 # Config validation ensures if one is set, both are set.
458 columns = columns + (self.color1_flux1_err,
459 self.color1_flux2_err)
461 if self.color2_flux1_err:
462 # Config validation ensures if one is set, both are set.
463 columns = columns + (self.color2_flux1_err,
464 self.color2_flux2_err)
466 return columns
468 def __call__(self, df):
469 k = 2.5/np.log(10.)
471 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units)
472 color1_mag1 = color1_flux1.to(u.ABmag).value
473 if self.color1_flux1_err:
474 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values
475 else:
476 color1_mag1_err = 0.0
478 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units)
479 color1_mag2 = color1_flux2.to(u.ABmag).value
480 if self.color1_flux2_err:
481 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values
482 else:
483 color1_mag2_err = 0.0
485 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units)
486 color2_mag1 = color2_flux1.to(u.ABmag).value
487 if self.color2_flux1_err:
488 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values
489 else:
490 color2_mag1_err = 0.0
492 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units)
493 color2_mag2 = color2_flux2.to(u.ABmag).value
494 if self.color2_flux2_err:
495 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values
496 else:
497 color2_mag2_err = 0.0
499 color1 = color1_mag1 - color1_mag2
500 err1_sq = color1_mag1_err**2. + color1_mag2_err**2.
501 color2 = color2_mag1 - color2_mag2
502 err2_sq = color2_mag1_err**2. + color2_mag2_err**2.
504 color_diff = color1 - color2
506 pull = color_diff/np.sqrt(err1_sq + err2_sq)
508 return pull