Coverage for python/lsst/meas/base/diaCalculationPlugins.py: 58%
378 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-18 02:47 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-18 02:47 -0700
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.
151 Notes
152 -----
153 This plugin was implemented to satisfy requirements of old APDB interface
154 which required ``pixelId`` column in DiaObject with HTM20 index. APDB
155 interface had migrated to not need that information, but we keep this
156 plugin in case it may be useful for something else.
157 """
158 ConfigClass = HTMIndexDiaPositionConfig
160 plugType = 'single'
162 inputCols = ["ra", "decl"]
163 outputCols = ["pixelId"]
164 needsFilter = False
166 def __init__(self, config, name, metadata):
167 DiaObjectCalculationPlugin.__init__(self, config, name, metadata)
168 self.pixelator = sphgeom.HtmPixelization(self.config.htmLevel)
170 @classmethod
171 def getExecutionOrder(cls):
172 return cls.FLUX_MOMENTS_CALCULATED
174 def calculate(self, diaObjects, diaObjectId, **kwargs):
175 """Compute the mean position of a DiaObject given a set of DiaSources
177 Parameters
178 ----------
179 diaObjects : `pandas.dataFrame`
180 Summary objects to store values in and read ra/decl from.
181 diaObjectId : `int`
182 Id of the diaObject to update.
183 **kwargs
184 Any additional keyword arguments that may be passed to the plugin.
185 """
186 sphPoint = geom.SpherePoint(
187 diaObjects.at[diaObjectId, "ra"] * geom.degrees,
188 diaObjects.at[diaObjectId, "decl"] * geom.degrees)
189 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index(
190 sphPoint.getVector())
193class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig):
194 pass
197@register("ap_nDiaSources")
198class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin):
199 """Compute the total number of DiaSources associated with this DiaObject.
200 """
202 ConfigClass = NumDiaSourcesDiaPluginConfig
203 outputCols = ["nDiaSources"]
204 plugType = "multi"
205 needsFilter = False
207 @classmethod
208 def getExecutionOrder(cls):
209 return cls.DEFAULT_CATALOGCALCULATION
211 def calculate(self, diaObjects, diaSources, **kwargs):
212 """Compute the total number of DiaSources associated with this DiaObject.
214 Parameters
215 ----------
216 diaObject : `dict`
217 Summary object to store values in and read ra/decl from.
218 **kwargs
219 Any additional keyword arguments that may be passed to the plugin.
220 """
221 diaObjects.loc[:, "nDiaSources"] = diaSources.diaObjectId.count()
224class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig):
225 pass
228@register("ap_diaObjectFlag")
229class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin):
230 """Find if any DiaSource is flagged.
232 Set the DiaObject flag if any DiaSource is flagged.
233 """
235 ConfigClass = NumDiaSourcesDiaPluginConfig
236 outputCols = ["flags"]
237 plugType = "multi"
238 needsFilter = False
240 @classmethod
241 def getExecutionOrder(cls):
242 return cls.DEFAULT_CATALOGCALCULATION
244 def calculate(self, diaObjects, diaSources, **kwargs):
245 """Find if any DiaSource is flagged.
247 Set the DiaObject flag if any DiaSource is flagged.
249 Parameters
250 ----------
251 diaObject : `dict`
252 Summary object to store values in and read ra/decl from.
253 **kwargs
254 Any additional keyword arguments that may be passed to the plugin.
255 """
256 diaObjects.loc[:, "flags"] = diaSources.flags.any().astype(np.uint64)
259class WeightedMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
260 pass
263@register("ap_meanFlux")
264class WeightedMeanDiaPsFlux(DiaObjectCalculationPlugin):
265 """Compute the weighted mean and mean error on the point source fluxes
266 of the DiaSource measured on the difference image.
268 Additionally store number of usable data points.
269 """
271 ConfigClass = WeightedMeanDiaPsFluxConfig
272 outputCols = ["PSFluxMean", "PSFluxMeanErr", "PSFluxNdata"]
273 plugType = "multi"
274 needsFilter = True
276 @classmethod
277 def getExecutionOrder(cls):
278 return cls.DEFAULT_CATALOGCALCULATION
280 @catchWarnings(warns=["invalid value encountered",
281 "divide by zero"])
282 def calculate(self,
283 diaObjects,
284 diaSources,
285 filterDiaSources,
286 filterName,
287 **kwargs):
288 """Compute the weighted mean and mean error of the point source flux.
290 Parameters
291 ----------
292 diaObject : `dict`
293 Summary object to store values in.
294 diaSources : `pandas.DataFrame`
295 DataFrame representing all diaSources associated with this
296 diaObject.
297 filterDiaSources : `pandas.DataFrame`
298 DataFrame representing diaSources associated with this
299 diaObject that are observed in the band pass ``filterName``.
300 filterName : `str`
301 Simple, string name of the filter for the flux being calculated.
302 **kwargs
303 Any additional keyword arguments that may be passed to the plugin.
304 """
305 meanName = "{}PSFluxMean".format(filterName)
306 errName = "{}PSFluxMeanErr".format(filterName)
307 nDataName = "{}PSFluxNdata".format(filterName)
308 if meanName not in diaObjects.columns:
309 diaObjects[meanName] = np.nan
310 if errName not in diaObjects.columns:
311 diaObjects[errName] = np.nan
312 if nDataName not in diaObjects.columns:
313 diaObjects[nDataName] = 0
315 def _weightedMean(df):
316 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
317 np.isnan(df["psFluxErr"]))]
318 tot_weight = np.nansum(1 / tmpDf["psFluxErr"] ** 2)
319 fluxMean = np.nansum(tmpDf["psFlux"]
320 / tmpDf["psFluxErr"] ** 2)
321 fluxMean /= tot_weight
322 if tot_weight > 0:
323 fluxMeanErr = np.sqrt(1 / tot_weight)
324 else:
325 fluxMeanErr = np.nan
326 nFluxData = len(tmpDf)
328 return pd.Series({meanName: fluxMean,
329 errName: fluxMeanErr,
330 nDataName: nFluxData},
331 dtype="object")
333 diaObjects.loc[:, [meanName, errName, nDataName]] = \
334 filterDiaSources.apply(_weightedMean)
337class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
338 percentiles = pexConfig.ListField(
339 dtype=int,
340 default=[5, 25, 50, 75, 95],
341 doc="Percentiles to calculate to compute values for. Should be "
342 "integer values."
343 )
346@register("ap_percentileFlux")
347class PercentileDiaPsFlux(DiaObjectCalculationPlugin):
348 """Compute percentiles of diaSource fluxes.
349 """
351 ConfigClass = PercentileDiaPsFluxConfig
352 # Output columns are created upon instantiation of the class.
353 outputCols = []
354 plugType = "multi"
355 needsFilter = True
357 def __init__(self, config, name, metadata, **kwargs):
358 DiaObjectCalculationPlugin.__init__(self,
359 config,
360 name,
361 metadata,
362 **kwargs)
363 self.outputCols = ["PSFluxPercentile{:02d}".format(percent)
364 for percent in self.config.percentiles]
366 @classmethod
367 def getExecutionOrder(cls):
368 return cls.DEFAULT_CATALOGCALCULATION
370 @catchWarnings(warns=["All-NaN slice encountered"])
371 def calculate(self,
372 diaObjects,
373 diaSources,
374 filterDiaSources,
375 filterName,
376 **kwargs):
377 """Compute the percentile fluxes of the point source flux.
379 Parameters
380 ----------
381 diaObject : `dict`
382 Summary object to store values in.
383 diaSources : `pandas.DataFrame`
384 DataFrame representing all diaSources associated with this
385 diaObject.
386 filterDiaSources : `pandas.DataFrame`
387 DataFrame representing diaSources associated with this
388 diaObject that are observed in the band pass ``filterName``.
389 filterName : `str`
390 Simple, string name of the filter for the flux being calculated.
391 **kwargs
392 Any additional keyword arguments that may be passed to the plugin.
393 """
394 pTileNames = []
395 for tilePercent in self.config.percentiles:
396 pTileName = "{}PSFluxPercentile{:02d}".format(filterName,
397 tilePercent)
398 pTileNames.append(pTileName)
399 if pTileName not in diaObjects.columns:
400 diaObjects[pTileName] = np.nan
402 def _fluxPercentiles(df):
403 pTiles = np.nanpercentile(df["psFlux"], self.config.percentiles)
404 return pd.Series(
405 dict((tileName, pTile)
406 for tileName, pTile in zip(pTileNames, pTiles)))
408 diaObjects.loc[:, pTileNames] = filterDiaSources.apply(_fluxPercentiles)
411class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
412 pass
415@register("ap_sigmaFlux")
416class SigmaDiaPsFlux(DiaObjectCalculationPlugin):
417 """Compute scatter of diaSource fluxes.
418 """
420 ConfigClass = SigmaDiaPsFluxConfig
421 # Output columns are created upon instantiation of the class.
422 outputCols = ["PSFluxSigma"]
423 plugType = "multi"
424 needsFilter = True
426 @classmethod
427 def getExecutionOrder(cls):
428 return cls.DEFAULT_CATALOGCALCULATION
430 def calculate(self,
431 diaObjects,
432 diaSources,
433 filterDiaSources,
434 filterName,
435 **kwargs):
436 """Compute the sigma fluxes of the point source flux.
438 Parameters
439 ----------
440 diaObject : `dict`
441 Summary object to store values in.
442 diaSources : `pandas.DataFrame`
443 DataFrame representing all diaSources associated with this
444 diaObject.
445 filterDiaSources : `pandas.DataFrame`
446 DataFrame representing diaSources associated with this
447 diaObject that are observed in the band pass ``filterName``.
448 filterName : `str`
449 Simple, string name of the filter for the flux being calculated.
450 **kwargs
451 Any additional keyword arguments that may be passed to the plugin.
452 """
453 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
454 # estimator of scatter (i.e. 'N - 1' instead of 'N').
455 diaObjects.loc[:, "{}PSFluxSigma".format(filterName)] = \
456 filterDiaSources.psFlux.std()
459class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig):
460 pass
463@register("ap_chi2Flux")
464class Chi2DiaPsFlux(DiaObjectCalculationPlugin):
465 """Compute chi2 of diaSource fluxes.
466 """
468 ConfigClass = Chi2DiaPsFluxConfig
470 # Required input Cols
471 inputCols = ["PSFluxMean"]
472 # Output columns are created upon instantiation of the class.
473 outputCols = ["PSFluxChi2"]
474 plugType = "multi"
475 needsFilter = True
477 @classmethod
478 def getExecutionOrder(cls):
479 return cls.FLUX_MOMENTS_CALCULATED
481 @catchWarnings(warns=["All-NaN slice encountered"])
482 def calculate(self,
483 diaObjects,
484 diaSources,
485 filterDiaSources,
486 filterName,
487 **kwargs):
488 """Compute the chi2 of the point source fluxes.
490 Parameters
491 ----------
492 diaObject : `dict`
493 Summary object to store values in.
494 diaSources : `pandas.DataFrame`
495 DataFrame representing all diaSources associated with this
496 diaObject.
497 filterDiaSources : `pandas.DataFrame`
498 DataFrame representing diaSources associated with this
499 diaObject that are observed in the band pass ``filterName``.
500 filterName : `str`
501 Simple, string name of the filter for the flux being calculated.
502 **kwargs
503 Any additional keyword arguments that may be passed to the plugin.
504 """
505 meanName = "{}PSFluxMean".format(filterName)
507 def _chi2(df):
508 delta = (df["psFlux"]
509 - diaObjects.at[df.diaObjectId.iat[0], meanName])
510 return np.nansum((delta / df["psFluxErr"]) ** 2)
512 diaObjects.loc[:, "{}PSFluxChi2".format(filterName)] = \
513 filterDiaSources.apply(_chi2)
516class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
517 pass
520@register("ap_madFlux")
521class MadDiaPsFlux(DiaObjectCalculationPlugin):
522 """Compute median absolute deviation of diaSource fluxes.
523 """
525 ConfigClass = MadDiaPsFluxConfig
527 # Required input Cols
528 # Output columns are created upon instantiation of the class.
529 outputCols = ["PSFluxMAD"]
530 plugType = "multi"
531 needsFilter = True
533 @classmethod
534 def getExecutionOrder(cls):
535 return cls.DEFAULT_CATALOGCALCULATION
537 @catchWarnings(warns=["All-NaN slice encountered"])
538 def calculate(self,
539 diaObjects,
540 diaSources,
541 filterDiaSources,
542 filterName,
543 **kwargs):
544 """Compute the median absolute deviation of the point source fluxes.
546 Parameters
547 ----------
548 diaObject : `dict`
549 Summary object to store values in.
550 diaSources : `pandas.DataFrame`
551 DataFrame representing all diaSources associated with this
552 diaObject.
553 filterDiaSources : `pandas.DataFrame`
554 DataFrame representing diaSources associated with this
555 diaObject that are observed in the band pass ``filterName``.
556 filterName : `str`
557 Simple, string name of the filter for the flux being calculated.
558 **kwargs
559 Any additional keyword arguments that may be passed to the plugin.
560 """
561 diaObjects.loc[:, "{}PSFluxMAD".format(filterName)] = \
562 filterDiaSources.psFlux.apply(median_absolute_deviation,
563 ignore_nan=True)
566class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
567 pass
570@register("ap_skewFlux")
571class SkewDiaPsFlux(DiaObjectCalculationPlugin):
572 """Compute the skew of diaSource fluxes.
573 """
575 ConfigClass = SkewDiaPsFluxConfig
577 # Required input Cols
578 # Output columns are created upon instantiation of the class.
579 outputCols = ["PSFluxSkew"]
580 plugType = "multi"
581 needsFilter = True
583 @classmethod
584 def getExecutionOrder(cls):
585 return cls.DEFAULT_CATALOGCALCULATION
587 def calculate(self,
588 diaObjects,
589 diaSources,
590 filterDiaSources,
591 filterName,
592 **kwargs):
593 """Compute the skew of the point source fluxes.
595 Parameters
596 ----------
597 diaObject : `dict`
598 Summary object to store values in.
599 diaSources : `pandas.DataFrame`
600 DataFrame representing all diaSources associated with this
601 diaObject.
602 filterDiaSources : `pandas.DataFrame`
603 DataFrame representing diaSources associated with this
604 diaObject that are observed in the band pass ``filterName``.
605 filterName : `str`
606 Simple, string name of the filter for the flux being calculated.
607 **kwargs
608 Any additional keyword arguments that may be passed to the plugin.
609 """
610 diaObjects.loc[:, "{}PSFluxSkew".format(filterName)] = \
611 filterDiaSources.psFlux.skew()
614class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
615 pass
618@register("ap_minMaxFlux")
619class MinMaxDiaPsFlux(DiaObjectCalculationPlugin):
620 """Compute min/max of diaSource fluxes.
621 """
623 ConfigClass = MinMaxDiaPsFluxConfig
625 # Required input Cols
626 # Output columns are created upon instantiation of the class.
627 outputCols = ["PSFluxMin", "PSFluxMax"]
628 plugType = "multi"
629 needsFilter = True
631 @classmethod
632 def getExecutionOrder(cls):
633 return cls.DEFAULT_CATALOGCALCULATION
635 def calculate(self,
636 diaObjects,
637 diaSources,
638 filterDiaSources,
639 filterName,
640 **kwargs):
641 """Compute min/max of the point source fluxes.
643 Parameters
644 ----------
645 diaObject : `dict`
646 Summary object to store values in.
647 diaSources : `pandas.DataFrame`
648 DataFrame representing all diaSources associated with this
649 diaObject.
650 filterDiaSources : `pandas.DataFrame`
651 DataFrame representing diaSources associated with this
652 diaObject that are observed in the band pass ``filterName``.
653 filterName : `str`
654 Simple, string name of the filter for the flux being calculated.
655 **kwargs
656 Any additional keyword arguments that may be passed to the plugin.
657 """
658 minName = "{}PSFluxMin".format(filterName)
659 if minName not in diaObjects.columns:
660 diaObjects[minName] = np.nan
661 maxName = "{}PSFluxMax".format(filterName)
662 if maxName not in diaObjects.columns:
663 diaObjects[maxName] = np.nan
665 diaObjects.loc[:, minName] = filterDiaSources.psFlux.min()
666 diaObjects.loc[:, maxName] = filterDiaSources.psFlux.max()
669class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
670 pass
673@register("ap_maxSlopeFlux")
674class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin):
675 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
676 """
678 ConfigClass = MinMaxDiaPsFluxConfig
680 # Required input Cols
681 # Output columns are created upon instantiation of the class.
682 outputCols = ["PSFluxMaxSlope"]
683 plugType = "multi"
684 needsFilter = True
686 @classmethod
687 def getExecutionOrder(cls):
688 return cls.DEFAULT_CATALOGCALCULATION
690 def calculate(self,
691 diaObjects,
692 diaSources,
693 filterDiaSources,
694 filterName,
695 **kwargs):
696 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
698 Parameters
699 ----------
700 diaObject : `dict`
701 Summary object to store values in.
702 diaSources : `pandas.DataFrame`
703 DataFrame representing all diaSources associated with this
704 diaObject.
705 filterDiaSources : `pandas.DataFrame`
706 DataFrame representing diaSources associated with this
707 diaObject that are observed in the band pass ``filterName``.
708 filterName : `str`
709 Simple, string name of the filter for the flux being calculated.
710 **kwargs
711 Any additional keyword arguments that may be passed to the plugin.
712 """
714 def _maxSlope(df):
715 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
716 np.isnan(df["midPointTai"]))]
717 if len(tmpDf) < 2:
718 return np.nan
719 times = tmpDf["midPointTai"].to_numpy()
720 timeArgs = times.argsort()
721 times = times[timeArgs]
722 fluxes = tmpDf["psFlux"].to_numpy()[timeArgs]
723 return (np.diff(fluxes) / np.diff(times)).max()
725 diaObjects.loc[:, "{}PSFluxMaxSlope".format(filterName)] = \
726 filterDiaSources.apply(_maxSlope)
729class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
730 pass
733@register("ap_meanErrFlux")
734class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin):
735 """Compute the mean of the dia source errors.
736 """
738 ConfigClass = ErrMeanDiaPsFluxConfig
740 # Required input Cols
741 # Output columns are created upon instantiation of the class.
742 outputCols = ["PSFluxErrMean"]
743 plugType = "multi"
744 needsFilter = True
746 @classmethod
747 def getExecutionOrder(cls):
748 return cls.DEFAULT_CATALOGCALCULATION
750 def calculate(self,
751 diaObjects,
752 diaSources,
753 filterDiaSources,
754 filterName,
755 **kwargs):
756 """Compute the mean of the dia source errors.
758 Parameters
759 ----------
760 diaObject : `dict`
761 Summary object to store values in.
762 diaSources : `pandas.DataFrame`
763 DataFrame representing all diaSources associated with this
764 diaObject.
765 filterDiaSources : `pandas.DataFrame`
766 DataFrame representing diaSources associated with this
767 diaObject that are observed in the band pass ``filterName``.
768 filterName : `str`
769 Simple, string name of the filter for the flux being calculated.
770 **kwargs
771 Any additional keyword arguments that may be passed to the plugin.
772 """
773 diaObjects.loc[:, "{}PSFluxErrMean".format(filterName)] = \
774 filterDiaSources.psFluxErr.mean()
777class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
778 pass
781@register("ap_linearFit")
782class LinearFitDiaPsFlux(DiaObjectCalculationPlugin):
783 """Compute fit a linear model to flux vs time.
784 """
786 ConfigClass = LinearFitDiaPsFluxConfig
788 # Required input Cols
789 # Output columns are created upon instantiation of the class.
790 outputCols = ["PSFluxLinearSlope", "PSFluxLinearIntercept"]
791 plugType = "multi"
792 needsFilter = True
794 @classmethod
795 def getExecutionOrder(cls):
796 return cls.DEFAULT_CATALOGCALCULATION
798 def calculate(self,
799 diaObjects,
800 diaSources,
801 filterDiaSources,
802 filterName,
803 **kwargs):
804 """Compute fit a linear model to flux vs time.
806 Parameters
807 ----------
808 diaObject : `dict`
809 Summary object to store values in.
810 diaSources : `pandas.DataFrame`
811 DataFrame representing all diaSources associated with this
812 diaObject.
813 filterDiaSources : `pandas.DataFrame`
814 DataFrame representing diaSources associated with this
815 diaObject that are observed in the band pass ``filterName``.
816 filterName : `str`
817 Simple, string name of the filter for the flux being calculated.
818 **kwargs
819 Any additional keyword arguments that may be passed to the plugin.
820 """
822 mName = "{}PSFluxLinearSlope".format(filterName)
823 if mName not in diaObjects.columns:
824 diaObjects[mName] = np.nan
825 bName = "{}PSFluxLinearIntercept".format(filterName)
826 if bName not in diaObjects.columns:
827 diaObjects[bName] = np.nan
829 def _linearFit(df):
830 tmpDf = df[~np.logical_or(
831 np.isnan(df["psFlux"]),
832 np.logical_or(np.isnan(df["psFluxErr"]),
833 np.isnan(df["midPointTai"])))]
834 if len(tmpDf) < 2:
835 return pd.Series({mName: np.nan, bName: np.nan})
836 fluxes = tmpDf["psFlux"].to_numpy()
837 errors = tmpDf["psFluxErr"].to_numpy()
838 times = tmpDf["midPointTai"].to_numpy()
839 A = np.array([times / errors, 1 / errors]).transpose()
840 m, b = lsq_linear(A, fluxes / errors).x
841 return pd.Series({mName: m, bName: b})
843 diaObjects.loc[:, [mName, bName]] = filterDiaSources.apply(_linearFit)
846class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig):
847 pass
850@register("ap_stetsonJ")
851class StetsonJDiaPsFlux(DiaObjectCalculationPlugin):
852 """Compute the StetsonJ statistic on the DIA point source fluxes.
853 """
855 ConfigClass = LinearFitDiaPsFluxConfig
857 # Required input Cols
858 inputCols = ["PSFluxMean"]
859 # Output columns are created upon instantiation of the class.
860 outputCols = ["PSFluxStetsonJ"]
861 plugType = "multi"
862 needsFilter = True
864 @classmethod
865 def getExecutionOrder(cls):
866 return cls.FLUX_MOMENTS_CALCULATED
868 def calculate(self,
869 diaObjects,
870 diaSources,
871 filterDiaSources,
872 filterName,
873 **kwargs):
874 """Compute the StetsonJ statistic on the DIA point source fluxes.
876 Parameters
877 ----------
878 diaObject : `dict`
879 Summary object to store values in.
880 diaSources : `pandas.DataFrame`
881 DataFrame representing all diaSources associated with this
882 diaObject.
883 filterDiaSources : `pandas.DataFrame`
884 DataFrame representing diaSources associated with this
885 diaObject that are observed in the band pass ``filterName``.
886 filterName : `str`
887 Simple, string name of the filter for the flux being calculated.
888 **kwargs
889 Any additional keyword arguments that may be passed to the plugin.
890 """
891 meanName = "{}PSFluxMean".format(filterName)
893 def _stetsonJ(df):
894 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]),
895 np.isnan(df["psFluxErr"]))]
896 if len(tmpDf) < 2:
897 return np.nan
898 fluxes = tmpDf["psFlux"].to_numpy()
899 errors = tmpDf["psFluxErr"].to_numpy()
901 return self._stetson_J(
902 fluxes,
903 errors,
904 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
906 diaObjects.loc[:, "{}PSFluxStetsonJ".format(filterName)] = \
907 filterDiaSources.apply(_stetsonJ)
909 def _stetson_J(self, fluxes, errors, mean=None):
910 """Compute the single band stetsonJ statistic.
912 Parameters
913 ----------
914 fluxes : `numpy.ndarray` (N,)
915 Calibrated lightcurve flux values.
916 errors : `numpy.ndarray` (N,)
917 Errors on the calibrated lightcurve fluxes.
918 mean : `float`
919 Starting mean from previous plugin.
921 Returns
922 -------
923 stetsonJ : `float`
924 stetsonJ statistic for the input fluxes and errors.
926 References
927 ----------
928 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
929 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
930 """
931 n_points = len(fluxes)
932 flux_mean = self._stetson_mean(fluxes, errors, mean)
933 delta_val = (
934 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
935 p_k = delta_val ** 2 - 1
937 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
939 def _stetson_mean(self,
940 values,
941 errors,
942 mean=None,
943 alpha=2.,
944 beta=2.,
945 n_iter=20,
946 tol=1e-6):
947 """Compute the stetson mean of the fluxes which down-weights outliers.
949 Weighted biased on an error weighted difference scaled by a constant
950 (1/``a``) and raised to the power beta. Higher betas more harshly
951 penalize outliers and ``a`` sets the number of sigma where a weighted
952 difference of 1 occurs.
954 Parameters
955 ----------
956 values : `numpy.dnarray`, (N,)
957 Input values to compute the mean of.
958 errors : `numpy.ndarray`, (N,)
959 Errors on the input values.
960 mean : `float`
961 Starting mean value or None.
962 alpha : `float`
963 Scalar down-weighting of the fractional difference. lower->more
964 clipping. (Default value is 2.)
965 beta : `float`
966 Power law slope of the used to down-weight outliers. higher->more
967 clipping. (Default value is 2.)
968 n_iter : `int`
969 Number of iterations of clipping.
970 tol : `float`
971 Fractional and absolute tolerance goal on the change in the mean
972 before exiting early. (Default value is 1e-6)
974 Returns
975 -------
976 mean : `float`
977 Weighted stetson mean result.
979 References
980 ----------
981 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
982 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
983 """
984 n_points = len(values)
985 n_factor = np.sqrt(n_points / (n_points - 1))
986 inv_var = 1 / errors ** 2
988 if mean is None:
989 mean = np.average(values, weights=inv_var)
990 for iter_idx in range(n_iter):
991 chi = np.fabs(n_factor * (values - mean) / errors)
992 tmp_mean = np.average(
993 values,
994 weights=inv_var / (1 + (chi / alpha) ** beta))
995 diff = np.fabs(tmp_mean - mean)
996 mean = tmp_mean
997 if diff / mean < tol and diff < tol:
998 break
999 return mean
1002class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1003 pass
1006@register("ap_meanTotFlux")
1007class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin):
1008 """Compute the weighted mean and mean error on the point source fluxes
1009 forced photometered at the DiaSource location in the calibrated image.
1011 Additionally store number of usable data points.
1012 """
1014 ConfigClass = WeightedMeanDiaPsFluxConfig
1015 outputCols = ["TOTFluxMean", "TOTFluxMeanErr"]
1016 plugType = "multi"
1017 needsFilter = True
1019 @classmethod
1020 def getExecutionOrder(cls):
1021 return cls.DEFAULT_CATALOGCALCULATION
1023 @catchWarnings(warns=["invalid value encountered",
1024 "divide by zero"])
1025 def calculate(self,
1026 diaObjects,
1027 diaSources,
1028 filterDiaSources,
1029 filterName,
1030 **kwargs):
1031 """Compute the weighted mean and mean error of the point source flux.
1033 Parameters
1034 ----------
1035 diaObject : `dict`
1036 Summary object to store values in.
1037 diaSources : `pandas.DataFrame`
1038 DataFrame representing all diaSources associated with this
1039 diaObject.
1040 filterDiaSources : `pandas.DataFrame`
1041 DataFrame representing diaSources associated with this
1042 diaObject that are observed in the band pass ``filterName``.
1043 filterName : `str`
1044 Simple, string name of the filter for the flux being calculated.
1045 **kwargs
1046 Any additional keyword arguments that may be passed to the plugin.
1047 """
1048 totMeanName = "{}TOTFluxMean".format(filterName)
1049 if totMeanName not in diaObjects.columns:
1050 diaObjects[totMeanName] = np.nan
1051 totErrName = "{}TOTFluxMeanErr".format(filterName)
1052 if totErrName not in diaObjects.columns:
1053 diaObjects[totErrName] = np.nan
1055 def _meanFlux(df):
1056 tmpDf = df[~np.logical_or(np.isnan(df["totFlux"]),
1057 np.isnan(df["totFluxErr"]))]
1058 tot_weight = np.nansum(1 / tmpDf["totFluxErr"] ** 2)
1059 fluxMean = np.nansum(tmpDf["totFlux"]
1060 / tmpDf["totFluxErr"] ** 2)
1061 fluxMean /= tot_weight
1062 fluxMeanErr = np.sqrt(1 / tot_weight)
1064 return pd.Series({totMeanName: fluxMean,
1065 totErrName: fluxMeanErr})
1067 diaObjects.loc[:, [totMeanName, totErrName]] = \
1068 filterDiaSources.apply(_meanFlux)
1071class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1072 pass
1075@register("ap_sigmaTotFlux")
1076class SigmaDiaTotFlux(DiaObjectCalculationPlugin):
1077 """Compute scatter of diaSource fluxes.
1078 """
1080 ConfigClass = SigmaDiaPsFluxConfig
1081 # Output columns are created upon instantiation of the class.
1082 outputCols = ["TOTFluxSigma"]
1083 plugType = "multi"
1084 needsFilter = True
1086 @classmethod
1087 def getExecutionOrder(cls):
1088 return cls.DEFAULT_CATALOGCALCULATION
1090 def calculate(self,
1091 diaObjects,
1092 diaSources,
1093 filterDiaSources,
1094 filterName,
1095 **kwargs):
1096 """Compute the sigma fluxes of the point source flux measured on the
1097 calibrated image.
1099 Parameters
1100 ----------
1101 diaObject : `dict`
1102 Summary object to store values in.
1103 diaSources : `pandas.DataFrame`
1104 DataFrame representing all diaSources associated with this
1105 diaObject.
1106 filterDiaSources : `pandas.DataFrame`
1107 DataFrame representing diaSources associated with this
1108 diaObject that are observed in the band pass ``filterName``.
1109 filterName : `str`
1110 Simple, string name of the filter for the flux being calculated.
1111 **kwargs
1112 Any additional keyword arguments that may be passed to the plugin.
1113 """
1114 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1115 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1116 diaObjects.loc[:, "{}TOTFluxSigma".format(filterName)] = \
1117 filterDiaSources.totFlux.std()