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

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().astype(np.uint64)
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] = 0
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 if tot_weight > 0:
311 fluxMeanErr = np.sqrt(1 / tot_weight)
312 else:
313 fluxMeanErr = np.nan
314 nFluxData = len(tmpDf)
316 return pd.Series({meanName: fluxMean,
317 errName: fluxMeanErr,
318 nDataName: nFluxData},
319 dtype="object")
321 diaObjects.loc[:, [meanName, errName, nDataName]] = \
322 filterDiaSources.apply(_weightedMean)
325class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
326 percentiles = pexConfig.ListField(
327 dtype=int,
328 default=[5, 25, 50, 75, 95],
329 doc="Percentiles to calculate to compute values for. Should be "
330 "integer values."
331 )
334@register("ap_percentileFlux")
335class PercentileDiaPsFlux(DiaObjectCalculationPlugin):
336 """Compute percentiles of diaSource fluxes.
337 """
339 ConfigClass = PercentileDiaPsFluxConfig
340 # Output columns are created upon instantiation of the class.
341 outputCols = []
342 plugType = "multi"
344 def __init__(self, config, name, metadata, **kwargs):
345 DiaObjectCalculationPlugin.__init__(self,
346 config,
347 name,
348 metadata,
349 **kwargs)
350 self.outputCols = ["PSFluxPercentile{:02d}".format(percent)
351 for percent in self.config.percentiles]
353 @classmethod
354 def getExecutionOrder(cls):
355 return cls.DEFAULT_CATALOGCALCULATION
357 @catchWarnings(warns=["All-NaN slice encountered"])
358 def calculate(self,
359 diaObjects,
360 diaSources,
361 filterDiaSources,
362 filterName,
363 **kwargs):
364 """Compute the percentile fluxes of the point source flux.
366 Parameters
367 ----------
368 diaObject : `dict`
369 Summary object to store values in.
370 diaSources : `pandas.DataFrame`
371 DataFrame representing all diaSources associated with this
372 diaObject.
373 filterDiaSources : `pandas.DataFrame`
374 DataFrame representing diaSources associated with this
375 diaObject that are observed in the band pass ``filterName``.
376 filterName : `str`
377 Simple, string name of the filter for the flux being calculated.
378 **kwargs
379 Any additional keyword arguments that may be passed to the plugin.
380 """
381 pTileNames = []
382 for tilePercent in self.config.percentiles:
383 pTileName = "{}PSFluxPercentile{:02d}".format(filterName,
384 tilePercent)
385 pTileNames.append(pTileName)
386 if pTileName not in diaObjects.columns:
387 diaObjects[pTileName] = np.nan
389 def _fluxPercentiles(df):
390 pTiles = np.nanpercentile(df["psFlux"], self.config.percentiles)
391 return pd.Series(
392 dict((tileName, pTile)
393 for tileName, pTile in zip(pTileNames, pTiles)))
395 diaObjects.loc[:, pTileNames] = filterDiaSources.apply(_fluxPercentiles)
398class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
399 pass
402@register("ap_sigmaFlux")
403class SigmaDiaPsFlux(DiaObjectCalculationPlugin):
404 """Compute scatter of diaSource fluxes.
405 """
407 ConfigClass = SigmaDiaPsFluxConfig
408 # Output columns are created upon instantiation of the class.
409 outputCols = ["PSFluxSigma"]
410 plugType = "multi"
412 @classmethod
413 def getExecutionOrder(cls):
414 return cls.DEFAULT_CATALOGCALCULATION
416 def calculate(self,
417 diaObjects,
418 diaSources,
419 filterDiaSources,
420 filterName,
421 **kwargs):
422 """Compute the sigma fluxes of the point source flux.
424 Parameters
425 ----------
426 diaObject : `dict`
427 Summary object to store values in.
428 diaSources : `pandas.DataFrame`
429 DataFrame representing all diaSources associated with this
430 diaObject.
431 filterDiaSources : `pandas.DataFrame`
432 DataFrame representing diaSources associated with this
433 diaObject that are observed in the band pass ``filterName``.
434 filterName : `str`
435 Simple, string name of the filter for the flux being calculated.
436 **kwargs
437 Any additional keyword arguments that may be passed to the plugin.
438 """
439 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
440 # estimator of scatter (i.e. 'N - 1' instead of 'N').
441 diaObjects.loc[:, "{}PSFluxSigma".format(filterName)] = \
442 filterDiaSources.psFlux.std()
445class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig):
446 pass
449@register("ap_chi2Flux")
450class Chi2DiaPsFlux(DiaObjectCalculationPlugin):
451 """Compute chi2 of diaSource fluxes.
452 """
454 ConfigClass = Chi2DiaPsFluxConfig
456 # Required input Cols
457 inputCols = ["PSFluxMean"]
458 # Output columns are created upon instantiation of the class.
459 outputCols = ["PSFluxChi2"]
460 plugType = "multi"
462 @classmethod
463 def getExecutionOrder(cls):
464 return cls.FLUX_MOMENTS_CALCULATED
466 @catchWarnings(warns=["All-NaN slice encountered"])
467 def calculate(self,
468 diaObjects,
469 diaSources,
470 filterDiaSources,
471 filterName,
472 **kwargs):
473 """Compute the chi2 of the point source fluxes.
475 Parameters
476 ----------
477 diaObject : `dict`
478 Summary object to store values in.
479 diaSources : `pandas.DataFrame`
480 DataFrame representing all diaSources associated with this
481 diaObject.
482 filterDiaSources : `pandas.DataFrame`
483 DataFrame representing diaSources associated with this
484 diaObject that are observed in the band pass ``filterName``.
485 filterName : `str`
486 Simple, string name of the filter for the flux being calculated.
487 **kwargs
488 Any additional keyword arguments that may be passed to the plugin.
489 """
490 meanName = "{}PSFluxMean".format(filterName)
492 def _chi2(df):
493 delta = (df["psFlux"]
494 - diaObjects.at[df.diaObjectId.iat[0], meanName])
495 return np.nansum((delta / df["psFluxErr"]) ** 2)
497 diaObjects.loc[:, "{}PSFluxChi2".format(filterName)] = \
498 filterDiaSources.apply(_chi2)
501class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
502 pass
505@register("ap_madFlux")
506class MadDiaPsFlux(DiaObjectCalculationPlugin):
507 """Compute median absolute deviation of diaSource fluxes.
508 """
510 ConfigClass = MadDiaPsFluxConfig
512 # Required input Cols
513 # Output columns are created upon instantiation of the class.
514 outputCols = ["PSFluxMAD"]
515 plugType = "multi"
517 @classmethod
518 def getExecutionOrder(cls):
519 return cls.DEFAULT_CATALOGCALCULATION
521 @catchWarnings(warns=["All-NaN slice encountered"])
522 def calculate(self,
523 diaObjects,
524 diaSources,
525 filterDiaSources,
526 filterName,
527 **kwargs):
528 """Compute the median absolute deviation of the point source fluxes.
530 Parameters
531 ----------
532 diaObject : `dict`
533 Summary object to store values in.
534 diaSources : `pandas.DataFrame`
535 DataFrame representing all diaSources associated with this
536 diaObject.
537 filterDiaSources : `pandas.DataFrame`
538 DataFrame representing diaSources associated with this
539 diaObject that are observed in the band pass ``filterName``.
540 filterName : `str`
541 Simple, string name of the filter for the flux being calculated.
542 **kwargs
543 Any additional keyword arguments that may be passed to the plugin.
544 """
545 diaObjects.loc[:, "{}PSFluxMAD".format(filterName)] = \
546 filterDiaSources.psFlux.apply(median_absolute_deviation,
547 ignore_nan=True)
550class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
551 pass
554@register("ap_skewFlux")
555class SkewDiaPsFlux(DiaObjectCalculationPlugin):
556 """Compute the skew of diaSource fluxes.
557 """
559 ConfigClass = SkewDiaPsFluxConfig
561 # Required input Cols
562 # Output columns are created upon instantiation of the class.
563 outputCols = ["PSFluxSkew"]
564 plugType = "multi"
566 @classmethod
567 def getExecutionOrder(cls):
568 return cls.DEFAULT_CATALOGCALCULATION
570 def calculate(self,
571 diaObjects,
572 diaSources,
573 filterDiaSources,
574 filterName,
575 **kwargs):
576 """Compute the skew of the point source fluxes.
578 Parameters
579 ----------
580 diaObject : `dict`
581 Summary object to store values in.
582 diaSources : `pandas.DataFrame`
583 DataFrame representing all diaSources associated with this
584 diaObject.
585 filterDiaSources : `pandas.DataFrame`
586 DataFrame representing diaSources associated with this
587 diaObject that are observed in the band pass ``filterName``.
588 filterName : `str`
589 Simple, string name of the filter for the flux being calculated.
590 **kwargs
591 Any additional keyword arguments that may be passed to the plugin.
592 """
593 diaObjects.loc[:, "{}PSFluxSkew".format(filterName)] = \
594 filterDiaSources.psFlux.skew()
597class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
598 pass
601@register("ap_minMaxFlux")
602class MinMaxDiaPsFlux(DiaObjectCalculationPlugin):
603 """Compute min/max of diaSource fluxes.
604 """
606 ConfigClass = MinMaxDiaPsFluxConfig
608 # Required input Cols
609 # Output columns are created upon instantiation of the class.
610 outputCols = ["PSFluxMin", "PSFluxMax"]
611 plugType = "multi"
613 @classmethod
614 def getExecutionOrder(cls):
615 return cls.DEFAULT_CATALOGCALCULATION
617 def calculate(self,
618 diaObjects,
619 diaSources,
620 filterDiaSources,
621 filterName,
622 **kwargs):
623 """Compute min/max of the point source fluxes.
625 Parameters
626 ----------
627 diaObject : `dict`
628 Summary object to store values in.
629 diaSources : `pandas.DataFrame`
630 DataFrame representing all diaSources associated with this
631 diaObject.
632 filterDiaSources : `pandas.DataFrame`
633 DataFrame representing diaSources associated with this
634 diaObject that are observed in the band pass ``filterName``.
635 filterName : `str`
636 Simple, string name of the filter for the flux being calculated.
637 **kwargs
638 Any additional keyword arguments that may be passed to the plugin.
639 """
640 minName = "{}PSFluxMin".format(filterName)
641 if minName not in diaObjects.columns:
642 diaObjects[minName] = np.nan
643 maxName = "{}PSFluxMax".format(filterName)
644 if maxName not in diaObjects.columns:
645 diaObjects[maxName] = np.nan
647 diaObjects.loc[:, minName] = filterDiaSources.psFlux.min()
648 diaObjects.loc[:, maxName] = filterDiaSources.psFlux.max()
651class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
652 pass
655@register("ap_maxSlopeFlux")
656class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin):
657 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
658 """
660 ConfigClass = MinMaxDiaPsFluxConfig
662 # Required input Cols
663 # Output columns are created upon instantiation of the class.
664 outputCols = ["PSFluxMaxSlope"]
665 plugType = "multi"
667 @classmethod
668 def getExecutionOrder(cls):
669 return cls.DEFAULT_CATALOGCALCULATION
671 def calculate(self,
672 diaObjects,
673 diaSources,
674 filterDiaSources,
675 filterName,
676 **kwargs):
677 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
679 Parameters
680 ----------
681 diaObject : `dict`
682 Summary object to store values in.
683 diaSources : `pandas.DataFrame`
684 DataFrame representing all diaSources associated with this
685 diaObject.
686 filterDiaSources : `pandas.DataFrame`
687 DataFrame representing diaSources associated with this
688 diaObject that are observed in the band pass ``filterName``.
689 filterName : `str`
690 Simple, string name of the filter for the flux being calculated.
691 **kwargs
692 Any additional keyword arguments that may be passed to the plugin.
693 """
695 def _maxSlope(df):
696 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
697 np.isnan(df["midPointTai"]))]
698 if len(tmpDf) < 2:
699 return np.nan
700 times = tmpDf["midPointTai"].to_numpy()
701 timeArgs = times.argsort()
702 times = times[timeArgs]
703 fluxes = tmpDf["psFlux"].to_numpy()[timeArgs]
704 return (np.diff(fluxes) / np.diff(times)).max()
706 diaObjects.loc[:, "{}PSFluxMaxSlope".format(filterName)] = \
707 filterDiaSources.apply(_maxSlope)
710class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
711 pass
714@register("ap_meanErrFlux")
715class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin):
716 """Compute the mean of the dia source errors.
717 """
719 ConfigClass = ErrMeanDiaPsFluxConfig
721 # Required input Cols
722 # Output columns are created upon instantiation of the class.
723 outputCols = ["PSFluxErrMean"]
724 plugType = "multi"
726 @classmethod
727 def getExecutionOrder(cls):
728 return cls.DEFAULT_CATALOGCALCULATION
730 def calculate(self,
731 diaObjects,
732 diaSources,
733 filterDiaSources,
734 filterName,
735 **kwargs):
736 """Compute the mean of the dia source errors.
738 Parameters
739 ----------
740 diaObject : `dict`
741 Summary object to store values in.
742 diaSources : `pandas.DataFrame`
743 DataFrame representing all diaSources associated with this
744 diaObject.
745 filterDiaSources : `pandas.DataFrame`
746 DataFrame representing diaSources associated with this
747 diaObject that are observed in the band pass ``filterName``.
748 filterName : `str`
749 Simple, string name of the filter for the flux being calculated.
750 **kwargs
751 Any additional keyword arguments that may be passed to the plugin.
752 """
753 diaObjects.loc[:, "{}PSFluxErrMean".format(filterName)] = \
754 filterDiaSources.psFluxErr.mean()
757class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
758 pass
761@register("ap_linearFit")
762class LinearFitDiaPsFlux(DiaObjectCalculationPlugin):
763 """Compute fit a linear model to flux vs time.
764 """
766 ConfigClass = LinearFitDiaPsFluxConfig
768 # Required input Cols
769 # Output columns are created upon instantiation of the class.
770 outputCols = ["PSFluxLinearSlope", "PSFluxLinearIntercept"]
771 plugType = "multi"
773 @classmethod
774 def getExecutionOrder(cls):
775 return cls.DEFAULT_CATALOGCALCULATION
777 def calculate(self,
778 diaObjects,
779 diaSources,
780 filterDiaSources,
781 filterName,
782 **kwargs):
783 """Compute fit a linear model to flux vs time.
785 Parameters
786 ----------
787 diaObject : `dict`
788 Summary object to store values in.
789 diaSources : `pandas.DataFrame`
790 DataFrame representing all diaSources associated with this
791 diaObject.
792 filterDiaSources : `pandas.DataFrame`
793 DataFrame representing diaSources associated with this
794 diaObject that are observed in the band pass ``filterName``.
795 filterName : `str`
796 Simple, string name of the filter for the flux being calculated.
797 **kwargs
798 Any additional keyword arguments that may be passed to the plugin.
799 """
801 mName = "{}PSFluxLinearSlope".format(filterName)
802 if mName not in diaObjects.columns:
803 diaObjects[mName] = np.nan
804 bName = "{}PSFluxLinearIntercept".format(filterName)
805 if bName not in diaObjects.columns:
806 diaObjects[bName] = np.nan
808 def _linearFit(df):
809 tmpDf = df[~np.logical_or(
810 np.isnan(df["psFlux"]),
811 np.logical_or(np.isnan(df["psFluxErr"]),
812 np.isnan(df["midPointTai"])))]
813 if len(tmpDf) < 2:
814 return pd.Series({mName: np.nan, bName: np.nan})
815 fluxes = tmpDf["psFlux"].to_numpy()
816 errors = tmpDf["psFluxErr"].to_numpy()
817 times = tmpDf["midPointTai"].to_numpy()
818 A = np.array([times / errors, 1 / errors]).transpose()
819 m, b = lsq_linear(A, fluxes / errors).x
820 return pd.Series({mName: m, bName: b})
822 diaObjects.loc[:, [mName, bName]] = filterDiaSources.apply(_linearFit)
825class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
826 pass
829@register("ap_stetsonJ")
830class StetsonJDiaPsFlux(DiaObjectCalculationPlugin):
831 """Compute the StetsonJ statistic on the DIA point source fluxes.
832 """
834 ConfigClass = LinearFitDiaPsFluxConfig
836 # Required input Cols
837 inputCols = ["PSFluxMean"]
838 # Output columns are created upon instantiation of the class.
839 outputCols = ["PSFluxStetsonJ"]
840 plugType = "multi"
842 @classmethod
843 def getExecutionOrder(cls):
844 return cls.FLUX_MOMENTS_CALCULATED
846 def calculate(self,
847 diaObjects,
848 diaSources,
849 filterDiaSources,
850 filterName,
851 **kwargs):
852 """Compute the StetsonJ statistic on the DIA point source fluxes.
854 Parameters
855 ----------
856 diaObject : `dict`
857 Summary object to store values in.
858 diaSources : `pandas.DataFrame`
859 DataFrame representing all diaSources associated with this
860 diaObject.
861 filterDiaSources : `pandas.DataFrame`
862 DataFrame representing diaSources associated with this
863 diaObject that are observed in the band pass ``filterName``.
864 filterName : `str`
865 Simple, string name of the filter for the flux being calculated.
866 **kwargs
867 Any additional keyword arguments that may be passed to the plugin.
868 """
869 meanName = "{}PSFluxMean".format(filterName)
871 def _stetsonJ(df):
872 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
873 np.isnan(df["psFluxErr"]))]
874 if len(tmpDf) < 2:
875 return np.nan
876 fluxes = tmpDf["psFlux"].to_numpy()
877 errors = tmpDf["psFluxErr"].to_numpy()
879 return self._stetson_J(
880 fluxes,
881 errors,
882 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
884 diaObjects.loc[:, "{}PSFluxStetsonJ".format(filterName)] = \
885 filterDiaSources.apply(_stetsonJ)
887 def _stetson_J(self, fluxes, errors, mean=None):
888 """Compute the single band stetsonJ statistic.
890 Parameters
891 ----------
892 fluxes : `numpy.ndarray` (N,)
893 Calibrated lightcurve flux values.
894 errors : `numpy.ndarray` (N,)
895 Errors on the calibrated lightcurve fluxes.
896 mean : `float`
897 Starting mean from previous plugin.
899 Returns
900 -------
901 stetsonJ : `float`
902 stetsonJ statistic for the input fluxes and errors.
904 References
905 ----------
906 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
907 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
908 """
909 n_points = len(fluxes)
910 flux_mean = self._stetson_mean(fluxes, errors, mean)
911 delta_val = (
912 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
913 p_k = delta_val ** 2 - 1
915 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
917 def _stetson_mean(self,
918 values,
919 errors,
920 mean=None,
921 alpha=2.,
922 beta=2.,
923 n_iter=20,
924 tol=1e-6):
925 """Compute the stetson mean of the fluxes which down-weights outliers.
927 Weighted biased on an error weighted difference scaled by a constant
928 (1/``a``) and raised to the power beta. Higher betas more harshly
929 penalize outliers and ``a`` sets the number of sigma where a weighted
930 difference of 1 occurs.
932 Parameters
933 ----------
934 values : `numpy.dnarray`, (N,)
935 Input values to compute the mean of.
936 errors : `numpy.ndarray`, (N,)
937 Errors on the input values.
938 mean : `float`
939 Starting mean value or None.
940 alpha : `float`
941 Scalar down-weighting of the fractional difference. lower->more
942 clipping. (Default value is 2.)
943 beta : `float`
944 Power law slope of the used to down-weight outliers. higher->more
945 clipping. (Default value is 2.)
946 n_iter : `int`
947 Number of iterations of clipping.
948 tol : `float`
949 Fractional and absolute tolerance goal on the change in the mean
950 before exiting early. (Default value is 1e-6)
952 Returns
953 -------
954 mean : `float`
955 Weighted stetson mean result.
957 References
958 ----------
959 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
960 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
961 """
962 n_points = len(values)
963 n_factor = np.sqrt(n_points / (n_points - 1))
964 inv_var = 1 / errors ** 2
966 if mean is None:
967 mean = np.average(values, weights=inv_var)
968 for iter_idx in range(n_iter):
969 chi = np.fabs(n_factor * (values - mean) / errors)
970 tmp_mean = np.average(
971 values,
972 weights=inv_var / (1 + (chi / alpha) ** beta))
973 diff = np.fabs(tmp_mean - mean)
974 mean = tmp_mean
975 if diff / mean < tol and diff < tol:
976 break
977 return mean
980class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
981 pass
984@register("ap_meanTotFlux")
985class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin):
986 """Compute the weighted mean and mean error on the point source fluxes
987 forced photometered at the DiaSource location in the calibrated image.
989 Additionally store number of usable data points.
990 """
992 ConfigClass = WeightedMeanDiaPsFluxConfig
993 outputCols = ["TOTFluxMean", "TOTFluxMeanErr"]
994 plugType = "multi"
996 @classmethod
997 def getExecutionOrder(cls):
998 return cls.DEFAULT_CATALOGCALCULATION
1000 @catchWarnings(warns=["invalid value encountered",
1001 "divide by zero"])
1002 def calculate(self,
1003 diaObjects,
1004 diaSources,
1005 filterDiaSources,
1006 filterName,
1007 **kwargs):
1008 """Compute the weighted mean and mean error of the point source flux.
1010 Parameters
1011 ----------
1012 diaObject : `dict`
1013 Summary object to store values in.
1014 diaSources : `pandas.DataFrame`
1015 DataFrame representing all diaSources associated with this
1016 diaObject.
1017 filterDiaSources : `pandas.DataFrame`
1018 DataFrame representing diaSources associated with this
1019 diaObject that are observed in the band pass ``filterName``.
1020 filterName : `str`
1021 Simple, string name of the filter for the flux being calculated.
1022 **kwargs
1023 Any additional keyword arguments that may be passed to the plugin.
1024 """
1025 totMeanName = "{}TOTFluxMean".format(filterName)
1026 if totMeanName not in diaObjects.columns:
1027 diaObjects[totMeanName] = np.nan
1028 totErrName = "{}TOTFluxMeanErr".format(filterName)
1029 if totErrName not in diaObjects.columns:
1030 diaObjects[totErrName] = np.nan
1032 def _meanFlux(df):
1033 tmpDf = df[~np.logical_or(np.isnan(df["totFlux"]),
1034 np.isnan(df["totFluxErr"]))]
1035 tot_weight = np.nansum(1 / tmpDf["totFluxErr"] ** 2)
1036 fluxMean = np.nansum(tmpDf["totFlux"]
1037 / tmpDf["totFluxErr"] ** 2)
1038 fluxMean /= tot_weight
1039 fluxMeanErr = np.sqrt(1 / tot_weight)
1041 return pd.Series({totMeanName: fluxMean,
1042 totErrName: fluxMeanErr})
1044 diaObjects.loc[:, [totMeanName, totErrName]] = \
1045 filterDiaSources.apply(_meanFlux)
1048class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1049 pass
1052@register("ap_sigmaTotFlux")
1053class SigmaDiaTotFlux(DiaObjectCalculationPlugin):
1054 """Compute scatter of diaSource fluxes.
1055 """
1057 ConfigClass = SigmaDiaPsFluxConfig
1058 # Output columns are created upon instantiation of the class.
1059 outputCols = ["TOTFluxSigma"]
1060 plugType = "multi"
1062 @classmethod
1063 def getExecutionOrder(cls):
1064 return cls.DEFAULT_CATALOGCALCULATION
1066 def calculate(self,
1067 diaObjects,
1068 diaSources,
1069 filterDiaSources,
1070 filterName,
1071 **kwargs):
1072 """Compute the sigma fluxes of the point source flux measured on the
1073 calibrated image.
1075 Parameters
1076 ----------
1077 diaObject : `dict`
1078 Summary object to store values in.
1079 diaSources : `pandas.DataFrame`
1080 DataFrame representing all diaSources associated with this
1081 diaObject.
1082 filterDiaSources : `pandas.DataFrame`
1083 DataFrame representing diaSources associated with this
1084 diaObject that are observed in the band pass ``filterName``.
1085 filterName : `str`
1086 Simple, string name of the filter for the flux being calculated.
1087 **kwargs
1088 Any additional keyword arguments that may be passed to the plugin.
1089 """
1090 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1091 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1092 diaObjects.loc[:, "{}TOTFluxSigma".format(filterName)] = \
1093 filterDiaSources.totFlux.std()