Coverage for python / lsst / meas / base / diaCalculationPlugins.py: 42%
535 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 08:48 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 08:48 +0000
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.pipe.base as pipeBase
39import lsst.sphgeom as sphgeom
40from astropy.timeseries import LombScargle
41from astropy.timeseries import LombScargleMultiband
42import math
43import statistics
45from .diaCalculation import (
46 DiaObjectCalculationPluginConfig,
47 DiaObjectCalculationPlugin)
48from .pluginRegistry import register
51__all__ = ("MeanDiaPositionConfig", "MeanDiaPosition",
52 "HTMIndexDiaPosition", "HTMIndexDiaPositionConfig",
53 "NumDiaSourcesDiaPlugin", "NumDiaSourcesDiaPluginConfig",
54 "SimpleSourceFlagDiaPlugin", "SimpleSourceFlagDiaPluginConfig",
55 "WeightedMeanDiaPsfFluxConfig", "WeightedMeanDiaPsfFlux",
56 "PercentileDiaPsfFlux", "PercentileDiaPsfFluxConfig",
57 "SigmaDiaPsfFlux", "SigmaDiaPsfFluxConfig",
58 "Chi2DiaPsfFlux", "Chi2DiaPsfFluxConfig",
59 "MadDiaPsfFlux", "MadDiaPsfFluxConfig",
60 "SkewDiaPsfFlux", "SkewDiaPsfFluxConfig",
61 "MinMaxDiaPsfFlux", "MinMaxDiaPsfFluxConfig",
62 "MaxSlopeDiaPsfFlux", "MaxSlopeDiaPsfFluxConfig",
63 "ErrMeanDiaPsfFlux", "ErrMeanDiaPsfFluxConfig",
64 "LinearFitDiaPsfFlux", "LinearFitDiaPsfFluxConfig",
65 "StetsonJDiaPsfFlux", "StetsonJDiaPsfFluxConfig",
66 "WeightedMeanDiaTotFlux", "WeightedMeanDiaTotFluxConfig",
67 "SigmaDiaTotFlux", "SigmaDiaTotFluxConfig",
68 "LombScarglePeriodogram", "LombScarglePeriodogramConfig",
69 "LombScarglePeriodogramMulti", "LombScarglePeriodogramMultiConfig",
70 "UnphysicalDiaSourceSeparation")
73def catchWarnings(_func=None, *, warns=[]):
74 """Decorator for generically catching numpy warnings.
75 """
76 def decoratorCatchWarnings(func):
77 @functools.wraps(func)
78 def wrapperCatchWarnings(*args, **kwargs):
79 with warnings.catch_warnings():
80 for val in warns:
81 warnings.filterwarnings("ignore", val)
82 return func(*args, **kwargs)
83 return wrapperCatchWarnings
85 if _func is None: 85 ↛ 88line 85 didn't jump to line 88 because the condition on line 85 was always true
86 return decoratorCatchWarnings
87 else:
88 return decoratorCatchWarnings(_func)
91def typeSafePandasAssignment(
92 target,
93 source,
94 columns,
95 default_dtype=np.float64,
96 int_fill_value=0,
97 # TODO DM-53254: Remove the force_int_to_float hack.
98 force_int_to_float=False,
99):
100 """
101 Assign from a source dataframe to a target dataframe in a type safe way.
103 Parameters
104 ----------
105 target : `pd.DataFrame`
106 Target pandas dataframe.
107 source : `pd.DataFrame` or `pd.Series`
108 Grouped source dataframe.
109 columns : `list` [`str`]
110 List of columns to transfer.
111 default_dtype : `np.dtype`, optional
112 Default datatype (if not in target).
113 int_fill_value : `int`, optional
114 Fill value for integer columns to avoid pandas insisting
115 that everything should be float-ified as nans.
116 force_int_to_float : `bool`, optional
117 Force integer columns to float columns? Use this option
118 for backwards compatibility for old pandas misfeatures which
119 are expected by some downstream processes.
120 """
121 is_series = isinstance(source, pd.Series)
122 for col in columns:
123 if is_series:
124 source_col = source
125 else:
126 source_col = source[col]
128 matched_length = False
129 if col in target.columns:
130 target_dtype = target[col].dtype
131 matched_length = len(target) == len(source)
132 else:
133 target_dtype = default_dtype
135 if (matched_length or pd.api.types.is_float_dtype(target_dtype)) and not force_int_to_float:
136 # If we have a matched length or float, we can do a
137 # straight assignment here.
138 target.loc[:, col] = source_col.astype(target_dtype)
139 else:
140 # With mis-matched integers, we must do this dance to preserve types.
141 # Note that this may lose precision with very large numbers.
143 # Convert to float
144 target[col] = target[col].astype(np.float64)
145 # Set the column, casting to float.
146 target.loc[:, col] = source_col.astype(np.float64)
147 if not force_int_to_float:
148 # Convert back to integer
149 target[col] = target[col].fillna(int_fill_value).astype(target_dtype)
152def compute_optimized_periodogram_grid(x0, oversampling_factor=5, nyquist_factor=100):
153 """
154 Computes an optimized periodogram frequency grid for a given time series.
156 Parameters
157 ----------
158 x0 : `array`
159 The input time axis.
160 oversampling_factor : `int`, optional
161 The oversampling factor for frequency grid.
162 nyquist_factor : `int`, optional
163 The Nyquist factor for frequency grid.
165 Returns
166 -------
167 frequencies : `array`
168 The computed optimized periodogram frequency grid.
169 """
171 num_points = len(x0)
172 baseline = np.max(x0) - np.min(x0)
174 # Calculate the frequency resolution based on oversampling factor and baseline
175 frequency_resolution = 1. / baseline / oversampling_factor
177 num_frequencies = int(
178 0.5 * oversampling_factor * nyquist_factor * num_points)
179 frequencies = frequency_resolution + \
180 frequency_resolution * np.arange(num_frequencies)
182 return frequencies
185class LombScarglePeriodogramConfig(DiaObjectCalculationPluginConfig):
186 pass
189@register("ap_lombScarglePeriodogram")
190class LombScarglePeriodogram(DiaObjectCalculationPlugin):
191 """Compute the single-band period of a DiaObject given a set of DiaSources.
192 """
193 ConfigClass = LombScarglePeriodogramConfig
195 plugType = "multi"
196 outputCols = ["period", "power"]
197 needsFilter = True
199 @classmethod
200 def getExecutionOrder(cls):
201 return cls.DEFAULT_CATALOGCALCULATION
203 @catchWarnings(warns=["All-NaN slice encountered"])
204 def calculate(self,
205 diaObjects,
206 diaSources,
207 filterDiaSources,
208 band):
209 """Compute the periodogram.
211 Parameters
212 ----------
213 diaObjects : `pandas.DataFrame`
214 Summary objects to store values in.
215 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
216 Catalog of DiaSources summarized by this DiaObject.
217 """
219 # Check and initialize output columns in diaObjects.
220 if (periodCol := f"{band}_period") not in diaObjects.columns:
221 diaObjects[periodCol] = np.nan
222 if (powerCol := f"{band}_power") not in diaObjects.columns:
223 diaObjects[powerCol] = np.nan
225 def _calculate_period(df, min_detections=5, nterms=1, oversampling_factor=5, nyquist_factor=100):
226 """Compute the Lomb-Scargle periodogram given a set of DiaSources.
228 Parameters
229 ----------
230 df : `pandas.DataFrame`
231 The input DataFrame.
232 min_detections : `int`, optional
233 The minimum number of detections.
234 nterms : `int`, optional
235 The number of terms in the Lomb-Scargle model.
236 oversampling_factor : `int`, optional
237 The oversampling factor for frequency grid.
238 nyquist_factor : `int`, optional
239 The Nyquist factor for frequency grid.
241 Returns
242 -------
243 pd_tab : `pandas.Series`
244 The output DataFrame with the Lomb-Scargle parameters.
245 """
246 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
247 np.isnan(df["midpointMjdTai"]))]
249 if len(tmpDf) < min_detections:
250 return pd.Series({periodCol: np.nan, powerCol: np.nan})
252 time = tmpDf["midpointMjdTai"].to_numpy()
253 flux = tmpDf["psfFlux"].to_numpy()
254 flux_err = tmpDf["psfFluxErr"].to_numpy()
256 lsp = LombScargle(time, flux, dy=flux_err, nterms=nterms)
257 f_grid = compute_optimized_periodogram_grid(
258 time, oversampling_factor=oversampling_factor, nyquist_factor=nyquist_factor)
259 period = 1/f_grid
260 power = lsp.power(f_grid)
262 return pd.Series({periodCol: period[np.argmax(power)],
263 powerCol: np.max(power)})
265 diaObjects.loc[:, [periodCol, powerCol]
266 ] = filterDiaSources.apply(_calculate_period)
269class LombScarglePeriodogramMultiConfig(DiaObjectCalculationPluginConfig):
270 pass
273@register("ap_lombScarglePeriodogramMulti")
274class LombScarglePeriodogramMulti(DiaObjectCalculationPlugin):
275 """Compute the multi-band LombScargle periodogram of a DiaObject given a set of DiaSources.
276 """
277 ConfigClass = LombScarglePeriodogramMultiConfig
279 plugType = "multi"
280 outputCols = ["multiPeriod", "multiPower",
281 "multiFap", "multiAmp", "multiPhase"]
282 needsFilter = True
284 @classmethod
285 def getExecutionOrder(cls):
286 return cls.DEFAULT_CATALOGCALCULATION
288 @staticmethod
289 def calculate_baluev_fap(time, n, maxPeriod, zmax):
290 """Calculate the False-Alarm probability using the Baluev approximation.
292 Parameters
293 ----------
294 time : `array`
295 The input time axis.
296 n : `int`
297 The number of detections.
298 maxPeriod : `float`
299 The maximum period in the grid.
300 zmax : `float`
301 The maximum power in the grid.
303 Returns
304 -------
305 fap_estimate : `float`
306 The False-Alarm probability Baluev approximation.
308 Notes
309 ----------
310 .. [1] Baluev, R. V. 2008, MNRAS, 385, 1279
311 .. [2] Süveges, M., Guy, L.P., Eyer, L., et al. 2015, MNRAS, 450, 2052
312 """
313 if n <= 2:
314 return np.nan
316 gam_ratio = math.factorial(int((n - 1)/2)) / math.factorial(int((n - 2)/2))
317 fu = 1/maxPeriod
318 return gam_ratio * np.sqrt(
319 4*np.pi*statistics.variance(time)
320 ) * fu * (1-zmax)**((n-4)/2) * np.sqrt(zmax)
322 @staticmethod
323 def generate_lsp_params(lsp_model, fbest, bands):
324 """Generate the Lomb-Scargle parameters.
325 Parameters
326 ----------
327 lsp_model : `astropy.timeseries.LombScargleMultiband`
328 The Lomb-Scargle model.
329 fbest : `float`
330 The best period.
331 bands : `array`
332 The bands of the time series.
334 Returns
335 -------
336 Amp : `array`
337 The amplitude of the time series.
338 Ph : `array`
339 The phase of the time series.
341 Notes
342 ----------
343 .. [1] VanderPlas, J. T., & Ivezic, Z. 2015, ApJ, 812, 18
344 """
345 best_params = lsp_model.model_parameters(fbest, units=True)
347 name_params = [f"theta_base_{i}" for i in range(3)]
348 name_params += [f"theta_band_{band}_{i}" for band in np.unique(bands) for i in range(3)]
350 df_params = pd.DataFrame([best_params], columns=name_params)
352 unique_bands = np.unique(bands)
354 amplitude_band = [np.sqrt(df_params[f"theta_band_{band}_1"]**2
355 + df_params[f"theta_band_{band}_2"]**2)
356 for band in unique_bands]
357 phase_bands = [np.arctan2(df_params[f"theta_band_{band}_2"],
358 df_params[f"theta_band_{band}_1"]) for band in unique_bands]
360 amp = [a[0] for a in amplitude_band]
361 ph = [p[0] for p in phase_bands]
363 return amp, ph
365 @catchWarnings(warns=["All-NaN slice encountered"])
366 def calculate(self,
367 diaObjects,
368 diaSources,
369 **kwargs):
370 """Compute the multi-band LombScargle periodogram of a DiaObject given
371 a set of DiaSources.
373 Parameters
374 ----------
375 diaObjects : `pandas.DataFrame`
376 Summary objects to store values in.
377 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
378 Catalog of DiaSources summarized by this DiaObject.
379 **kwargs : `dict`
380 Unused kwargs that are always passed to a plugin.
381 """
383 bands_arr = diaSources['band'].unique().values
384 unique_bands = np.unique(np.concatenate(bands_arr))
385 # Check and initialize output columns in diaObjects.
386 if (periodCol := "multiPeriod") not in diaObjects.columns:
387 diaObjects[periodCol] = np.nan
388 if (powerCol := "multiPower") not in diaObjects.columns:
389 diaObjects[powerCol] = np.nan
390 if (fapCol := "multiFap") not in diaObjects.columns:
391 diaObjects[fapCol] = np.nan
392 ampCol = "multiAmp"
393 phaseCol = "multiPhase"
394 for i in range(len(unique_bands)):
395 ampCol_band = f"{unique_bands[i]}_{ampCol}"
396 if ampCol_band not in diaObjects.columns:
397 diaObjects[ampCol_band] = np.nan
398 phaseCol_band = f"{unique_bands[i]}_{phaseCol}"
399 if phaseCol_band not in diaObjects.columns:
400 diaObjects[phaseCol_band] = np.nan
402 def _calculate_period_multi(df, all_unique_bands,
403 min_detections=9, oversampling_factor=5, nyquist_factor=100):
404 """Calculate the multi-band Lomb-Scargle periodogram.
406 Parameters
407 ----------
408 df : `pandas.DataFrame`
409 The input DataFrame.
410 all_unique_bands : `list` of `str`
411 List of all bands present in the diaSource table that is being worked on.
412 min_detections : `int`, optional
413 The minimum number of detections, including all bands.
414 oversampling_factor : `int`, optional
415 The oversampling factor for frequency grid.
416 nyquist_factor : `int`, optional
417 The Nyquist factor for frequency grid.
419 Returns
420 -------
421 pd_tab : `pandas.Series`
422 The output DataFrame with the Lomb-Scargle parameters.
423 """
424 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
425 np.isnan(df["midpointMjdTai"]))]
427 if (len(tmpDf)) < min_detections:
428 pd_tab_nodet = pd.Series({periodCol: np.nan,
429 powerCol: np.nan,
430 fapCol: np.nan})
431 for band in all_unique_bands:
432 pd_tab_nodet[f"{band}_{ampCol}"] = np.nan
433 pd_tab_nodet[f"{band}_{phaseCol}"] = np.nan
435 return pd_tab_nodet
437 time = tmpDf["midpointMjdTai"].to_numpy()
438 flux = tmpDf["psfFlux"].to_numpy()
439 flux_err = tmpDf["psfFluxErr"].to_numpy()
440 bands = tmpDf["band"].to_numpy()
442 lsp = LombScargleMultiband(time, flux, bands, dy=flux_err,
443 nterms_base=1, nterms_band=1)
445 f_grid = compute_optimized_periodogram_grid(
446 time, oversampling_factor=oversampling_factor, nyquist_factor=nyquist_factor)
447 period = 1/f_grid
448 power = lsp.power(f_grid)
450 fap_estimate = self.calculate_baluev_fap(
451 time, len(time), period[np.argmax(power)], np.max(power))
453 params_table_new = self.generate_lsp_params(lsp, f_grid[np.argmax(power)], bands)
455 pd_tab = pd.Series({periodCol: period[np.argmax(power)],
456 powerCol: np.max(power),
457 fapCol: fap_estimate
458 })
460 # Initialize the per-band amplitude/phase columns as NaNs
461 for band in all_unique_bands:
462 pd_tab[f"{band}_{ampCol}"] = np.nan
463 pd_tab[f"{band}_{phaseCol}"] = np.nan
465 # Populate the values of only the bands that have data for this diaSource
466 unique_bands = np.unique(bands)
467 for i in range(len(unique_bands)):
468 pd_tab[f"{unique_bands[i]}_{ampCol}"] = params_table_new[0][i]
469 pd_tab[f"{unique_bands[i]}_{phaseCol}"] = params_table_new[1][i]
471 return pd_tab
473 columns_list = [periodCol, powerCol, fapCol]
474 for i in range(len(unique_bands)):
475 columns_list.append(f"{unique_bands[i]}_{ampCol}")
476 columns_list.append(f"{unique_bands[i]}_{phaseCol}")
478 diaObjects.loc[:, columns_list
479 ] = diaSources.apply(_calculate_period_multi, unique_bands)
482class UnphysicalDiaSourceSeparation(pipeBase.AlgorithmError):
483 """Raised if associated DiaSources are unphysically separated.
485 Parameters
486 ----------
487 separation : `float`
488 Observed separation in arseconds.
489 max_allowed_separation : `float`
490 Configured maximum separation in arcseconds.
491 """
493 def __init__(self, separation, max_allowed_separation) -> None:
494 self._separation = separation
495 self._max_allowed_separation = max_allowed_separation
496 super().__init__(f"Observed DiaSource separation {separation} exceeds allowed value of "
497 f"{max_allowed_separation}")
499 @property
500 def metadata(self) -> dict:
501 return {
502 "separation": self._separation,
503 "max_allowed_separation": self._max_allowed_separation,
504 }
507class MeanDiaPositionConfig(DiaObjectCalculationPluginConfig):
508 MaxAllowedDiaSourceSeparation = pexConfig.Field(
509 dtype=float,
510 default=3.0,
511 doc="Max allowed separation of associated DiaSources in arcsec. "
512 "Raises if unphysical separation is found. "
513 )
516@register("ap_meanPosition")
517class MeanDiaPosition(DiaObjectCalculationPlugin):
518 """Compute the mean position of a DiaObject given a set of DiaSources.
519 """
521 ConfigClass = MeanDiaPositionConfig
523 plugType = 'multi'
525 outputCols = ["ra", "dec"]
526 needsFilter = False
528 @classmethod
529 def getExecutionOrder(cls):
530 return cls.DEFAULT_CATALOGCALCULATION
532 def calculate(self, diaObjects, diaSources, **kwargs):
533 """Compute the mean ra/dec position of the diaObject given the
534 diaSource locations.
536 Parameters
537 ----------
538 diaObjects : `pandas.DataFrame`
539 Summary objects to store values in.
540 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
541 Catalog of DiaSources summarized by this DiaObject.
542 **kwargs
543 Any additional keyword arguments that may be passed to the plugin.
544 """
545 for outCol in self.outputCols:
546 if outCol not in diaObjects.columns:
547 diaObjects[outCol] = np.nan
549 def _computeMeanPos(df):
550 coords = list(geom.SpherePoint(src["ra"], src["dec"], geom.degrees)
551 for idx, src in df.iterrows())
552 aveCoord = geom.averageSpherePoint(coords)
554 # We don't want the DIAObject position to move due to misassociated sources
555 maxSep = max(aveCoord.separation(coord).asArcseconds() for coord in coords)
557 if maxSep > self.config.MaxAllowedDiaSourceSeparation:
558 raise UnphysicalDiaSourceSeparation(maxSep,
559 self.config.MaxAllowedDiaSourceSeparation)
561 return pd.Series({"ra": aveCoord.getRa().asDegrees(),
562 "dec": aveCoord.getDec().asDegrees()})
564 ans = diaSources.apply(_computeMeanPos)
565 typeSafePandasAssignment(diaObjects, ans, ["ra", "dec"])
568class HTMIndexDiaPositionConfig(DiaObjectCalculationPluginConfig):
570 htmLevel = pexConfig.Field(
571 dtype=int,
572 doc="Level of the HTM pixelization.",
573 default=20,
574 )
577@register("ap_HTMIndex")
578class HTMIndexDiaPosition(DiaObjectCalculationPlugin):
579 """Compute the mean position of a DiaObject given a set of DiaSources.
581 Notes
582 -----
583 This plugin was implemented to satisfy requirements of old APDB interface
584 which required ``pixelId`` column in DiaObject with HTM20 index. APDB
585 interface had migrated to not need that information, but we keep this
586 plugin in case it may be useful for something else.
587 """
588 ConfigClass = HTMIndexDiaPositionConfig
590 plugType = 'single'
592 inputCols = ["ra", "dec"]
593 outputCols = ["pixelId"]
594 needsFilter = False
596 def __init__(self, config, name, metadata):
597 DiaObjectCalculationPlugin.__init__(self, config, name, metadata)
598 self.pixelator = sphgeom.HtmPixelization(self.config.htmLevel)
600 @classmethod
601 def getExecutionOrder(cls):
602 return cls.FLUX_MOMENTS_CALCULATED
604 def calculate(self, diaObjects, diaObjectId, **kwargs):
605 """Compute the mean position of a DiaObject given a set of DiaSources
607 Parameters
608 ----------
609 diaObjects : `pandas.dataFrame`
610 Summary objects to store values in and read ra/dec from.
611 diaObjectId : `int`
612 Id of the diaObject to update.
613 **kwargs
614 Any additional keyword arguments that may be passed to the plugin.
615 """
616 sphPoint = geom.SpherePoint(
617 diaObjects.at[diaObjectId, "ra"] * geom.degrees,
618 diaObjects.at[diaObjectId, "dec"] * geom.degrees)
619 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index(
620 sphPoint.getVector())
623class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig):
624 pass
627@register("ap_nDiaSources")
628class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin):
629 """Compute the total number of DiaSources associated with this DiaObject.
630 """
632 ConfigClass = NumDiaSourcesDiaPluginConfig
633 outputCols = ["nDiaSources"]
634 plugType = "multi"
635 needsFilter = False
637 @classmethod
638 def getExecutionOrder(cls):
639 return cls.DEFAULT_CATALOGCALCULATION
641 def calculate(self, diaObjects, diaSources, **kwargs):
642 """Compute the total number of DiaSources associated with this DiaObject.
644 Parameters
645 ----------
646 diaObject : `dict`
647 Summary object to store values in and read ra/dec from.
648 **kwargs
649 Any additional keyword arguments that may be passed to the plugin.
650 """
651 typeSafePandasAssignment(diaObjects, diaSources.diaObjectId.count(), ["nDiaSources"])
654class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig):
655 pass
658@register("ap_diaObjectFlag")
659class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin):
660 """Find if any DiaSource is flagged.
662 Set the DiaObject flag if any DiaSource is flagged.
663 """
665 ConfigClass = NumDiaSourcesDiaPluginConfig
666 outputCols = ["flags"]
667 plugType = "multi"
668 needsFilter = False
670 @classmethod
671 def getExecutionOrder(cls):
672 return cls.DEFAULT_CATALOGCALCULATION
674 def calculate(self, diaObjects, diaSources, **kwargs):
675 """Find if any DiaSource is flagged.
677 Set the DiaObject flag if any DiaSource is flagged.
679 Parameters
680 ----------
681 diaObject : `dict`
682 Summary object to store values in and read ra/dec from.
683 **kwargs
684 Any additional keyword arguments that may be passed to the plugin.
685 """
686 typeSafePandasAssignment(diaObjects, diaSources.flags.any(), ["flags"], default_dtype=np.uint64)
689class WeightedMeanDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
690 pass
693@register("ap_meanFlux")
694class WeightedMeanDiaPsfFlux(DiaObjectCalculationPlugin):
695 """Compute the weighted mean and mean error on the point source fluxes
696 of the DiaSource measured on the difference image.
698 Additionally store number of usable data points.
699 """
701 ConfigClass = WeightedMeanDiaPsfFluxConfig
702 outputCols = ["psfFluxMean", "psfFluxMeanErr", "psfFluxNdata"]
703 plugType = "multi"
704 needsFilter = True
706 @classmethod
707 def getExecutionOrder(cls):
708 return cls.DEFAULT_CATALOGCALCULATION
710 @catchWarnings(warns=["invalid value encountered",
711 "divide by zero"])
712 def calculate(self,
713 diaObjects,
714 diaSources,
715 filterDiaSources,
716 band,
717 **kwargs):
718 """Compute the weighted mean and mean error of the point source flux.
720 Parameters
721 ----------
722 diaObject : `dict`
723 Summary object to store values in.
724 diaSources : `pandas.DataFrame`
725 DataFrame representing all diaSources associated with this
726 diaObject.
727 filterDiaSources : `pandas.DataFrame`
728 DataFrame representing diaSources associated with this
729 diaObject that are observed in the band pass ``band``.
730 band : `str`
731 Simple, string name of the filter for the flux being calculated.
732 **kwargs
733 Any additional keyword arguments that may be passed to the plugin.
734 """
735 meanName = "{}_psfFluxMean".format(band)
736 errName = "{}_psfFluxMeanErr".format(band)
737 nDataName = "{}_psfFluxNdata".format(band)
738 if meanName not in diaObjects.columns:
739 diaObjects[meanName] = np.nan
740 if errName not in diaObjects.columns:
741 diaObjects[errName] = np.nan
742 if nDataName not in diaObjects.columns:
743 diaObjects[nDataName] = 0
745 def _weightedMean(df):
746 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
747 np.isnan(df["psfFluxErr"]))]
748 tot_weight = np.nansum(1 / tmpDf["psfFluxErr"] ** 2)
749 fluxMean = np.nansum(tmpDf["psfFlux"]
750 / tmpDf["psfFluxErr"] ** 2)
751 fluxMean /= tot_weight
752 if tot_weight > 0:
753 fluxMeanErr = np.sqrt(1 / tot_weight)
754 else:
755 fluxMeanErr = np.nan
756 nFluxData = len(tmpDf)
758 return pd.Series({meanName: fluxMean,
759 errName: fluxMeanErr,
760 nDataName: nFluxData},
761 dtype="object")
762 df = filterDiaSources.apply(_weightedMean).astype(diaObjects.dtypes[[meanName, errName, nDataName]])
763 # TODO DM-53254: Remove the force_int_to_float hack.
764 typeSafePandasAssignment(diaObjects, df, [meanName, errName, nDataName], force_int_to_float=True)
767class PercentileDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
768 percentiles = pexConfig.ListField(
769 dtype=int,
770 default=[5, 25, 50, 75, 95],
771 doc="Percentiles to calculate to compute values for. Should be "
772 "integer values."
773 )
776@register("ap_percentileFlux")
777class PercentileDiaPsfFlux(DiaObjectCalculationPlugin):
778 """Compute percentiles of diaSource fluxes.
779 """
781 ConfigClass = PercentileDiaPsfFluxConfig
782 # Output columns are created upon instantiation of the class.
783 outputCols = []
784 plugType = "multi"
785 needsFilter = True
787 def __init__(self, config, name, metadata, **kwargs):
788 DiaObjectCalculationPlugin.__init__(self,
789 config,
790 name,
791 metadata,
792 **kwargs)
793 self.outputCols = ["psfFluxPercentile{:02d}".format(percent)
794 for percent in self.config.percentiles]
796 @classmethod
797 def getExecutionOrder(cls):
798 return cls.DEFAULT_CATALOGCALCULATION
800 @catchWarnings(warns=["All-NaN slice encountered"])
801 def calculate(self,
802 diaObjects,
803 diaSources,
804 filterDiaSources,
805 band,
806 **kwargs):
807 """Compute the percentile fluxes of the point source flux.
809 Parameters
810 ----------
811 diaObject : `dict`
812 Summary object to store values in.
813 diaSources : `pandas.DataFrame`
814 DataFrame representing all diaSources associated with this
815 diaObject.
816 filterDiaSources : `pandas.DataFrame`
817 DataFrame representing diaSources associated with this
818 diaObject that are observed in the band pass ``band``.
819 band : `str`
820 Simple, string name of the filter for the flux being calculated.
821 **kwargs
822 Any additional keyword arguments that may be passed to the plugin.
823 """
824 pTileNames = []
825 for tilePercent in self.config.percentiles:
826 pTileName = "{}_psfFluxPercentile{:02d}".format(band,
827 tilePercent)
828 pTileNames.append(pTileName)
829 if pTileName not in diaObjects.columns:
830 diaObjects[pTileName] = np.nan
832 def _fluxPercentiles(df):
833 pTiles = np.nanpercentile(df["psfFlux"], self.config.percentiles)
834 return pd.Series(
835 dict((tileName, pTile)
836 for tileName, pTile in zip(pTileNames, pTiles)))
838 typeSafePandasAssignment(
839 diaObjects,
840 filterDiaSources.apply(_fluxPercentiles),
841 pTileNames,
842 )
845class SigmaDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
846 pass
849@register("ap_sigmaFlux")
850class SigmaDiaPsfFlux(DiaObjectCalculationPlugin):
851 """Compute scatter of diaSource fluxes.
852 """
854 ConfigClass = SigmaDiaPsfFluxConfig
855 # Output columns are created upon instantiation of the class.
856 outputCols = ["psfFluxSigma"]
857 plugType = "multi"
858 needsFilter = True
860 @classmethod
861 def getExecutionOrder(cls):
862 return cls.DEFAULT_CATALOGCALCULATION
864 def calculate(self,
865 diaObjects,
866 diaSources,
867 filterDiaSources,
868 band,
869 **kwargs):
870 """Compute the sigma fluxes of the point source flux.
872 Parameters
873 ----------
874 diaObject : `dict`
875 Summary object to store values in.
876 diaSources : `pandas.DataFrame`
877 DataFrame representing all diaSources associated with this
878 diaObject.
879 filterDiaSources : `pandas.DataFrame`
880 DataFrame representing diaSources associated with this
881 diaObject that are observed in the band pass ``band``.
882 band : `str`
883 Simple, string name of the filter for the flux being calculated.
884 **kwargs
885 Any additional keyword arguments that may be passed to the plugin.
886 """
887 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
888 # estimator of scatter (i.e. 'N - 1' instead of 'N').
889 column = "{}_psfFluxSigma".format(band)
891 typeSafePandasAssignment(
892 diaObjects,
893 filterDiaSources.psfFlux.std(),
894 [column],
895 )
898class Chi2DiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
899 pass
902@register("ap_chi2Flux")
903class Chi2DiaPsfFlux(DiaObjectCalculationPlugin):
904 """Compute chi2 of diaSource fluxes.
905 """
907 ConfigClass = Chi2DiaPsfFluxConfig
909 # Required input Cols
910 inputCols = ["psfFluxMean"]
911 # Output columns are created upon instantiation of the class.
912 outputCols = ["psfFluxChi2"]
913 plugType = "multi"
914 needsFilter = True
916 @classmethod
917 def getExecutionOrder(cls):
918 return cls.FLUX_MOMENTS_CALCULATED
920 @catchWarnings(warns=["All-NaN slice encountered"])
921 def calculate(self,
922 diaObjects,
923 diaSources,
924 filterDiaSources,
925 band,
926 **kwargs):
927 """Compute the chi2 of the point source fluxes.
929 Parameters
930 ----------
931 diaObject : `dict`
932 Summary object to store values in.
933 diaSources : `pandas.DataFrame`
934 DataFrame representing all diaSources associated with this
935 diaObject.
936 filterDiaSources : `pandas.DataFrame`
937 DataFrame representing diaSources associated with this
938 diaObject that are observed in the band pass ``band``.
939 band : `str`
940 Simple, string name of the filter for the flux being calculated.
941 **kwargs
942 Any additional keyword arguments that may be passed to the plugin.
943 """
944 meanName = "{}_psfFluxMean".format(band)
945 column = "{}_psfFluxChi2".format(band)
947 def _chi2(df):
948 delta = (df["psfFlux"]
949 - diaObjects.at[df.diaObjectId.iat[0], meanName])
950 return np.nansum((delta / df["psfFluxErr"]) ** 2)
952 typeSafePandasAssignment(
953 diaObjects,
954 filterDiaSources.apply(_chi2),
955 [column],
956 )
959class MadDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
960 pass
963@register("ap_madFlux")
964class MadDiaPsfFlux(DiaObjectCalculationPlugin):
965 """Compute median absolute deviation of diaSource fluxes.
966 """
968 ConfigClass = MadDiaPsfFluxConfig
970 # Required input Cols
971 # Output columns are created upon instantiation of the class.
972 outputCols = ["psfFluxMAD"]
973 plugType = "multi"
974 needsFilter = True
976 @classmethod
977 def getExecutionOrder(cls):
978 return cls.DEFAULT_CATALOGCALCULATION
980 @catchWarnings(warns=["All-NaN slice encountered"])
981 def calculate(self,
982 diaObjects,
983 diaSources,
984 filterDiaSources,
985 band,
986 **kwargs):
987 """Compute the median absolute deviation of the point source fluxes.
989 Parameters
990 ----------
991 diaObject : `dict`
992 Summary object to store values in.
993 diaSources : `pandas.DataFrame`
994 DataFrame representing all diaSources associated with this
995 diaObject.
996 filterDiaSources : `pandas.DataFrame`
997 DataFrame representing diaSources associated with this
998 diaObject that are observed in the band pass ``band``.
999 band : `str`
1000 Simple, string name of the filter for the flux being calculated.
1001 **kwargs
1002 Any additional keyword arguments that may be passed to the plugin.
1003 """
1004 column = "{}_psfFluxMAD".format(band)
1006 typeSafePandasAssignment(
1007 diaObjects,
1008 filterDiaSources.psfFlux.apply(median_absolute_deviation, ignore_nan=True),
1009 [column],
1010 )
1013class SkewDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1014 pass
1017@register("ap_skewFlux")
1018class SkewDiaPsfFlux(DiaObjectCalculationPlugin):
1019 """Compute the skew of diaSource fluxes.
1020 """
1022 ConfigClass = SkewDiaPsfFluxConfig
1024 # Required input Cols
1025 # Output columns are created upon instantiation of the class.
1026 outputCols = ["psfFluxSkew"]
1027 plugType = "multi"
1028 needsFilter = True
1030 @classmethod
1031 def getExecutionOrder(cls):
1032 return cls.DEFAULT_CATALOGCALCULATION
1034 def calculate(self,
1035 diaObjects,
1036 diaSources,
1037 filterDiaSources,
1038 band,
1039 **kwargs):
1040 """Compute the skew of the point source fluxes.
1042 Parameters
1043 ----------
1044 diaObject : `dict`
1045 Summary object to store values in.
1046 diaSources : `pandas.DataFrame`
1047 DataFrame representing all diaSources associated with this
1048 diaObject.
1049 filterDiaSources : `pandas.DataFrame`
1050 DataFrame representing diaSources associated with this
1051 diaObject that are observed in the band pass ``band``.
1052 band : `str`
1053 Simple, string name of the filter for the flux being calculated.
1054 **kwargs
1055 Any additional keyword arguments that may be passed to the plugin.
1056 """
1057 column = "{}_psfFluxSkew".format(band)
1059 typeSafePandasAssignment(
1060 diaObjects,
1061 filterDiaSources.psfFlux.skew(),
1062 [column],
1063 )
1066class MinMaxDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1067 pass
1070@register("ap_minMaxFlux")
1071class MinMaxDiaPsfFlux(DiaObjectCalculationPlugin):
1072 """Compute min/max of diaSource fluxes.
1073 """
1075 ConfigClass = MinMaxDiaPsfFluxConfig
1077 # Required input Cols
1078 # Output columns are created upon instantiation of the class.
1079 outputCols = ["psfFluxMin", "psfFluxMax"]
1080 plugType = "multi"
1081 needsFilter = True
1083 @classmethod
1084 def getExecutionOrder(cls):
1085 return cls.DEFAULT_CATALOGCALCULATION
1087 def calculate(self,
1088 diaObjects,
1089 diaSources,
1090 filterDiaSources,
1091 band,
1092 **kwargs):
1093 """Compute min/max of the point source fluxes.
1095 Parameters
1096 ----------
1097 diaObject : `dict`
1098 Summary object to store values in.
1099 diaSources : `pandas.DataFrame`
1100 DataFrame representing all diaSources associated with this
1101 diaObject.
1102 filterDiaSources : `pandas.DataFrame`
1103 DataFrame representing diaSources associated with this
1104 diaObject that are observed in the band pass ``band``.
1105 band : `str`
1106 Simple, string name of the filter for the flux being calculated.
1107 **kwargs
1108 Any additional keyword arguments that may be passed to the plugin.
1109 """
1110 minName = "{}_psfFluxMin".format(band)
1111 if minName not in diaObjects.columns:
1112 diaObjects[minName] = np.nan
1113 maxName = "{}_psfFluxMax".format(band)
1114 if maxName not in diaObjects.columns:
1115 diaObjects[maxName] = np.nan
1117 typeSafePandasAssignment(
1118 diaObjects,
1119 filterDiaSources.psfFlux.min(),
1120 [minName],
1121 )
1122 typeSafePandasAssignment(
1123 diaObjects,
1124 filterDiaSources.psfFlux.max(),
1125 [maxName],
1126 )
1129class MaxSlopeDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1130 pass
1133@register("ap_maxSlopeFlux")
1134class MaxSlopeDiaPsfFlux(DiaObjectCalculationPlugin):
1135 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
1136 """
1138 ConfigClass = MinMaxDiaPsfFluxConfig
1140 # Required input Cols
1141 # Output columns are created upon instantiation of the class.
1142 outputCols = ["psfFluxMaxSlope"]
1143 plugType = "multi"
1144 needsFilter = True
1146 @classmethod
1147 def getExecutionOrder(cls):
1148 return cls.DEFAULT_CATALOGCALCULATION
1150 def calculate(self,
1151 diaObjects,
1152 diaSources,
1153 filterDiaSources,
1154 band,
1155 **kwargs):
1156 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
1158 Parameters
1159 ----------
1160 diaObject : `dict`
1161 Summary object to store values in.
1162 diaSources : `pandas.DataFrame`
1163 DataFrame representing all diaSources associated with this
1164 diaObject.
1165 filterDiaSources : `pandas.DataFrame`
1166 DataFrame representing diaSources associated with this
1167 diaObject that are observed in the band pass ``band``.
1168 band : `str`
1169 Simple, string name of the filter for the flux being calculated.
1170 **kwargs
1171 Any additional keyword arguments that may be passed to the plugin.
1172 """
1174 def _maxSlope(df):
1175 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
1176 np.isnan(df["midpointMjdTai"]))]
1177 if len(tmpDf) < 2:
1178 return np.nan
1179 times = tmpDf["midpointMjdTai"].to_numpy()
1180 timeArgs = times.argsort()
1181 times = times[timeArgs]
1182 fluxes = tmpDf["psfFlux"].to_numpy()[timeArgs]
1183 return (np.diff(fluxes) / np.diff(times)).max()
1185 column = "{}_psfFluxMaxSlope".format(band)
1187 typeSafePandasAssignment(
1188 diaObjects,
1189 filterDiaSources.apply(_maxSlope),
1190 [column],
1191 )
1194class ErrMeanDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1195 pass
1198@register("ap_meanErrFlux")
1199class ErrMeanDiaPsfFlux(DiaObjectCalculationPlugin):
1200 """Compute the mean of the dia source errors.
1201 """
1203 ConfigClass = ErrMeanDiaPsfFluxConfig
1205 # Required input Cols
1206 # Output columns are created upon instantiation of the class.
1207 outputCols = ["psfFluxErrMean"]
1208 plugType = "multi"
1209 needsFilter = True
1211 @classmethod
1212 def getExecutionOrder(cls):
1213 return cls.DEFAULT_CATALOGCALCULATION
1215 def calculate(self,
1216 diaObjects,
1217 diaSources,
1218 filterDiaSources,
1219 band,
1220 **kwargs):
1221 """Compute the mean of the dia source errors.
1223 Parameters
1224 ----------
1225 diaObject : `dict`
1226 Summary object to store values in.
1227 diaSources : `pandas.DataFrame`
1228 DataFrame representing all diaSources associated with this
1229 diaObject.
1230 filterDiaSources : `pandas.DataFrame`
1231 DataFrame representing diaSources associated with this
1232 diaObject that are observed in the band pass ``band``.
1233 band : `str`
1234 Simple, string name of the filter for the flux being calculated.
1235 **kwargs
1236 Any additional keyword arguments that may be passed to the plugin.
1237 """
1238 column = "{}_psfFluxErrMean".format(band)
1240 typeSafePandasAssignment(
1241 diaObjects,
1242 filterDiaSources.psfFluxErr.mean(),
1243 [column],
1244 # Note that the schemas expect this to be single-precision.
1245 default_dtype=np.float32,
1246 )
1249class LinearFitDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1250 pass
1253@register("ap_linearFit")
1254class LinearFitDiaPsfFlux(DiaObjectCalculationPlugin):
1255 """Compute fit a linear model to flux vs time.
1256 """
1258 ConfigClass = LinearFitDiaPsfFluxConfig
1260 # Required input Cols
1261 # Output columns are created upon instantiation of the class.
1262 outputCols = ["psfFluxLinearSlope", "psfFluxLinearIntercept"]
1263 plugType = "multi"
1264 needsFilter = True
1266 @classmethod
1267 def getExecutionOrder(cls):
1268 return cls.DEFAULT_CATALOGCALCULATION
1270 def calculate(self,
1271 diaObjects,
1272 diaSources,
1273 filterDiaSources,
1274 band,
1275 **kwargs):
1276 """Compute fit a linear model to flux vs time.
1278 Parameters
1279 ----------
1280 diaObject : `dict`
1281 Summary object to store values in.
1282 diaSources : `pandas.DataFrame`
1283 DataFrame representing all diaSources associated with this
1284 diaObject.
1285 filterDiaSources : `pandas.DataFrame`
1286 DataFrame representing diaSources associated with this
1287 diaObject that are observed in the band pass ``band``.
1288 band : `str`
1289 Simple, string name of the filter for the flux being calculated.
1290 **kwargs
1291 Any additional keyword arguments that may be passed to the plugin.
1292 """
1294 mName = "{}_psfFluxLinearSlope".format(band)
1295 if mName not in diaObjects.columns:
1296 diaObjects[mName] = np.nan
1297 bName = "{}_psfFluxLinearIntercept".format(band)
1298 if bName not in diaObjects.columns:
1299 diaObjects[bName] = np.nan
1300 dtype = diaObjects[mName].dtype
1302 def _linearFit(df):
1303 tmpDf = df[~np.logical_or(
1304 np.isnan(df["psfFlux"]),
1305 np.logical_or(np.isnan(df["psfFluxErr"]),
1306 np.isnan(df["midpointMjdTai"])))]
1307 if len(tmpDf) < 2:
1308 return pd.Series({mName: np.nan, bName: np.nan})
1309 fluxes = tmpDf["psfFlux"].to_numpy()
1310 errors = tmpDf["psfFluxErr"].to_numpy()
1311 times = tmpDf["midpointMjdTai"].to_numpy()
1312 A = np.array([times / errors, 1 / errors]).transpose()
1313 m, b = lsq_linear(A, fluxes / errors).x
1314 return pd.Series({mName: m, bName: b}, dtype=dtype)
1316 typeSafePandasAssignment(
1317 diaObjects,
1318 filterDiaSources.apply(_linearFit),
1319 [mName, bName],
1320 )
1323class StetsonJDiaPsfFluxConfig(DiaObjectCalculationPluginConfig):
1324 pass
1327@register("ap_stetsonJ")
1328class StetsonJDiaPsfFlux(DiaObjectCalculationPlugin):
1329 """Compute the StetsonJ statistic on the DIA point source fluxes.
1330 """
1332 ConfigClass = LinearFitDiaPsfFluxConfig
1334 # Required input Cols
1335 inputCols = ["psfFluxMean"]
1336 # Output columns are created upon instantiation of the class.
1337 outputCols = ["psfFluxStetsonJ"]
1338 plugType = "multi"
1339 needsFilter = True
1341 @classmethod
1342 def getExecutionOrder(cls):
1343 return cls.FLUX_MOMENTS_CALCULATED
1345 def calculate(self,
1346 diaObjects,
1347 diaSources,
1348 filterDiaSources,
1349 band,
1350 **kwargs):
1351 """Compute the StetsonJ statistic on the DIA point source fluxes.
1353 Parameters
1354 ----------
1355 diaObject : `dict`
1356 Summary object to store values in.
1357 diaSources : `pandas.DataFrame`
1358 DataFrame representing all diaSources associated with this
1359 diaObject.
1360 filterDiaSources : `pandas.DataFrame`
1361 DataFrame representing diaSources associated with this
1362 diaObject that are observed in the band pass ``band``.
1363 band : `str`
1364 Simple, string name of the filter for the flux being calculated.
1365 **kwargs
1366 Any additional keyword arguments that may be passed to the plugin.
1367 """
1368 meanName = "{}_psfFluxMean".format(band)
1370 def _stetsonJ(df):
1371 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
1372 np.isnan(df["psfFluxErr"]))]
1373 if len(tmpDf) < 2:
1374 return np.nan
1375 fluxes = tmpDf["psfFlux"].to_numpy()
1376 errors = tmpDf["psfFluxErr"].to_numpy()
1378 return self._stetson_J(
1379 fluxes,
1380 errors,
1381 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
1383 column = "{}_psfFluxStetsonJ".format(band)
1384 typeSafePandasAssignment(
1385 diaObjects,
1386 filterDiaSources.apply(_stetsonJ),
1387 [column],
1388 )
1390 def _stetson_J(self, fluxes, errors, mean=None):
1391 """Compute the single band stetsonJ statistic.
1393 Parameters
1394 ----------
1395 fluxes : `numpy.ndarray` (N,)
1396 Calibrated lightcurve flux values.
1397 errors : `numpy.ndarray` (N,)
1398 Errors on the calibrated lightcurve fluxes.
1399 mean : `float`
1400 Starting mean from previous plugin.
1402 Returns
1403 -------
1404 stetsonJ : `float`
1405 stetsonJ statistic for the input fluxes and errors.
1407 References
1408 ----------
1409 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
1410 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
1411 """
1412 n_points = len(fluxes)
1413 flux_mean = self._stetson_mean(fluxes, errors, mean)
1414 delta_val = (
1415 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
1416 p_k = delta_val ** 2 - 1
1418 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
1420 def _stetson_mean(self,
1421 values,
1422 errors,
1423 mean=None,
1424 alpha=2.,
1425 beta=2.,
1426 n_iter=20,
1427 tol=1e-6):
1428 """Compute the stetson mean of the fluxes which down-weights outliers.
1430 Weighted biased on an error weighted difference scaled by a constant
1431 (1/``a``) and raised to the power beta. Higher betas more harshly
1432 penalize outliers and ``a`` sets the number of sigma where a weighted
1433 difference of 1 occurs.
1435 Parameters
1436 ----------
1437 values : `numpy.dnarray`, (N,)
1438 Input values to compute the mean of.
1439 errors : `numpy.ndarray`, (N,)
1440 Errors on the input values.
1441 mean : `float`
1442 Starting mean value or None.
1443 alpha : `float`
1444 Scalar down-weighting of the fractional difference. lower->more
1445 clipping. (Default value is 2.)
1446 beta : `float`
1447 Power law slope of the used to down-weight outliers. higher->more
1448 clipping. (Default value is 2.)
1449 n_iter : `int`
1450 Number of iterations of clipping.
1451 tol : `float`
1452 Fractional and absolute tolerance goal on the change in the mean
1453 before exiting early. (Default value is 1e-6)
1455 Returns
1456 -------
1457 mean : `float`
1458 Weighted stetson mean result.
1460 References
1461 ----------
1462 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
1463 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
1464 """
1465 n_points = len(values)
1466 n_factor = np.sqrt(n_points / (n_points - 1))
1467 inv_var = 1 / errors ** 2
1469 if mean is None:
1470 mean = np.average(values, weights=inv_var)
1471 for iter_idx in range(n_iter):
1472 chi = np.fabs(n_factor * (values - mean) / errors)
1473 tmp_mean = np.average(
1474 values,
1475 weights=inv_var / (1 + (chi / alpha) ** beta))
1476 diff = np.fabs(tmp_mean - mean)
1477 mean = tmp_mean
1478 if diff / mean < tol and diff < tol:
1479 break
1480 return mean
1483class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1484 pass
1487@register("ap_meanTotFlux")
1488class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin):
1489 """Compute the weighted mean and mean error on the point source fluxes
1490 forced photometered at the DiaSource location in the calibrated image.
1492 Additionally store number of usable data points.
1493 """
1495 ConfigClass = WeightedMeanDiaPsfFluxConfig
1496 outputCols = ["scienceFluxMean", "scienceFluxMeanErr"]
1497 plugType = "multi"
1498 needsFilter = True
1500 @classmethod
1501 def getExecutionOrder(cls):
1502 return cls.DEFAULT_CATALOGCALCULATION
1504 @catchWarnings(warns=["invalid value encountered",
1505 "divide by zero"])
1506 def calculate(self,
1507 diaObjects,
1508 diaSources,
1509 filterDiaSources,
1510 band,
1511 **kwargs):
1512 """Compute the weighted mean and mean error of the point source flux.
1514 Parameters
1515 ----------
1516 diaObject : `dict`
1517 Summary object to store values in.
1518 diaSources : `pandas.DataFrame`
1519 DataFrame representing all diaSources associated with this
1520 diaObject.
1521 filterDiaSources : `pandas.DataFrame`
1522 DataFrame representing diaSources associated with this
1523 diaObject that are observed in the band pass ``band``.
1524 band : `str`
1525 Simple, string name of the filter for the flux being calculated.
1526 **kwargs
1527 Any additional keyword arguments that may be passed to the plugin.
1528 """
1529 totMeanName = "{}_scienceFluxMean".format(band)
1530 if totMeanName not in diaObjects.columns:
1531 diaObjects[totMeanName] = np.nan
1532 totErrName = "{}_scienceFluxMeanErr".format(band)
1533 if totErrName not in diaObjects.columns:
1534 diaObjects[totErrName] = np.nan
1536 def _meanFlux(df):
1537 tmpDf = df[~np.logical_or(np.isnan(df["scienceFlux"]),
1538 np.isnan(df["scienceFluxErr"]))]
1539 tot_weight = np.nansum(1 / tmpDf["scienceFluxErr"] ** 2)
1540 fluxMean = np.nansum(tmpDf["scienceFlux"]
1541 / tmpDf["scienceFluxErr"] ** 2)
1542 fluxMean /= tot_weight
1543 fluxMeanErr = np.sqrt(1 / tot_weight)
1545 return pd.Series({totMeanName: fluxMean,
1546 totErrName: fluxMeanErr})
1548 df = filterDiaSources.apply(_meanFlux).astype(diaObjects.dtypes[[totMeanName, totErrName]])
1549 typeSafePandasAssignment(diaObjects, df, [totMeanName, totErrName])
1552class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig):
1553 pass
1556@register("ap_sigmaTotFlux")
1557class SigmaDiaTotFlux(DiaObjectCalculationPlugin):
1558 """Compute scatter of diaSource fluxes.
1559 """
1561 ConfigClass = SigmaDiaPsfFluxConfig
1562 # Output columns are created upon instantiation of the class.
1563 outputCols = ["scienceFluxSigma"]
1564 plugType = "multi"
1565 needsFilter = True
1567 @classmethod
1568 def getExecutionOrder(cls):
1569 return cls.DEFAULT_CATALOGCALCULATION
1571 def calculate(self,
1572 diaObjects,
1573 diaSources,
1574 filterDiaSources,
1575 band,
1576 **kwargs):
1577 """Compute the sigma fluxes of the point source flux measured on the
1578 calibrated image.
1580 Parameters
1581 ----------
1582 diaObject : `dict`
1583 Summary object to store values in.
1584 diaSources : `pandas.DataFrame`
1585 DataFrame representing all diaSources associated with this
1586 diaObject.
1587 filterDiaSources : `pandas.DataFrame`
1588 DataFrame representing diaSources associated with this
1589 diaObject that are observed in the band pass ``band``.
1590 band : `str`
1591 Simple, string name of the filter for the flux being calculated.
1592 **kwargs
1593 Any additional keyword arguments that may be passed to the plugin.
1594 """
1595 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1596 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1597 column = "{}_scienceFluxSigma".format(band)
1598 typeSafePandasAssignment(diaObjects, filterDiaSources.scienceFlux.std(), [column])