Coverage for python/lsst/ap/association/diaCalculationPlugins.py : 51%

Hot-keys 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# This file is part of ap_association.
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"""Plugins for use in DiaSource summary statistics.
24Output columns must be
25as defined in the schema of the Apdb both in name and units.
26"""
28import functools
29import warnings
31from astropy.stats import median_absolute_deviation
32import numpy as np
33import pandas as pd
34from scipy.optimize import lsq_linear
36import lsst.geom as geom
37import lsst.pex.config as pexConfig
38import lsst.sphgeom as sphgeom
40from .diaCalculation import (
41 DiaObjectCalculationPluginConfig,
42 DiaObjectCalculationPlugin)
43from lsst.meas.base.pluginRegistry import register
45__all__ = ("MeanDiaPositionConfig", "MeanDiaPosition",
46 "HTMIndexDiaPosition", "HTMIndexDiaPositionConfig",
47 "NumDiaSourcesDiaPlugin", "NumDiaSourcesDiaPluginConfig",
48 "SimpleSourceFlagDiaPlugin", "SimpleSourceFlagDiaPluginConfig",
49 "WeightedMeanDiaPsFluxConfig", "WeightedMeanDiaPsFlux",
50 "PercentileDiaPsFlux", "PercentileDiaPsFluxConfig",
51 "SigmaDiaPsFlux", "SigmaDiaPsFluxConfig",
52 "Chi2DiaPsFlux", "Chi2DiaPsFluxConfig",
53 "MadDiaPsFlux", "MadDiaPsFluxConfig",
54 "SkewDiaPsFlux", "SkewDiaPsFluxConfig",
55 "MinMaxDiaPsFlux", "MinMaxDiaPsFluxConfig",
56 "MaxSlopeDiaPsFlux", "MaxSlopeDiaPsFluxConfig",
57 "ErrMeanDiaPsFlux", "ErrMeanDiaPsFluxConfig",
58 "LinearFitDiaPsFlux", "LinearFitDiaPsFluxConfig",
59 "StetsonJDiaPsFlux", "StetsonJDiaPsFluxConfig",
60 "WeightedMeanDiaTotFlux", "WeightedMeanDiaTotFluxConfig",
61 "SigmaDiaTotFlux", "SigmaDiaTotFluxConfig")
64def catchWarnings(_func=None, *, warns=[]):
65 """Decorator for generically catching numpy warnings.
66 """
67 def decoratorCatchWarnings(func):
68 @functools.wraps(func)
69 def wrapperCatchWarnings(*args, **kwargs):
70 with warnings.catch_warnings():
71 for val in warns:
72 warnings.filterwarnings("ignore", val)
73 return func(*args, **kwargs)
74 return wrapperCatchWarnings
76 if _func is None: 76 ↛ 79line 76 didn't jump to line 79, because the condition on line 76 was never false
77 return decoratorCatchWarnings
78 else:
79 return decoratorCatchWarnings(_func)
82class MeanDiaPositionConfig(DiaObjectCalculationPluginConfig):
83 pass
86@register("ap_meanPosition")
87class MeanDiaPosition(DiaObjectCalculationPlugin):
88 """Compute the mean position of a DiaObject given a set of DiaSources.
89 """
91 ConfigClass = MeanDiaPositionConfig
93 plugType = 'multi'
95 outputCols = ["ra", "decl", "radecTai"]
97 @classmethod
98 def getExecutionOrder(cls):
99 return cls.DEFAULT_CATALOGCALCULATION
101 def calculate(self, diaObjects, diaSources, **kwargs):
102 """Compute the mean ra/dec position of the diaObject given the
103 diaSource locations.
105 Parameters
106 ----------
107 diaObjects : `pandas.DataFrame`
108 Summary objects to store values in.
109 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
110 Catalog of DiaSources summarized by this DiaObject.
111 **kwargs
112 Any additional keyword arguments that may be passed to the plugin.
113 """
114 for outCol in self.outputCols:
115 if outCol not in diaObjects.columns:
116 diaObjects[outCol] = np.nan
118 def _computeMeanPos(df):
119 aveCoord = geom.averageSpherePoint(
120 list(geom.SpherePoint(src["ra"], src["decl"], geom.degrees)
121 for idx, src in df.iterrows()))
122 ra = aveCoord.getRa().asDegrees()
123 decl = aveCoord.getDec().asDegrees()
124 if np.isnan(ra) or np.isnan(decl):
125 radecTai = np.nan
126 else:
127 radecTai = df["midPointTai"].max()
129 return pd.Series({"ra": aveCoord.getRa().asDegrees(),
130 "decl": aveCoord.getDec().asDegrees(),
131 "radecTai": radecTai})
133 ans = diaSources.apply(_computeMeanPos)
134 diaObjects.loc[:, ["ra", "decl", "radecTai"]] = ans
137class HTMIndexDiaPositionConfig(DiaObjectCalculationPluginConfig):
139 htmLevel = pexConfig.Field(
140 dtype=int,
141 doc="Level of the HTM pixelization.",
142 default=20,
143 )
146@register("ap_HTMIndex")
147class HTMIndexDiaPosition(DiaObjectCalculationPlugin):
148 """Compute the mean position of a DiaObject given a set of DiaSources.
149 """
150 ConfigClass = HTMIndexDiaPositionConfig
152 plugType = 'single'
154 inputCols = ["ra", "decl"]
155 outputCols = ["pixelId"]
157 def __init__(self, config, name, metadata):
158 DiaObjectCalculationPlugin.__init__(self, config, name, metadata)
159 self.pixelator = sphgeom.HtmPixelization(self.config.htmLevel)
161 @classmethod
162 def getExecutionOrder(cls):
163 return cls.FLUX_MOMENTS_CALCULATED
165 def calculate(self, diaObjects, diaObjectId, **kwargs):
166 """Compute the mean position of a DiaObject given a set of DiaSources
168 Parameters
169 ----------
170 diaObjects : `pandas.dataFrame`
171 Summary objects to store values in and read ra/decl from.
172 diaObjectId : `int`
173 Id of the diaObject to update.
174 **kwargs
175 Any additional keyword arguments that may be passed to the plugin.
176 """
177 sphPoint = geom.SpherePoint(
178 diaObjects.at[diaObjectId, "ra"] * geom.degrees,
179 diaObjects.at[diaObjectId, "decl"] * geom.degrees)
180 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index(
181 sphPoint.getVector())
184class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig):
185 pass
188@register("ap_nDiaSources")
189class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin):
190 """Compute the total number of DiaSources associated with this DiaObject.
191 """
193 ConfigClass = NumDiaSourcesDiaPluginConfig
194 outputCols = ["nDiaSources"]
195 plugType = "multi"
197 @classmethod
198 def getExecutionOrder(cls):
199 return cls.DEFAULT_CATALOGCALCULATION
201 def calculate(self, diaObjects, diaSources, **kwargs):
202 """Compute the total number of DiaSources associated with this DiaObject.
204 Parameters
205 ----------
206 diaObject : `dict`
207 Summary object to store values in and read ra/decl from.
208 **kwargs
209 Any additional keyword arguments that may be passed to the plugin.
210 """
211 diaObjects.loc[:, "nDiaSources"] = diaSources.diaObjectId.count()
214class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig):
215 pass
218@register("ap_diaObjectFlag")
219class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin):
220 """Find if any DiaSource is flagged.
222 Set the DiaObject flag if any DiaSource is flagged.
223 """
225 ConfigClass = NumDiaSourcesDiaPluginConfig
226 outputCols = ["flags"]
227 plugType = "multi"
229 @classmethod
230 def getExecutionOrder(cls):
231 return cls.DEFAULT_CATALOGCALCULATION
233 def calculate(self, diaObjects, diaSources, **kwargs):
234 """Find if any DiaSource is flagged.
236 Set the DiaObject flag if any DiaSource is flagged.
238 Parameters
239 ----------
240 diaObject : `dict`
241 Summary object to store values in and read ra/decl from.
242 **kwargs
243 Any additional keyword arguments that may be passed to the plugin.
244 """
245 diaObjects.loc[:, "flags"] = diaSources.flags.any()
248class WeightedMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
249 pass
252@register("ap_meanFlux")
253class WeightedMeanDiaPsFlux(DiaObjectCalculationPlugin):
254 """Compute the weighted mean and mean error on the point source fluxes
255 of the DiaSource measured on the difference image.
257 Additionally store number of usable data points.
258 """
260 ConfigClass = WeightedMeanDiaPsFluxConfig
261 outputCols = ["PSFluxMean", "PSFluxMeanErr", "PSFluxNdata"]
262 plugType = "multi"
264 @classmethod
265 def getExecutionOrder(cls):
266 return cls.DEFAULT_CATALOGCALCULATION
268 @catchWarnings(warns=["invalid value encountered",
269 "divide by zero"])
270 def calculate(self,
271 diaObjects,
272 diaSources,
273 filterDiaSources,
274 filterName,
275 **kwargs):
276 """Compute the weighted mean and mean error of the point source flux.
278 Parameters
279 ----------
280 diaObject : `dict`
281 Summary object to store values in.
282 diaSources : `pandas.DataFrame`
283 DataFrame representing all diaSources associated with this
284 diaObject.
285 filterDiaSources : `pandas.DataFrame`
286 DataFrame representing diaSources associated with this
287 diaObject that are observed in the band pass ``filterName``.
288 filterName : `str`
289 Simple, string name of the filter for the flux being calculated.
290 **kwargs
291 Any additional keyword arguments that may be passed to the plugin.
292 """
293 meanName = "{}PSFluxMean".format(filterName)
294 errName = "{}PSFluxMeanErr".format(filterName)
295 nDataName = "{}PSFluxNdata".format(filterName)
296 if meanName not in diaObjects.columns:
297 diaObjects[meanName] = np.nan
298 if errName not in diaObjects.columns:
299 diaObjects[errName] = np.nan
300 if nDataName not in diaObjects.columns:
301 diaObjects[nDataName] = np.nan
303 def _weightedMean(df):
304 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
305 np.isnan(df["psFluxErr"]))]
306 tot_weight = np.nansum(1 / tmpDf["psFluxErr"] ** 2)
307 fluxMean = np.nansum(tmpDf["psFlux"]
308 / tmpDf["psFluxErr"] ** 2)
309 fluxMean /= tot_weight
310 fluxMeanErr = np.sqrt(1 / tot_weight)
311 nFluxData = len(tmpDf)
313 return pd.Series({meanName: fluxMean,
314 errName: fluxMeanErr,
315 nDataName: nFluxData})
317 diaObjects.loc[:, [meanName, errName, nDataName]] = \
318 filterDiaSources.apply(_weightedMean)
321class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
322 percentiles = pexConfig.ListField(
323 dtype=int,
324 default=[5, 25, 50, 75, 95],
325 doc="Percentiles to calculate to compute values for. Should be "
326 "integer values."
327 )
330@register("ap_percentileFlux")
331class PercentileDiaPsFlux(DiaObjectCalculationPlugin):
332 """Compute percentiles of diaSource fluxes.
333 """
335 ConfigClass = PercentileDiaPsFluxConfig
336 # Output columns are created upon instantiation of the class.
337 outputCols = []
338 plugType = "multi"
340 def __init__(self, config, name, metadata, **kwargs):
341 DiaObjectCalculationPlugin.__init__(self,
342 config,
343 name,
344 metadata,
345 **kwargs)
346 self.outputCols = ["PSFluxPercentile{:02d}".format(percent)
347 for percent in self.config.percentiles]
349 @classmethod
350 def getExecutionOrder(cls):
351 return cls.DEFAULT_CATALOGCALCULATION
353 @catchWarnings(warns=["All-NaN slice encountered"])
354 def calculate(self,
355 diaObjects,
356 diaSources,
357 filterDiaSources,
358 filterName,
359 **kwargs):
360 """Compute the percentile fluxes of the point source flux.
362 Parameters
363 ----------
364 diaObject : `dict`
365 Summary object to store values in.
366 diaSources : `pandas.DataFrame`
367 DataFrame representing all diaSources associated with this
368 diaObject.
369 filterDiaSources : `pandas.DataFrame`
370 DataFrame representing diaSources associated with this
371 diaObject that are observed in the band pass ``filterName``.
372 filterName : `str`
373 Simple, string name of the filter for the flux being calculated.
374 **kwargs
375 Any additional keyword arguments that may be passed to the plugin.
376 """
377 pTileNames = []
378 for tilePercent in self.config.percentiles:
379 pTileName = "{}PSFluxPercentile{:02d}".format(filterName,
380 tilePercent)
381 pTileNames.append(pTileName)
382 if pTileName not in diaObjects.columns:
383 diaObjects[pTileName] = np.nan
385 def _fluxPercentiles(df):
386 pTiles = np.nanpercentile(df["psFlux"], self.config.percentiles)
387 return pd.Series(
388 dict((tileName, pTile)
389 for tileName, pTile in zip(pTileNames, pTiles)))
391 diaObjects.loc[:, pTileNames] = filterDiaSources.apply(_fluxPercentiles)
394class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
395 pass
398@register("ap_sigmaFlux")
399class SigmaDiaPsFlux(DiaObjectCalculationPlugin):
400 """Compute scatter of diaSource fluxes.
401 """
403 ConfigClass = SigmaDiaPsFluxConfig
404 # Output columns are created upon instantiation of the class.
405 outputCols = ["PSFluxSigma"]
406 plugType = "multi"
408 @classmethod
409 def getExecutionOrder(cls):
410 return cls.DEFAULT_CATALOGCALCULATION
412 def calculate(self,
413 diaObjects,
414 diaSources,
415 filterDiaSources,
416 filterName,
417 **kwargs):
418 """Compute the sigma fluxes of the point source flux.
420 Parameters
421 ----------
422 diaObject : `dict`
423 Summary object to store values in.
424 diaSources : `pandas.DataFrame`
425 DataFrame representing all diaSources associated with this
426 diaObject.
427 filterDiaSources : `pandas.DataFrame`
428 DataFrame representing diaSources associated with this
429 diaObject that are observed in the band pass ``filterName``.
430 filterName : `str`
431 Simple, string name of the filter for the flux being calculated.
432 **kwargs
433 Any additional keyword arguments that may be passed to the plugin.
434 """
435 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
436 # estimator of scatter (i.e. 'N - 1' instead of 'N').
437 diaObjects.loc[:, "{}PSFluxSigma".format(filterName)] = \
438 filterDiaSources.psFlux.std()
441class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig):
442 pass
445@register("ap_chi2Flux")
446class Chi2DiaPsFlux(DiaObjectCalculationPlugin):
447 """Compute chi2 of diaSource fluxes.
448 """
450 ConfigClass = Chi2DiaPsFluxConfig
452 # Required input Cols
453 inputCols = ["PSFluxMean"]
454 # Output columns are created upon instantiation of the class.
455 outputCols = ["PSFluxChi2"]
456 plugType = "multi"
458 @classmethod
459 def getExecutionOrder(cls):
460 return cls.FLUX_MOMENTS_CALCULATED
462 @catchWarnings(warns=["All-NaN slice encountered"])
463 def calculate(self,
464 diaObjects,
465 diaSources,
466 filterDiaSources,
467 filterName,
468 **kwargs):
469 """Compute the chi2 of the point source fluxes.
471 Parameters
472 ----------
473 diaObject : `dict`
474 Summary object to store values in.
475 diaSources : `pandas.DataFrame`
476 DataFrame representing all diaSources associated with this
477 diaObject.
478 filterDiaSources : `pandas.DataFrame`
479 DataFrame representing diaSources associated with this
480 diaObject that are observed in the band pass ``filterName``.
481 filterName : `str`
482 Simple, string name of the filter for the flux being calculated.
483 **kwargs
484 Any additional keyword arguments that may be passed to the plugin.
485 """
486 meanName = "{}PSFluxMean".format(filterName)
488 def _chi2(df):
489 delta = (df["psFlux"]
490 - diaObjects.at[df.diaObjectId.iat[0], meanName])
491 return np.nansum((delta / df["psFluxErr"]) ** 2)
493 diaObjects.loc[:, "{}PSFluxChi2".format(filterName)] = \
494 filterDiaSources.apply(_chi2)
497class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
498 pass
501@register("ap_madFlux")
502class MadDiaPsFlux(DiaObjectCalculationPlugin):
503 """Compute median absolute deviation of diaSource fluxes.
504 """
506 ConfigClass = MadDiaPsFluxConfig
508 # Required input Cols
509 # Output columns are created upon instantiation of the class.
510 outputCols = ["PSFluxMAD"]
511 plugType = "multi"
513 @classmethod
514 def getExecutionOrder(cls):
515 return cls.DEFAULT_CATALOGCALCULATION
517 @catchWarnings(warns=["All-NaN slice encountered"])
518 def calculate(self,
519 diaObjects,
520 diaSources,
521 filterDiaSources,
522 filterName,
523 **kwargs):
524 """Compute the median absolute deviation of the point source fluxes.
526 Parameters
527 ----------
528 diaObject : `dict`
529 Summary object to store values in.
530 diaSources : `pandas.DataFrame`
531 DataFrame representing all diaSources associated with this
532 diaObject.
533 filterDiaSources : `pandas.DataFrame`
534 DataFrame representing diaSources associated with this
535 diaObject that are observed in the band pass ``filterName``.
536 filterName : `str`
537 Simple, string name of the filter for the flux being calculated.
538 **kwargs
539 Any additional keyword arguments that may be passed to the plugin.
540 """
541 diaObjects.loc[:, "{}PSFluxMAD".format(filterName)] = \
542 filterDiaSources.psFlux.apply(median_absolute_deviation,
543 ignore_nan=True)
546class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
547 pass
550@register("ap_skewFlux")
551class SkewDiaPsFlux(DiaObjectCalculationPlugin):
552 """Compute the skew of diaSource fluxes.
553 """
555 ConfigClass = SkewDiaPsFluxConfig
557 # Required input Cols
558 # Output columns are created upon instantiation of the class.
559 outputCols = ["PSFluxSkew"]
560 plugType = "multi"
562 @classmethod
563 def getExecutionOrder(cls):
564 return cls.DEFAULT_CATALOGCALCULATION
566 def calculate(self,
567 diaObjects,
568 diaSources,
569 filterDiaSources,
570 filterName,
571 **kwargs):
572 """Compute the skew of the point source fluxes.
574 Parameters
575 ----------
576 diaObject : `dict`
577 Summary object to store values in.
578 diaSources : `pandas.DataFrame`
579 DataFrame representing all diaSources associated with this
580 diaObject.
581 filterDiaSources : `pandas.DataFrame`
582 DataFrame representing diaSources associated with this
583 diaObject that are observed in the band pass ``filterName``.
584 filterName : `str`
585 Simple, string name of the filter for the flux being calculated.
586 **kwargs
587 Any additional keyword arguments that may be passed to the plugin.
588 """
589 diaObjects.loc[:, "{}PSFluxSkew".format(filterName)] = \
590 filterDiaSources.psFlux.skew()
593class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
594 pass
597@register("ap_minMaxFlux")
598class MinMaxDiaPsFlux(DiaObjectCalculationPlugin):
599 """Compute min/max of diaSource fluxes.
600 """
602 ConfigClass = MinMaxDiaPsFluxConfig
604 # Required input Cols
605 # Output columns are created upon instantiation of the class.
606 outputCols = ["PSFluxMin", "PSFluxMax"]
607 plugType = "multi"
609 @classmethod
610 def getExecutionOrder(cls):
611 return cls.DEFAULT_CATALOGCALCULATION
613 def calculate(self,
614 diaObjects,
615 diaSources,
616 filterDiaSources,
617 filterName,
618 **kwargs):
619 """Compute min/max of the point source fluxes.
621 Parameters
622 ----------
623 diaObject : `dict`
624 Summary object to store values in.
625 diaSources : `pandas.DataFrame`
626 DataFrame representing all diaSources associated with this
627 diaObject.
628 filterDiaSources : `pandas.DataFrame`
629 DataFrame representing diaSources associated with this
630 diaObject that are observed in the band pass ``filterName``.
631 filterName : `str`
632 Simple, string name of the filter for the flux being calculated.
633 **kwargs
634 Any additional keyword arguments that may be passed to the plugin.
635 """
636 minName = "{}PSFluxMin".format(filterName)
637 if minName not in diaObjects.columns:
638 diaObjects[minName] = np.nan
639 maxName = "{}PSFluxMax".format(filterName)
640 if maxName not in diaObjects.columns:
641 diaObjects[maxName] = np.nan
643 diaObjects.loc[:, minName] = filterDiaSources.psFlux.min()
644 diaObjects.loc[:, maxName] = filterDiaSources.psFlux.max()
647class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
648 pass
651@register("ap_maxSlopeFlux")
652class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin):
653 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
654 """
656 ConfigClass = MinMaxDiaPsFluxConfig
658 # Required input Cols
659 # Output columns are created upon instantiation of the class.
660 outputCols = ["PSFluxMaxSlope"]
661 plugType = "multi"
663 @classmethod
664 def getExecutionOrder(cls):
665 return cls.DEFAULT_CATALOGCALCULATION
667 def calculate(self,
668 diaObjects,
669 diaSources,
670 filterDiaSources,
671 filterName,
672 **kwargs):
673 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
675 Parameters
676 ----------
677 diaObject : `dict`
678 Summary object to store values in.
679 diaSources : `pandas.DataFrame`
680 DataFrame representing all diaSources associated with this
681 diaObject.
682 filterDiaSources : `pandas.DataFrame`
683 DataFrame representing diaSources associated with this
684 diaObject that are observed in the band pass ``filterName``.
685 filterName : `str`
686 Simple, string name of the filter for the flux being calculated.
687 **kwargs
688 Any additional keyword arguments that may be passed to the plugin.
689 """
691 def _maxSlope(df):
692 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
693 np.isnan(df["midPointTai"]))]
694 if len(tmpDf) < 2:
695 return np.nan
696 times = tmpDf["midPointTai"].to_numpy()
697 timeArgs = times.argsort()
698 times = times[timeArgs]
699 fluxes = tmpDf["psFlux"].to_numpy()[timeArgs]
700 return (np.diff(fluxes) / np.diff(times)).max()
702 diaObjects.loc[:, "{}PSFluxMaxSlope".format(filterName)] = \
703 filterDiaSources.apply(_maxSlope)
706class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
707 pass
710@register("ap_meanErrFlux")
711class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin):
712 """Compute the mean of the dia source errors.
713 """
715 ConfigClass = ErrMeanDiaPsFluxConfig
717 # Required input Cols
718 # Output columns are created upon instantiation of the class.
719 outputCols = ["PSFluxErrMean"]
720 plugType = "multi"
722 @classmethod
723 def getExecutionOrder(cls):
724 return cls.DEFAULT_CATALOGCALCULATION
726 def calculate(self,
727 diaObjects,
728 diaSources,
729 filterDiaSources,
730 filterName,
731 **kwargs):
732 """Compute the mean of the dia source errors.
734 Parameters
735 ----------
736 diaObject : `dict`
737 Summary object to store values in.
738 diaSources : `pandas.DataFrame`
739 DataFrame representing all diaSources associated with this
740 diaObject.
741 filterDiaSources : `pandas.DataFrame`
742 DataFrame representing diaSources associated with this
743 diaObject that are observed in the band pass ``filterName``.
744 filterName : `str`
745 Simple, string name of the filter for the flux being calculated.
746 **kwargs
747 Any additional keyword arguments that may be passed to the plugin.
748 """
749 diaObjects.loc[:, "{}PSFluxErrMean".format(filterName)] = \
750 filterDiaSources.psFluxErr.mean()
753class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
754 pass
757@register("ap_linearFit")
758class LinearFitDiaPsFlux(DiaObjectCalculationPlugin):
759 """Compute fit a linear model to flux vs time.
760 """
762 ConfigClass = LinearFitDiaPsFluxConfig
764 # Required input Cols
765 # Output columns are created upon instantiation of the class.
766 outputCols = ["PSFluxLinearSlope", "PSFluxLinearIntercept"]
767 plugType = "multi"
769 @classmethod
770 def getExecutionOrder(cls):
771 return cls.DEFAULT_CATALOGCALCULATION
773 def calculate(self,
774 diaObjects,
775 diaSources,
776 filterDiaSources,
777 filterName,
778 **kwargs):
779 """Compute fit a linear model to flux vs time.
781 Parameters
782 ----------
783 diaObject : `dict`
784 Summary object to store values in.
785 diaSources : `pandas.DataFrame`
786 DataFrame representing all diaSources associated with this
787 diaObject.
788 filterDiaSources : `pandas.DataFrame`
789 DataFrame representing diaSources associated with this
790 diaObject that are observed in the band pass ``filterName``.
791 filterName : `str`
792 Simple, string name of the filter for the flux being calculated.
793 **kwargs
794 Any additional keyword arguments that may be passed to the plugin.
795 """
797 mName = "{}PSFluxLinearSlope".format(filterName)
798 if mName not in diaObjects.columns:
799 diaObjects[mName] = np.nan
800 bName = "{}PSFluxLinearIntercept".format(filterName)
801 if bName not in diaObjects.columns:
802 diaObjects[bName] = np.nan
804 def _linearFit(df):
805 tmpDf = df[~np.logical_or(
806 np.isnan(df["psFlux"]),
807 np.logical_or(np.isnan(df["psFluxErr"]),
808 np.isnan(df["midPointTai"])))]
809 if len(tmpDf) < 2:
810 return pd.Series({mName: np.nan, bName: np.nan})
811 fluxes = tmpDf["psFlux"].to_numpy()
812 errors = tmpDf["psFluxErr"].to_numpy()
813 times = tmpDf["midPointTai"].to_numpy()
814 A = np.array([times / errors, 1 / errors]).transpose()
815 m, b = lsq_linear(A, fluxes / errors).x
816 return pd.Series({mName: m, bName: b})
818 diaObjects.loc[:, [mName, bName]] = filterDiaSources.apply(_linearFit)
821class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
822 pass
825@register("ap_stetsonJ")
826class StetsonJDiaPsFlux(DiaObjectCalculationPlugin):
827 """Compute the StetsonJ statistic on the DIA point source fluxes.
828 """
830 ConfigClass = LinearFitDiaPsFluxConfig
832 # Required input Cols
833 inputCols = ["PSFluxMean"]
834 # Output columns are created upon instantiation of the class.
835 outputCols = ["PSFluxStetsonJ"]
836 plugType = "multi"
838 @classmethod
839 def getExecutionOrder(cls):
840 return cls.FLUX_MOMENTS_CALCULATED
842 def calculate(self,
843 diaObjects,
844 diaSources,
845 filterDiaSources,
846 filterName,
847 **kwargs):
848 """Compute the StetsonJ statistic on the DIA point source fluxes.
850 Parameters
851 ----------
852 diaObject : `dict`
853 Summary object to store values in.
854 diaSources : `pandas.DataFrame`
855 DataFrame representing all diaSources associated with this
856 diaObject.
857 filterDiaSources : `pandas.DataFrame`
858 DataFrame representing diaSources associated with this
859 diaObject that are observed in the band pass ``filterName``.
860 filterName : `str`
861 Simple, string name of the filter for the flux being calculated.
862 **kwargs
863 Any additional keyword arguments that may be passed to the plugin.
864 """
865 meanName = "{}PSFluxMean".format(filterName)
867 def _stetsonJ(df):
868 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
869 np.isnan(df["psFluxErr"]))]
870 if len(tmpDf) < 2:
871 return np.nan
872 fluxes = tmpDf["psFlux"].to_numpy()
873 errors = tmpDf["psFluxErr"].to_numpy()
875 return self._stetson_J(
876 fluxes,
877 errors,
878 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
880 diaObjects.loc[:, "{}PSFluxStetsonJ".format(filterName)] = \
881 filterDiaSources.apply(_stetsonJ)
883 def _stetson_J(self, fluxes, errors, mean=None):
884 """Compute the single band stetsonJ statistic.
886 Parameters
887 ----------
888 fluxes : `numpy.ndarray` (N,)
889 Calibrated lightcurve flux values.
890 errors : `numpy.ndarray` (N,)
891 Errors on the calibrated lightcurve fluxes.
892 mean : `float`
893 Starting mean from previous plugin.
895 Returns
896 -------
897 stetsonJ : `float`
898 stetsonJ statistic for the input fluxes and errors.
900 References
901 ----------
902 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
903 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
904 """
905 n_points = len(fluxes)
906 flux_mean = self._stetson_mean(fluxes, errors, mean)
907 delta_val = (
908 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
909 p_k = delta_val ** 2 - 1
911 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
913 def _stetson_mean(self,
914 values,
915 errors,
916 mean=None,
917 alpha=2.,
918 beta=2.,
919 n_iter=20,
920 tol=1e-6):
921 """Compute the stetson mean of the fluxes which down-weights outliers.
923 Weighted biased on an error weighted difference scaled by a constant
924 (1/``a``) and raised to the power beta. Higher betas more harshly
925 penalize outliers and ``a`` sets the number of sigma where a weighted
926 difference of 1 occurs.
928 Parameters
929 ----------
930 values : `numpy.dnarray`, (N,)
931 Input values to compute the mean of.
932 errors : `numpy.ndarray`, (N,)
933 Errors on the input values.
934 mean : `float`
935 Starting mean value or None.
936 alpha : `float`
937 Scalar down-weighting of the fractional difference. lower->more
938 clipping. (Default value is 2.)
939 beta : `float`
940 Power law slope of the used to down-weight outliers. higher->more
941 clipping. (Default value is 2.)
942 n_iter : `int`
943 Number of iterations of clipping.
944 tol : `float`
945 Fractional and absolute tolerance goal on the change in the mean
946 before exiting early. (Default value is 1e-6)
948 Returns
949 -------
950 mean : `float`
951 Weighted stetson mean result.
953 References
954 ----------
955 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
956 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
957 """
958 n_points = len(values)
959 n_factor = np.sqrt(n_points / (n_points - 1))
960 inv_var = 1 / errors ** 2
962 if mean is None:
963 mean = np.average(values, weights=inv_var)
964 for iter_idx in range(n_iter):
965 chi = np.fabs(n_factor * (values - mean) / errors)
966 tmp_mean = np.average(
967 values,
968 weights=inv_var / (1 + (chi / alpha) ** beta))
969 diff = np.fabs(tmp_mean - mean)
970 mean = tmp_mean
971 if diff / mean < tol and diff < tol:
972 break
973 return mean
976class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
977 pass
980@register("ap_meanTotFlux")
981class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin):
982 """Compute the weighted mean and mean error on the point source fluxes
983 forced photometered at the DiaSource location in the calibrated image.
985 Additionally store number of usable data points.
986 """
988 ConfigClass = WeightedMeanDiaPsFluxConfig
989 outputCols = ["TOTFluxMean", "TOTFluxMeanErr"]
990 plugType = "multi"
992 @classmethod
993 def getExecutionOrder(cls):
994 return cls.DEFAULT_CATALOGCALCULATION
996 @catchWarnings(warns=["invalid value encountered",
997 "divide by zero"])
998 def calculate(self,
999 diaObjects,
1000 diaSources,
1001 filterDiaSources,
1002 filterName,
1003 **kwargs):
1004 """Compute the weighted mean and mean error of the point source flux.
1006 Parameters
1007 ----------
1008 diaObject : `dict`
1009 Summary object to store values in.
1010 diaSources : `pandas.DataFrame`
1011 DataFrame representing all diaSources associated with this
1012 diaObject.
1013 filterDiaSources : `pandas.DataFrame`
1014 DataFrame representing diaSources associated with this
1015 diaObject that are observed in the band pass ``filterName``.
1016 filterName : `str`
1017 Simple, string name of the filter for the flux being calculated.
1018 **kwargs
1019 Any additional keyword arguments that may be passed to the plugin.
1020 """
1021 totMeanName = "{}TOTFluxMean".format(filterName)
1022 if totMeanName not in diaObjects.columns:
1023 diaObjects[totMeanName] = np.nan
1024 totErrName = "{}TOTFluxMeanErr".format(filterName)
1025 if totErrName not in diaObjects.columns:
1026 diaObjects[totErrName] = np.nan
1028 def _meanFlux(df):
1029 tmpDf = df[~np.logical_or(np.isnan(df["totFlux"]),
1030 np.isnan(df["totFluxErr"]))]
1031 tot_weight = np.nansum(1 / tmpDf["totFluxErr"] ** 2)
1032 fluxMean = np.nansum(tmpDf["totFlux"]
1033 / tmpDf["totFluxErr"] ** 2)
1034 fluxMean /= tot_weight
1035 fluxMeanErr = np.sqrt(1 / tot_weight)
1037 return pd.Series({totMeanName: fluxMean,
1038 totErrName: fluxMeanErr})
1040 diaObjects.loc[:, [totMeanName, totErrName]] = \
1041 filterDiaSources.apply(_meanFlux)
1044class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1045 pass
1048@register("ap_sigmaTotFlux")
1049class SigmaDiaTotFlux(DiaObjectCalculationPlugin):
1050 """Compute scatter of diaSource fluxes.
1051 """
1053 ConfigClass = SigmaDiaPsFluxConfig
1054 # Output columns are created upon instantiation of the class.
1055 outputCols = ["TOTFluxSigma"]
1056 plugType = "multi"
1058 @classmethod
1059 def getExecutionOrder(cls):
1060 return cls.DEFAULT_CATALOGCALCULATION
1062 def calculate(self,
1063 diaObjects,
1064 diaSources,
1065 filterDiaSources,
1066 filterName,
1067 **kwargs):
1068 """Compute the sigma fluxes of the point source flux measured on the
1069 calibrated image.
1071 Parameters
1072 ----------
1073 diaObject : `dict`
1074 Summary object to store values in.
1075 diaSources : `pandas.DataFrame`
1076 DataFrame representing all diaSources associated with this
1077 diaObject.
1078 filterDiaSources : `pandas.DataFrame`
1079 DataFrame representing diaSources associated with this
1080 diaObject that are observed in the band pass ``filterName``.
1081 filterName : `str`
1082 Simple, string name of the filter for the flux being calculated.
1083 **kwargs
1084 Any additional keyword arguments that may be passed to the plugin.
1085 """
1086 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1087 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1088 diaObjects.loc[:, "{}TOTFluxSigma".format(filterName)] = \
1089 filterDiaSources.totFlux.std()