Coverage for python/lsst/meas/base/diaCalculationPlugins.py: 55%
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# 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 .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"]
96 needsFilter = False
98 @classmethod
99 def getExecutionOrder(cls):
100 return cls.DEFAULT_CATALOGCALCULATION
102 def calculate(self, diaObjects, diaSources, **kwargs):
103 """Compute the mean ra/dec position of the diaObject given the
104 diaSource locations.
106 Parameters
107 ----------
108 diaObjects : `pandas.DataFrame`
109 Summary objects to store values in.
110 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
111 Catalog of DiaSources summarized by this DiaObject.
112 **kwargs
113 Any additional keyword arguments that may be passed to the plugin.
114 """
115 for outCol in self.outputCols:
116 if outCol not in diaObjects.columns:
117 diaObjects[outCol] = np.nan
119 def _computeMeanPos(df):
120 aveCoord = geom.averageSpherePoint(
121 list(geom.SpherePoint(src["ra"], src["decl"], geom.degrees)
122 for idx, src in df.iterrows()))
123 ra = aveCoord.getRa().asDegrees()
124 decl = aveCoord.getDec().asDegrees()
125 if np.isnan(ra) or np.isnan(decl):
126 radecTai = np.nan
127 else:
128 radecTai = df["midPointTai"].max()
130 return pd.Series({"ra": aveCoord.getRa().asDegrees(),
131 "decl": aveCoord.getDec().asDegrees(),
132 "radecTai": radecTai})
134 ans = diaSources.apply(_computeMeanPos)
135 diaObjects.loc[:, ["ra", "decl", "radecTai"]] = ans
138class HTMIndexDiaPositionConfig(DiaObjectCalculationPluginConfig):
140 htmLevel = pexConfig.Field(
141 dtype=int,
142 doc="Level of the HTM pixelization.",
143 default=20,
144 )
147@register("ap_HTMIndex")
148class HTMIndexDiaPosition(DiaObjectCalculationPlugin):
149 """Compute the mean position of a DiaObject given a set of DiaSources.
150 """
151 ConfigClass = HTMIndexDiaPositionConfig
153 plugType = 'single'
155 inputCols = ["ra", "decl"]
156 outputCols = ["pixelId"]
157 needsFilter = False
159 def __init__(self, config, name, metadata):
160 DiaObjectCalculationPlugin.__init__(self, config, name, metadata)
161 self.pixelator = sphgeom.HtmPixelization(self.config.htmLevel)
163 @classmethod
164 def getExecutionOrder(cls):
165 return cls.FLUX_MOMENTS_CALCULATED
167 def calculate(self, diaObjects, diaObjectId, **kwargs):
168 """Compute the mean position of a DiaObject given a set of DiaSources
170 Parameters
171 ----------
172 diaObjects : `pandas.dataFrame`
173 Summary objects to store values in and read ra/decl from.
174 diaObjectId : `int`
175 Id of the diaObject to update.
176 **kwargs
177 Any additional keyword arguments that may be passed to the plugin.
178 """
179 sphPoint = geom.SpherePoint(
180 diaObjects.at[diaObjectId, "ra"] * geom.degrees,
181 diaObjects.at[diaObjectId, "decl"] * geom.degrees)
182 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index(
183 sphPoint.getVector())
186class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig):
187 pass
190@register("ap_nDiaSources")
191class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin):
192 """Compute the total number of DiaSources associated with this DiaObject.
193 """
195 ConfigClass = NumDiaSourcesDiaPluginConfig
196 outputCols = ["nDiaSources"]
197 plugType = "multi"
198 needsFilter = False
200 @classmethod
201 def getExecutionOrder(cls):
202 return cls.DEFAULT_CATALOGCALCULATION
204 def calculate(self, diaObjects, diaSources, **kwargs):
205 """Compute the total number of DiaSources associated with this DiaObject.
207 Parameters
208 ----------
209 diaObject : `dict`
210 Summary object to store values in and read ra/decl from.
211 **kwargs
212 Any additional keyword arguments that may be passed to the plugin.
213 """
214 diaObjects.loc[:, "nDiaSources"] = diaSources.diaObjectId.count()
217class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig):
218 pass
221@register("ap_diaObjectFlag")
222class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin):
223 """Find if any DiaSource is flagged.
225 Set the DiaObject flag if any DiaSource is flagged.
226 """
228 ConfigClass = NumDiaSourcesDiaPluginConfig
229 outputCols = ["flags"]
230 plugType = "multi"
231 needsFilter = False
233 @classmethod
234 def getExecutionOrder(cls):
235 return cls.DEFAULT_CATALOGCALCULATION
237 def calculate(self, diaObjects, diaSources, **kwargs):
238 """Find if any DiaSource is flagged.
240 Set the DiaObject flag if any DiaSource is flagged.
242 Parameters
243 ----------
244 diaObject : `dict`
245 Summary object to store values in and read ra/decl from.
246 **kwargs
247 Any additional keyword arguments that may be passed to the plugin.
248 """
249 diaObjects.loc[:, "flags"] = diaSources.flags.any().astype(np.uint64)
252class WeightedMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
253 pass
256@register("ap_meanFlux")
257class WeightedMeanDiaPsFlux(DiaObjectCalculationPlugin):
258 """Compute the weighted mean and mean error on the point source fluxes
259 of the DiaSource measured on the difference image.
261 Additionally store number of usable data points.
262 """
264 ConfigClass = WeightedMeanDiaPsFluxConfig
265 outputCols = ["PSFluxMean", "PSFluxMeanErr", "PSFluxNdata"]
266 plugType = "multi"
267 needsFilter = True
269 @classmethod
270 def getExecutionOrder(cls):
271 return cls.DEFAULT_CATALOGCALCULATION
273 @catchWarnings(warns=["invalid value encountered",
274 "divide by zero"])
275 def calculate(self,
276 diaObjects,
277 diaSources,
278 filterDiaSources,
279 filterName,
280 **kwargs):
281 """Compute the weighted mean and mean error of the point source flux.
283 Parameters
284 ----------
285 diaObject : `dict`
286 Summary object to store values in.
287 diaSources : `pandas.DataFrame`
288 DataFrame representing all diaSources associated with this
289 diaObject.
290 filterDiaSources : `pandas.DataFrame`
291 DataFrame representing diaSources associated with this
292 diaObject that are observed in the band pass ``filterName``.
293 filterName : `str`
294 Simple, string name of the filter for the flux being calculated.
295 **kwargs
296 Any additional keyword arguments that may be passed to the plugin.
297 """
298 meanName = "{}PSFluxMean".format(filterName)
299 errName = "{}PSFluxMeanErr".format(filterName)
300 nDataName = "{}PSFluxNdata".format(filterName)
301 if meanName not in diaObjects.columns:
302 diaObjects[meanName] = np.nan
303 if errName not in diaObjects.columns:
304 diaObjects[errName] = np.nan
305 if nDataName not in diaObjects.columns:
306 diaObjects[nDataName] = 0
308 def _weightedMean(df):
309 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
310 np.isnan(df["psFluxErr"]))]
311 tot_weight = np.nansum(1 / tmpDf["psFluxErr"] ** 2)
312 fluxMean = np.nansum(tmpDf["psFlux"]
313 / tmpDf["psFluxErr"] ** 2)
314 fluxMean /= tot_weight
315 if tot_weight > 0:
316 fluxMeanErr = np.sqrt(1 / tot_weight)
317 else:
318 fluxMeanErr = np.nan
319 nFluxData = len(tmpDf)
321 return pd.Series({meanName: fluxMean,
322 errName: fluxMeanErr,
323 nDataName: nFluxData},
324 dtype="object")
326 diaObjects.loc[:, [meanName, errName, nDataName]] = \
327 filterDiaSources.apply(_weightedMean)
330class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
331 percentiles = pexConfig.ListField(
332 dtype=int,
333 default=[5, 25, 50, 75, 95],
334 doc="Percentiles to calculate to compute values for. Should be "
335 "integer values."
336 )
339@register("ap_percentileFlux")
340class PercentileDiaPsFlux(DiaObjectCalculationPlugin):
341 """Compute percentiles of diaSource fluxes.
342 """
344 ConfigClass = PercentileDiaPsFluxConfig
345 # Output columns are created upon instantiation of the class.
346 outputCols = []
347 plugType = "multi"
348 needsFilter = True
350 def __init__(self, config, name, metadata, **kwargs):
351 DiaObjectCalculationPlugin.__init__(self,
352 config,
353 name,
354 metadata,
355 **kwargs)
356 self.outputCols = ["PSFluxPercentile{:02d}".format(percent)
357 for percent in self.config.percentiles]
359 @classmethod
360 def getExecutionOrder(cls):
361 return cls.DEFAULT_CATALOGCALCULATION
363 @catchWarnings(warns=["All-NaN slice encountered"])
364 def calculate(self,
365 diaObjects,
366 diaSources,
367 filterDiaSources,
368 filterName,
369 **kwargs):
370 """Compute the percentile fluxes of the point source flux.
372 Parameters
373 ----------
374 diaObject : `dict`
375 Summary object to store values in.
376 diaSources : `pandas.DataFrame`
377 DataFrame representing all diaSources associated with this
378 diaObject.
379 filterDiaSources : `pandas.DataFrame`
380 DataFrame representing diaSources associated with this
381 diaObject that are observed in the band pass ``filterName``.
382 filterName : `str`
383 Simple, string name of the filter for the flux being calculated.
384 **kwargs
385 Any additional keyword arguments that may be passed to the plugin.
386 """
387 pTileNames = []
388 for tilePercent in self.config.percentiles:
389 pTileName = "{}PSFluxPercentile{:02d}".format(filterName,
390 tilePercent)
391 pTileNames.append(pTileName)
392 if pTileName not in diaObjects.columns:
393 diaObjects[pTileName] = np.nan
395 def _fluxPercentiles(df):
396 pTiles = np.nanpercentile(df["psFlux"], self.config.percentiles)
397 return pd.Series(
398 dict((tileName, pTile)
399 for tileName, pTile in zip(pTileNames, pTiles)))
401 diaObjects.loc[:, pTileNames] = filterDiaSources.apply(_fluxPercentiles)
404class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
405 pass
408@register("ap_sigmaFlux")
409class SigmaDiaPsFlux(DiaObjectCalculationPlugin):
410 """Compute scatter of diaSource fluxes.
411 """
413 ConfigClass = SigmaDiaPsFluxConfig
414 # Output columns are created upon instantiation of the class.
415 outputCols = ["PSFluxSigma"]
416 plugType = "multi"
417 needsFilter = True
419 @classmethod
420 def getExecutionOrder(cls):
421 return cls.DEFAULT_CATALOGCALCULATION
423 def calculate(self,
424 diaObjects,
425 diaSources,
426 filterDiaSources,
427 filterName,
428 **kwargs):
429 """Compute the sigma fluxes of the point source flux.
431 Parameters
432 ----------
433 diaObject : `dict`
434 Summary object to store values in.
435 diaSources : `pandas.DataFrame`
436 DataFrame representing all diaSources associated with this
437 diaObject.
438 filterDiaSources : `pandas.DataFrame`
439 DataFrame representing diaSources associated with this
440 diaObject that are observed in the band pass ``filterName``.
441 filterName : `str`
442 Simple, string name of the filter for the flux being calculated.
443 **kwargs
444 Any additional keyword arguments that may be passed to the plugin.
445 """
446 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
447 # estimator of scatter (i.e. 'N - 1' instead of 'N').
448 diaObjects.loc[:, "{}PSFluxSigma".format(filterName)] = \
449 filterDiaSources.psFlux.std()
452class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig):
453 pass
456@register("ap_chi2Flux")
457class Chi2DiaPsFlux(DiaObjectCalculationPlugin):
458 """Compute chi2 of diaSource fluxes.
459 """
461 ConfigClass = Chi2DiaPsFluxConfig
463 # Required input Cols
464 inputCols = ["PSFluxMean"]
465 # Output columns are created upon instantiation of the class.
466 outputCols = ["PSFluxChi2"]
467 plugType = "multi"
468 needsFilter = True
470 @classmethod
471 def getExecutionOrder(cls):
472 return cls.FLUX_MOMENTS_CALCULATED
474 @catchWarnings(warns=["All-NaN slice encountered"])
475 def calculate(self,
476 diaObjects,
477 diaSources,
478 filterDiaSources,
479 filterName,
480 **kwargs):
481 """Compute the chi2 of the point source fluxes.
483 Parameters
484 ----------
485 diaObject : `dict`
486 Summary object to store values in.
487 diaSources : `pandas.DataFrame`
488 DataFrame representing all diaSources associated with this
489 diaObject.
490 filterDiaSources : `pandas.DataFrame`
491 DataFrame representing diaSources associated with this
492 diaObject that are observed in the band pass ``filterName``.
493 filterName : `str`
494 Simple, string name of the filter for the flux being calculated.
495 **kwargs
496 Any additional keyword arguments that may be passed to the plugin.
497 """
498 meanName = "{}PSFluxMean".format(filterName)
500 def _chi2(df):
501 delta = (df["psFlux"]
502 - diaObjects.at[df.diaObjectId.iat[0], meanName])
503 return np.nansum((delta / df["psFluxErr"]) ** 2)
505 diaObjects.loc[:, "{}PSFluxChi2".format(filterName)] = \
506 filterDiaSources.apply(_chi2)
509class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
510 pass
513@register("ap_madFlux")
514class MadDiaPsFlux(DiaObjectCalculationPlugin):
515 """Compute median absolute deviation of diaSource fluxes.
516 """
518 ConfigClass = MadDiaPsFluxConfig
520 # Required input Cols
521 # Output columns are created upon instantiation of the class.
522 outputCols = ["PSFluxMAD"]
523 plugType = "multi"
524 needsFilter = True
526 @classmethod
527 def getExecutionOrder(cls):
528 return cls.DEFAULT_CATALOGCALCULATION
530 @catchWarnings(warns=["All-NaN slice encountered"])
531 def calculate(self,
532 diaObjects,
533 diaSources,
534 filterDiaSources,
535 filterName,
536 **kwargs):
537 """Compute the median absolute deviation of the point source fluxes.
539 Parameters
540 ----------
541 diaObject : `dict`
542 Summary object to store values in.
543 diaSources : `pandas.DataFrame`
544 DataFrame representing all diaSources associated with this
545 diaObject.
546 filterDiaSources : `pandas.DataFrame`
547 DataFrame representing diaSources associated with this
548 diaObject that are observed in the band pass ``filterName``.
549 filterName : `str`
550 Simple, string name of the filter for the flux being calculated.
551 **kwargs
552 Any additional keyword arguments that may be passed to the plugin.
553 """
554 diaObjects.loc[:, "{}PSFluxMAD".format(filterName)] = \
555 filterDiaSources.psFlux.apply(median_absolute_deviation,
556 ignore_nan=True)
559class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
560 pass
563@register("ap_skewFlux")
564class SkewDiaPsFlux(DiaObjectCalculationPlugin):
565 """Compute the skew of diaSource fluxes.
566 """
568 ConfigClass = SkewDiaPsFluxConfig
570 # Required input Cols
571 # Output columns are created upon instantiation of the class.
572 outputCols = ["PSFluxSkew"]
573 plugType = "multi"
574 needsFilter = True
576 @classmethod
577 def getExecutionOrder(cls):
578 return cls.DEFAULT_CATALOGCALCULATION
580 def calculate(self,
581 diaObjects,
582 diaSources,
583 filterDiaSources,
584 filterName,
585 **kwargs):
586 """Compute the skew of the point source fluxes.
588 Parameters
589 ----------
590 diaObject : `dict`
591 Summary object to store values in.
592 diaSources : `pandas.DataFrame`
593 DataFrame representing all diaSources associated with this
594 diaObject.
595 filterDiaSources : `pandas.DataFrame`
596 DataFrame representing diaSources associated with this
597 diaObject that are observed in the band pass ``filterName``.
598 filterName : `str`
599 Simple, string name of the filter for the flux being calculated.
600 **kwargs
601 Any additional keyword arguments that may be passed to the plugin.
602 """
603 diaObjects.loc[:, "{}PSFluxSkew".format(filterName)] = \
604 filterDiaSources.psFlux.skew()
607class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
608 pass
611@register("ap_minMaxFlux")
612class MinMaxDiaPsFlux(DiaObjectCalculationPlugin):
613 """Compute min/max of diaSource fluxes.
614 """
616 ConfigClass = MinMaxDiaPsFluxConfig
618 # Required input Cols
619 # Output columns are created upon instantiation of the class.
620 outputCols = ["PSFluxMin", "PSFluxMax"]
621 plugType = "multi"
622 needsFilter = True
624 @classmethod
625 def getExecutionOrder(cls):
626 return cls.DEFAULT_CATALOGCALCULATION
628 def calculate(self,
629 diaObjects,
630 diaSources,
631 filterDiaSources,
632 filterName,
633 **kwargs):
634 """Compute min/max of the point source fluxes.
636 Parameters
637 ----------
638 diaObject : `dict`
639 Summary object to store values in.
640 diaSources : `pandas.DataFrame`
641 DataFrame representing all diaSources associated with this
642 diaObject.
643 filterDiaSources : `pandas.DataFrame`
644 DataFrame representing diaSources associated with this
645 diaObject that are observed in the band pass ``filterName``.
646 filterName : `str`
647 Simple, string name of the filter for the flux being calculated.
648 **kwargs
649 Any additional keyword arguments that may be passed to the plugin.
650 """
651 minName = "{}PSFluxMin".format(filterName)
652 if minName not in diaObjects.columns:
653 diaObjects[minName] = np.nan
654 maxName = "{}PSFluxMax".format(filterName)
655 if maxName not in diaObjects.columns:
656 diaObjects[maxName] = np.nan
658 diaObjects.loc[:, minName] = filterDiaSources.psFlux.min()
659 diaObjects.loc[:, maxName] = filterDiaSources.psFlux.max()
662class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
663 pass
666@register("ap_maxSlopeFlux")
667class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin):
668 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
669 """
671 ConfigClass = MinMaxDiaPsFluxConfig
673 # Required input Cols
674 # Output columns are created upon instantiation of the class.
675 outputCols = ["PSFluxMaxSlope"]
676 plugType = "multi"
677 needsFilter = True
679 @classmethod
680 def getExecutionOrder(cls):
681 return cls.DEFAULT_CATALOGCALCULATION
683 def calculate(self,
684 diaObjects,
685 diaSources,
686 filterDiaSources,
687 filterName,
688 **kwargs):
689 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
691 Parameters
692 ----------
693 diaObject : `dict`
694 Summary object to store values in.
695 diaSources : `pandas.DataFrame`
696 DataFrame representing all diaSources associated with this
697 diaObject.
698 filterDiaSources : `pandas.DataFrame`
699 DataFrame representing diaSources associated with this
700 diaObject that are observed in the band pass ``filterName``.
701 filterName : `str`
702 Simple, string name of the filter for the flux being calculated.
703 **kwargs
704 Any additional keyword arguments that may be passed to the plugin.
705 """
707 def _maxSlope(df):
708 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
709 np.isnan(df["midPointTai"]))]
710 if len(tmpDf) < 2:
711 return np.nan
712 times = tmpDf["midPointTai"].to_numpy()
713 timeArgs = times.argsort()
714 times = times[timeArgs]
715 fluxes = tmpDf["psFlux"].to_numpy()[timeArgs]
716 return (np.diff(fluxes) / np.diff(times)).max()
718 diaObjects.loc[:, "{}PSFluxMaxSlope".format(filterName)] = \
719 filterDiaSources.apply(_maxSlope)
722class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
723 pass
726@register("ap_meanErrFlux")
727class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin):
728 """Compute the mean of the dia source errors.
729 """
731 ConfigClass = ErrMeanDiaPsFluxConfig
733 # Required input Cols
734 # Output columns are created upon instantiation of the class.
735 outputCols = ["PSFluxErrMean"]
736 plugType = "multi"
737 needsFilter = True
739 @classmethod
740 def getExecutionOrder(cls):
741 return cls.DEFAULT_CATALOGCALCULATION
743 def calculate(self,
744 diaObjects,
745 diaSources,
746 filterDiaSources,
747 filterName,
748 **kwargs):
749 """Compute the mean of the dia source errors.
751 Parameters
752 ----------
753 diaObject : `dict`
754 Summary object to store values in.
755 diaSources : `pandas.DataFrame`
756 DataFrame representing all diaSources associated with this
757 diaObject.
758 filterDiaSources : `pandas.DataFrame`
759 DataFrame representing diaSources associated with this
760 diaObject that are observed in the band pass ``filterName``.
761 filterName : `str`
762 Simple, string name of the filter for the flux being calculated.
763 **kwargs
764 Any additional keyword arguments that may be passed to the plugin.
765 """
766 diaObjects.loc[:, "{}PSFluxErrMean".format(filterName)] = \
767 filterDiaSources.psFluxErr.mean()
770class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
771 pass
774@register("ap_linearFit")
775class LinearFitDiaPsFlux(DiaObjectCalculationPlugin):
776 """Compute fit a linear model to flux vs time.
777 """
779 ConfigClass = LinearFitDiaPsFluxConfig
781 # Required input Cols
782 # Output columns are created upon instantiation of the class.
783 outputCols = ["PSFluxLinearSlope", "PSFluxLinearIntercept"]
784 plugType = "multi"
785 needsFilter = True
787 @classmethod
788 def getExecutionOrder(cls):
789 return cls.DEFAULT_CATALOGCALCULATION
791 def calculate(self,
792 diaObjects,
793 diaSources,
794 filterDiaSources,
795 filterName,
796 **kwargs):
797 """Compute fit a linear model to flux vs time.
799 Parameters
800 ----------
801 diaObject : `dict`
802 Summary object to store values in.
803 diaSources : `pandas.DataFrame`
804 DataFrame representing all diaSources associated with this
805 diaObject.
806 filterDiaSources : `pandas.DataFrame`
807 DataFrame representing diaSources associated with this
808 diaObject that are observed in the band pass ``filterName``.
809 filterName : `str`
810 Simple, string name of the filter for the flux being calculated.
811 **kwargs
812 Any additional keyword arguments that may be passed to the plugin.
813 """
815 mName = "{}PSFluxLinearSlope".format(filterName)
816 if mName not in diaObjects.columns:
817 diaObjects[mName] = np.nan
818 bName = "{}PSFluxLinearIntercept".format(filterName)
819 if bName not in diaObjects.columns:
820 diaObjects[bName] = np.nan
822 def _linearFit(df):
823 tmpDf = df[~np.logical_or(
824 np.isnan(df["psFlux"]),
825 np.logical_or(np.isnan(df["psFluxErr"]),
826 np.isnan(df["midPointTai"])))]
827 if len(tmpDf) < 2:
828 return pd.Series({mName: np.nan, bName: np.nan})
829 fluxes = tmpDf["psFlux"].to_numpy()
830 errors = tmpDf["psFluxErr"].to_numpy()
831 times = tmpDf["midPointTai"].to_numpy()
832 A = np.array([times / errors, 1 / errors]).transpose()
833 m, b = lsq_linear(A, fluxes / errors).x
834 return pd.Series({mName: m, bName: b})
836 diaObjects.loc[:, [mName, bName]] = filterDiaSources.apply(_linearFit)
839class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
840 pass
843@register("ap_stetsonJ")
844class StetsonJDiaPsFlux(DiaObjectCalculationPlugin):
845 """Compute the StetsonJ statistic on the DIA point source fluxes.
846 """
848 ConfigClass = LinearFitDiaPsFluxConfig
850 # Required input Cols
851 inputCols = ["PSFluxMean"]
852 # Output columns are created upon instantiation of the class.
853 outputCols = ["PSFluxStetsonJ"]
854 plugType = "multi"
855 needsFilter = True
857 @classmethod
858 def getExecutionOrder(cls):
859 return cls.FLUX_MOMENTS_CALCULATED
861 def calculate(self,
862 diaObjects,
863 diaSources,
864 filterDiaSources,
865 filterName,
866 **kwargs):
867 """Compute the StetsonJ statistic on the DIA point source fluxes.
869 Parameters
870 ----------
871 diaObject : `dict`
872 Summary object to store values in.
873 diaSources : `pandas.DataFrame`
874 DataFrame representing all diaSources associated with this
875 diaObject.
876 filterDiaSources : `pandas.DataFrame`
877 DataFrame representing diaSources associated with this
878 diaObject that are observed in the band pass ``filterName``.
879 filterName : `str`
880 Simple, string name of the filter for the flux being calculated.
881 **kwargs
882 Any additional keyword arguments that may be passed to the plugin.
883 """
884 meanName = "{}PSFluxMean".format(filterName)
886 def _stetsonJ(df):
887 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
888 np.isnan(df["psFluxErr"]))]
889 if len(tmpDf) < 2:
890 return np.nan
891 fluxes = tmpDf["psFlux"].to_numpy()
892 errors = tmpDf["psFluxErr"].to_numpy()
894 return self._stetson_J(
895 fluxes,
896 errors,
897 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
899 diaObjects.loc[:, "{}PSFluxStetsonJ".format(filterName)] = \
900 filterDiaSources.apply(_stetsonJ)
902 def _stetson_J(self, fluxes, errors, mean=None):
903 """Compute the single band stetsonJ statistic.
905 Parameters
906 ----------
907 fluxes : `numpy.ndarray` (N,)
908 Calibrated lightcurve flux values.
909 errors : `numpy.ndarray` (N,)
910 Errors on the calibrated lightcurve fluxes.
911 mean : `float`
912 Starting mean from previous plugin.
914 Returns
915 -------
916 stetsonJ : `float`
917 stetsonJ statistic for the input fluxes and errors.
919 References
920 ----------
921 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
922 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
923 """
924 n_points = len(fluxes)
925 flux_mean = self._stetson_mean(fluxes, errors, mean)
926 delta_val = (
927 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
928 p_k = delta_val ** 2 - 1
930 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
932 def _stetson_mean(self,
933 values,
934 errors,
935 mean=None,
936 alpha=2.,
937 beta=2.,
938 n_iter=20,
939 tol=1e-6):
940 """Compute the stetson mean of the fluxes which down-weights outliers.
942 Weighted biased on an error weighted difference scaled by a constant
943 (1/``a``) and raised to the power beta. Higher betas more harshly
944 penalize outliers and ``a`` sets the number of sigma where a weighted
945 difference of 1 occurs.
947 Parameters
948 ----------
949 values : `numpy.dnarray`, (N,)
950 Input values to compute the mean of.
951 errors : `numpy.ndarray`, (N,)
952 Errors on the input values.
953 mean : `float`
954 Starting mean value or None.
955 alpha : `float`
956 Scalar down-weighting of the fractional difference. lower->more
957 clipping. (Default value is 2.)
958 beta : `float`
959 Power law slope of the used to down-weight outliers. higher->more
960 clipping. (Default value is 2.)
961 n_iter : `int`
962 Number of iterations of clipping.
963 tol : `float`
964 Fractional and absolute tolerance goal on the change in the mean
965 before exiting early. (Default value is 1e-6)
967 Returns
968 -------
969 mean : `float`
970 Weighted stetson mean result.
972 References
973 ----------
974 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
975 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
976 """
977 n_points = len(values)
978 n_factor = np.sqrt(n_points / (n_points - 1))
979 inv_var = 1 / errors ** 2
981 if mean is None:
982 mean = np.average(values, weights=inv_var)
983 for iter_idx in range(n_iter):
984 chi = np.fabs(n_factor * (values - mean) / errors)
985 tmp_mean = np.average(
986 values,
987 weights=inv_var / (1 + (chi / alpha) ** beta))
988 diff = np.fabs(tmp_mean - mean)
989 mean = tmp_mean
990 if diff / mean < tol and diff < tol:
991 break
992 return mean
995class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
996 pass
999@register("ap_meanTotFlux")
1000class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin):
1001 """Compute the weighted mean and mean error on the point source fluxes
1002 forced photometered at the DiaSource location in the calibrated image.
1004 Additionally store number of usable data points.
1005 """
1007 ConfigClass = WeightedMeanDiaPsFluxConfig
1008 outputCols = ["TOTFluxMean", "TOTFluxMeanErr"]
1009 plugType = "multi"
1010 needsFilter = True
1012 @classmethod
1013 def getExecutionOrder(cls):
1014 return cls.DEFAULT_CATALOGCALCULATION
1016 @catchWarnings(warns=["invalid value encountered",
1017 "divide by zero"])
1018 def calculate(self,
1019 diaObjects,
1020 diaSources,
1021 filterDiaSources,
1022 filterName,
1023 **kwargs):
1024 """Compute the weighted mean and mean error of the point source flux.
1026 Parameters
1027 ----------
1028 diaObject : `dict`
1029 Summary object to store values in.
1030 diaSources : `pandas.DataFrame`
1031 DataFrame representing all diaSources associated with this
1032 diaObject.
1033 filterDiaSources : `pandas.DataFrame`
1034 DataFrame representing diaSources associated with this
1035 diaObject that are observed in the band pass ``filterName``.
1036 filterName : `str`
1037 Simple, string name of the filter for the flux being calculated.
1038 **kwargs
1039 Any additional keyword arguments that may be passed to the plugin.
1040 """
1041 totMeanName = "{}TOTFluxMean".format(filterName)
1042 if totMeanName not in diaObjects.columns:
1043 diaObjects[totMeanName] = np.nan
1044 totErrName = "{}TOTFluxMeanErr".format(filterName)
1045 if totErrName not in diaObjects.columns:
1046 diaObjects[totErrName] = np.nan
1048 def _meanFlux(df):
1049 tmpDf = df[~np.logical_or(np.isnan(df["totFlux"]),
1050 np.isnan(df["totFluxErr"]))]
1051 tot_weight = np.nansum(1 / tmpDf["totFluxErr"] ** 2)
1052 fluxMean = np.nansum(tmpDf["totFlux"]
1053 / tmpDf["totFluxErr"] ** 2)
1054 fluxMean /= tot_weight
1055 fluxMeanErr = np.sqrt(1 / tot_weight)
1057 return pd.Series({totMeanName: fluxMean,
1058 totErrName: fluxMeanErr})
1060 diaObjects.loc[:, [totMeanName, totErrName]] = \
1061 filterDiaSources.apply(_meanFlux)
1064class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1065 pass
1068@register("ap_sigmaTotFlux")
1069class SigmaDiaTotFlux(DiaObjectCalculationPlugin):
1070 """Compute scatter of diaSource fluxes.
1071 """
1073 ConfigClass = SigmaDiaPsFluxConfig
1074 # Output columns are created upon instantiation of the class.
1075 outputCols = ["TOTFluxSigma"]
1076 plugType = "multi"
1077 needsFilter = True
1079 @classmethod
1080 def getExecutionOrder(cls):
1081 return cls.DEFAULT_CATALOGCALCULATION
1083 def calculate(self,
1084 diaObjects,
1085 diaSources,
1086 filterDiaSources,
1087 filterName,
1088 **kwargs):
1089 """Compute the sigma fluxes of the point source flux measured on the
1090 calibrated image.
1092 Parameters
1093 ----------
1094 diaObject : `dict`
1095 Summary object to store values in.
1096 diaSources : `pandas.DataFrame`
1097 DataFrame representing all diaSources associated with this
1098 diaObject.
1099 filterDiaSources : `pandas.DataFrame`
1100 DataFrame representing diaSources associated with this
1101 diaObject that are observed in the band pass ``filterName``.
1102 filterName : `str`
1103 Simple, string name of the filter for the flux being calculated.
1104 **kwargs
1105 Any additional keyword arguments that may be passed to the plugin.
1106 """
1107 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1108 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1109 diaObjects.loc[:, "{}TOTFluxSigma".format(filterName)] = \
1110 filterDiaSources.totFlux.std()