lsst.meas.base ga44f29b7aa+21155385c5
Loading...
Searching...
No Matches
diaCalculationPlugins.py
Go to the documentation of this file.
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/>.
21
22"""Plugins for use in DiaSource summary statistics.
23
24Output columns must be
25as defined in the schema of the Apdb both in name and units.
26"""
27
28import functools
29import warnings
30
31from astropy.stats import median_absolute_deviation
32import numpy as np
33import pandas as pd
34from scipy.optimize import lsq_linear
35
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
44
45from .diaCalculation import (
46 DiaObjectCalculationPluginConfig,
47 DiaObjectCalculationPlugin)
48from .pluginRegistry import register
49
50
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")
71
72
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
84
85 if _func is None:
86 return decoratorCatchWarnings
87 else:
88 return decoratorCatchWarnings(_func)
89
90
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.
102
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]
127
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
134
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.
142
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)
150
151
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.
155
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.
164
165 Returns
166 -------
167 frequencies : `array`
168 The computed optimized periodogram frequency grid.
169 """
170
171 num_points = len(x0)
172 baseline = np.max(x0) - np.min(x0)
173
174 # Calculate the frequency resolution based on oversampling factor and baseline
175 frequency_resolution = 1. / baseline / oversampling_factor
176
177 num_frequencies = int(
178 0.5 * oversampling_factor * nyquist_factor * num_points)
179 frequencies = frequency_resolution + \
180 frequency_resolution * np.arange(num_frequencies)
181
182 return frequencies
183
184
186 pass
187
188
189@register("ap_lombScarglePeriodogram")
191 """Compute the single-band period of a DiaObject given a set of DiaSources.
192 """
193 ConfigClass = LombScarglePeriodogramConfig
194
195 plugType = "multi"
196 outputCols = ["period", "power"]
197 needsFilter = True
198
199 @classmethod
202
203 @catchWarnings(warns=["All-NaN slice encountered"])
204 def calculate(self,
205 diaObjects,
206 diaSources,
207 filterDiaSources,
208 band):
209 """Compute the periodogram.
210
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 """
218
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
224
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.
227
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.
240
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"]))]
248
249 if len(tmpDf) < min_detections:
250 return pd.Series({periodCol: np.nan, powerCol: np.nan})
251
252 time = tmpDf["midpointMjdTai"].to_numpy()
253 flux = tmpDf["psfFlux"].to_numpy()
254 flux_err = tmpDf["psfFluxErr"].to_numpy()
255
256 lsp = LombScargle(time, flux, dy=flux_err, nterms=nterms)
258 time, oversampling_factor=oversampling_factor, nyquist_factor=nyquist_factor)
259 period = 1/f_grid
260 power = lsp.power(f_grid)
261
262 return pd.Series({periodCol: period[np.argmax(power)],
263 powerCol: np.max(power)})
264
265 diaObjects.loc[:, [periodCol, powerCol]
266 ] = filterDiaSources.apply(_calculate_period)
267
268
270 pass
271
272
273@register("ap_lombScarglePeriodogramMulti")
275 """Compute the multi-band LombScargle periodogram of a DiaObject given a set of DiaSources.
276 """
277 ConfigClass = LombScarglePeriodogramMultiConfig
278
279 plugType = "multi"
280 outputCols = ["multiPeriod", "multiPower",
281 "multiFap", "multiAmp", "multiPhase"]
282 needsFilter = True
283
284 @classmethod
287
288 @staticmethod
289 def calculate_baluev_fap(time, n, maxPeriod, zmax):
290 """Calculate the False-Alarm probability using the Baluev approximation.
291
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.
302
303 Returns
304 -------
305 fap_estimate : `float`
306 The False-Alarm probability Baluev approximation.
307
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
315
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)
321
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.
333
334 Returns
335 -------
336 Amp : `array`
337 The amplitude of the time series.
338 Ph : `array`
339 The phase of the time series.
340
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)
346
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)]
349
350 df_params = pd.DataFrame([best_params], columns=name_params)
351
352 unique_bands = np.unique(bands)
353
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]
359
360 amp = [a[0] for a in amplitude_band]
361 ph = [p[0] for p in phase_bands]
362
363 return amp, ph
364
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.
372
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 """
382
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
401
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.
405
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.
418
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"]))]
426
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
434
435 return pd_tab_nodet
436
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()
441
442 lsp = LombScargleMultiband(time, flux, bands, dy=flux_err,
443 nterms_base=1, nterms_band=1)
444
446 time, oversampling_factor=oversampling_factor, nyquist_factor=nyquist_factor)
447 period = 1/f_grid
448 power = lsp.power(f_grid)
449
450 fap_estimate = self.calculate_baluev_fap(
451 time, len(time), period[np.argmax(power)], np.max(power))
452
453 params_table_new = self.generate_lsp_params(lsp, f_grid[np.argmax(power)], bands)
454
455 pd_tab = pd.Series({periodCol: period[np.argmax(power)],
456 powerCol: np.max(power),
457 fapCol: fap_estimate
458 })
459
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
464
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]
470
471 return pd_tab
472
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}")
477
478 diaObjects.loc[:, columns_list
479 ] = diaSources.apply(_calculate_period_multi, unique_bands)
480
481
482class UnphysicalDiaSourceSeparation(pipeBase.AlgorithmError):
483 """Raised if associated DiaSources are unphysically separated.
484
485 Parameters
486 ----------
487 separation : `float`
488 Observed separation in arseconds.
489 max_allowed_separation : `float`
490 Configured maximum separation in arcseconds.
491 """
492
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}")
498
499 @property
500 def metadata(self) -> dict:
501 return {
502 "separation": self._separation,
503 "max_allowed_separation": self._max_allowed_separation,
504 }
505
506
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 )
514
515
516@register("ap_meanPosition")
518 """Compute the mean position and position uncertainty of a DiaObject
519 from its associated DiaSources.
520
521 The reported (ra, dec) is the inverse-variance weighted spherical
522 mean of the per-source positions, using only the DiaSources whose
523 per-source coordinate errors are finite. The reported covariance
524 is the covariance of that weighted-mean estimator,
525
526 C_formal = (sum_i inv(C_i))^-1,
527
528 where ``C_i`` is the per-source 2x2 covariance, multiplied by a
529 *chi-squared scale factor* that inflates the result when the
530 empirical scatter is larger than the per-source errors predict:
531
532 chi2 = sum_i r_i^T inv(C_i) r_i (r_i = tangent-plane
533 residual from the mean)
534 dof = 2 * (N - 1)
535 C_obj = max(1, chi2 / dof) * C_formal
536
537 When the data agree with the per-source errors (chi2 ~ dof) the
538 scale factor is unity and ``C_obj == C_formal``. When the residuals
539 exceed what the errors predict, the covariance is inflated.
540 """
541
542 ConfigClass = MeanDiaPositionConfig
543
544 plugType = 'multi'
545
546 outputCols = ["ra", "dec", "raErr", "decErr", "ra_dec_Cov"]
547 needsFilter = False
548
549 @classmethod
552
553 def calculate(self, diaObjects, diaSources, **kwargs):
554 """Compute the mean ra/dec position and its uncertainty for each
555 DiaObject.
556
557 Parameters
558 ----------
559 diaObjects : `pandas.DataFrame`
560 Summary objects to store values in.
561 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy`
562 Catalog of DiaSources summarized by this DiaObject.
563 **kwargs
564 Any additional keyword arguments that may be passed to the plugin.
565 """
566 for outCol in self.outputCols:
567 if outCol not in diaObjects.columns:
568 diaObjects[outCol] = np.nan
569
570 maxAllowedSep = self.config.MaxAllowedDiaSourceSeparation
571
572 def _column(df, name):
573 """Read a column as float64, or return all-NaN if absent."""
574 if name in df.columns:
575 return df[name].to_numpy(dtype=np.float64)
576 return np.full(len(df), np.nan)
577
578 def _computeMeanPos(df):
579 coords = list(geom.SpherePoint(src["ra"], src["dec"], geom.degrees)
580 for idx, src in df.iterrows())
581 # Quick check of the unweighted mean to catch unphysical objects
582 # and as a fallback.
583 unweightedAvg = geom.averageSpherePoint(coords)
584 maxSep = max(unweightedAvg.separation(coord).asArcseconds() for coord in coords)
585 if maxSep > maxAllowedSep:
586 raise UnphysicalDiaSourceSeparation(maxSep, maxAllowedSep)
587
588 nSrc = len(coords)
589 raErr = _column(df, "raErr")
590 decErr = _column(df, "decErr")
591 raDecCov = _column(df, "ra_dec_Cov")
592 finiteDiag = (np.isfinite(raErr) & (raErr > 0)) & (np.isfinite(decErr) & (decErr > 0))
593 nDiag = int(finiteDiag.sum())
594
595 # Tangent-plane offsets from the unweighted mean reference,
596 # in degrees (arc-length east, north).
597 offsets = np.array(
598 [[off.asDegrees() for off in unweightedAvg.getTangentPlaneOffset(coord)] for coord in coords]
599 ) if nSrc >= 1 else np.zeros((0, 2))
600
601 aveCoord = unweightedAvg
602 varRa = np.nan
603 varDec = np.nan
604 raDecCovObj = np.nan
605
606 if nDiag >= 1:
607 # Weighted-mean path: include only sources with finite
608 # per-source coordinate errors.
609 idx = np.where(finiteDiag)[0]
610 sigmaRaSq = raErr[idx]**2
611 sigmaDecSq = decErr[idx]**2
612 # Use full 2x2 weights only when every included source
613 # has a finite ra_dec_Cov; otherwise use diagonal weights
614 # for all of them and emit NaN for the output covariance.
615 haveFullCov = bool(np.isfinite(raDecCov[idx]).all())
616
617 C = np.zeros((nDiag, 2, 2), dtype=np.float64)
618 C[:, 0, 0] = sigmaRaSq
619 C[:, 1, 1] = sigmaDecSq
620 if haveFullCov:
621 C[:, 0, 1] = raDecCov[idx]
622 C[:, 1, 0] = raDecCov[idx]
623 try:
624 W = np.linalg.inv(C)
625 except np.linalg.LinAlgError:
626 # Singular per-source covariance: drop off-diagonal
627 # and retry with diagonal weights only.
628 haveFullCov = False
629 C[:, 0, 1] = 0.0
630 C[:, 1, 0] = 0.0
631 W = np.linalg.inv(C)
632
633 W_sum = W.sum(axis=0)
634 C_formal = np.linalg.inv(W_sum)
635
636 # Weighted-mean tangent-plane offset from the unweighted
637 # reference: mu_offset = (sum W_i)^-1 (sum W_i r_i). Then
638 # apply that offset to the unweighted mean to get the
639 # weighted spherical mean. SpherePoint.offset takes a
640 # bearing measured counter-clockwise from east (bearing 0
641 # is due east, bearing pi/2 is due north).
642 mu_offset = C_formal @ np.einsum('nij,nj->i', W, offsets[idx])
643 east, north = float(mu_offset[0]), float(mu_offset[1])
644 sep_deg = float(np.hypot(east, north))
645 if sep_deg > 0.0:
646 bearing = geom.Angle(float(np.arctan2(north, east)), geom.radians)
647 aveCoord = unweightedAvg.offset(bearing, sep_deg*geom.degrees)
648
649 varRaFormal = float(C_formal[0, 0])
650 varDecFormal = float(C_formal[1, 1])
651 covFormal = float(C_formal[0, 1]) if haveFullCov else np.nan
652
653 if nDiag == 1:
654 # Single included source: pass its own 2x2 covariance through.
655 varRa = varRaFormal
656 varDec = varDecFormal
657 raDecCovObj = covFormal
658 else:
659 # Inflate the variance if the reduced chi**2 fit of the
660 # diaSource coordinates is greater than 1, to account for
661 # scatter.
662 residuals = offsets[idx] - mu_offset
663 if haveFullCov:
664 # Vectorized Σₙ rₙᵀ Wₙ rₙ (r = residuals).
665 chi2 = float(np.einsum('ni,nij,nj->', residuals, W, residuals))
666 else:
667 chi2 = float(np.sum(residuals[:, 0]**2/sigmaRaSq + residuals[:, 1]**2/sigmaDecSq))
668 dof = 2*(nDiag - 1)
669 scale = max(1.0, chi2/dof)
670 varRa = varRaFormal*scale
671 varDec = varDecFormal*scale
672 raDecCovObj = (covFormal*scale if np.isfinite(covFormal) else np.nan)
673 else:
674 # No source has finite per-source coordinate errors. Fall
675 # back to the unweighted mean position plus the empirical
676 # SEM (when N >= 2), and warn that the per-source errors
677 # were not usable.
678 warnings.warn(
679 "No DiaSources with finite coordinate errors; falling "
680 "back to the unweighted mean position with scatter-only "
681 "uncertainty.",
682 stacklevel=2,
683 )
684 if nSrc >= 2:
685 sampleCov = np.cov(offsets, rowvar=False, ddof=1)
686 semCov = sampleCov/nSrc
687 varRa = float(semCov[0, 0])
688 varDec = float(semCov[1, 1])
689 raDecCovObj = float(semCov[0, 1])
690 # else: nSrc == 1 with no error -- outputs stay NaN.
691
692 raErrObj = float(np.sqrt(varRa)) if np.isfinite(varRa) else np.nan
693 decErrObj = float(np.sqrt(varDec)) if np.isfinite(varDec) else np.nan
694
695 return pd.Series({"ra": aveCoord.getRa().asDegrees(),
696 "dec": aveCoord.getDec().asDegrees(),
697 "raErr": raErrObj,
698 "decErr": decErrObj,
699 "ra_dec_Cov": raDecCovObj})
700
701 ans = diaSources.apply(_computeMeanPos)
702 typeSafePandasAssignment(diaObjects, ans, ["ra", "dec", "raErr", "decErr", "ra_dec_Cov"])
703
704
706
707 htmLevel = pexConfig.Field(
708 dtype=int,
709 doc="Level of the HTM pixelization.",
710 default=20,
711 )
712
713
714@register("ap_HTMIndex")
716 """Compute the mean position of a DiaObject given a set of DiaSources.
717
718 Notes
719 -----
720 This plugin was implemented to satisfy requirements of old APDB interface
721 which required ``pixelId`` column in DiaObject with HTM20 index. APDB
722 interface had migrated to not need that information, but we keep this
723 plugin in case it may be useful for something else.
724 """
725 ConfigClass = HTMIndexDiaPositionConfig
726
727 plugType = 'single'
728
729 inputCols = ["ra", "dec"]
730 outputCols = ["pixelId"]
731 needsFilter = False
732
733 def __init__(self, config, name, metadata):
734 DiaObjectCalculationPlugin.__init__(self, config, name, metadata)
736
737 @classmethod
739 return cls.FLUX_MOMENTS_CALCULATED
740
741 def calculate(self, diaObjects, diaObjectId, **kwargs):
742 """Compute the mean position of a DiaObject given a set of DiaSources
743
744 Parameters
745 ----------
746 diaObjects : `pandas.dataFrame`
747 Summary objects to store values in and read ra/dec from.
748 diaObjectId : `int`
749 Id of the diaObject to update.
750 **kwargs
751 Any additional keyword arguments that may be passed to the plugin.
752 """
753 sphPoint = geom.SpherePoint(
754 diaObjects.at[diaObjectId, "ra"] * geom.degrees,
755 diaObjects.at[diaObjectId, "dec"] * geom.degrees)
756 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index(
757 sphPoint.getVector())
758
759
761 pass
762
763
764@register("ap_nDiaSources")
766 """Compute the total number of DiaSources associated with this DiaObject.
767 """
768
769 ConfigClass = NumDiaSourcesDiaPluginConfig
770 outputCols = ["nDiaSources"]
771 plugType = "multi"
772 needsFilter = False
773
774 @classmethod
777
778 def calculate(self, diaObjects, diaSources, **kwargs):
779 """Compute the total number of DiaSources associated with this DiaObject.
780
781 Parameters
782 ----------
783 diaObject : `dict`
784 Summary object to store values in and read ra/dec from.
785 **kwargs
786 Any additional keyword arguments that may be passed to the plugin.
787 """
788 typeSafePandasAssignment(diaObjects, diaSources.diaObjectId.count(), ["nDiaSources"])
789
790
792 pass
793
794
795@register("ap_diaObjectFlag")
797 """Find if any DiaSource is flagged.
798
799 Set the DiaObject flag if any DiaSource is flagged.
800 """
801
802 ConfigClass = NumDiaSourcesDiaPluginConfig
803 outputCols = ["flags"]
804 plugType = "multi"
805 needsFilter = False
806
807 @classmethod
810
811 def calculate(self, diaObjects, diaSources, **kwargs):
812 """Find if any DiaSource is flagged.
813
814 Set the DiaObject flag if any DiaSource is flagged.
815
816 Parameters
817 ----------
818 diaObject : `dict`
819 Summary object to store values in and read ra/dec from.
820 **kwargs
821 Any additional keyword arguments that may be passed to the plugin.
822 """
823 typeSafePandasAssignment(diaObjects, diaSources.flags.any(), ["flags"], default_dtype=np.uint64)
824
825
832 """Compute the weighted mean and mean error on the point source fluxes
833 of the DiaSource measured on the difference image.
834
835 Additionally store number of usable data points.
836 """
837
838 ConfigClass = WeightedMeanDiaPsfFluxConfig
839 outputCols = ["psfFluxMean", "psfFluxMeanErr", "psfFluxNdata"]
840 plugType = "multi"
841 needsFilter = True
842
843 @classmethod
846
847 @catchWarnings(warns=["invalid value encountered",
848 "divide by zero"])
849 def calculate(self,
850 diaObjects,
851 diaSources,
852 filterDiaSources,
853 band,
854 **kwargs):
855 """Compute the weighted mean and mean error of the point source flux.
856
857 Parameters
858 ----------
859 diaObject : `dict`
860 Summary object to store values in.
861 diaSources : `pandas.DataFrame`
862 DataFrame representing all diaSources associated with this
863 diaObject.
864 filterDiaSources : `pandas.DataFrame`
865 DataFrame representing diaSources associated with this
866 diaObject that are observed in the band pass ``band``.
867 band : `str`
868 Simple, string name of the filter for the flux being calculated.
869 **kwargs
870 Any additional keyword arguments that may be passed to the plugin.
871 """
872 meanName = "{}_psfFluxMean".format(band)
873 errName = "{}_psfFluxMeanErr".format(band)
874 nDataName = "{}_psfFluxNdata".format(band)
875 if meanName not in diaObjects.columns:
876 diaObjects[meanName] = np.nan
877 if errName not in diaObjects.columns:
878 diaObjects[errName] = np.nan
879 if nDataName not in diaObjects.columns:
880 diaObjects[nDataName] = 0
881
882 def _weightedMean(df):
883 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
884 np.isnan(df["psfFluxErr"]))]
885 tot_weight = np.nansum(1 / tmpDf["psfFluxErr"] ** 2)
886 fluxMean = np.nansum(tmpDf["psfFlux"]
887 / tmpDf["psfFluxErr"] ** 2)
888 fluxMean /= tot_weight
889 if tot_weight > 0:
890 fluxMeanErr = np.sqrt(1 / tot_weight)
891 else:
892 fluxMeanErr = np.nan
893 nFluxData = len(tmpDf)
894
895 return pd.Series({meanName: fluxMean,
896 errName: fluxMeanErr,
897 nDataName: nFluxData},
898 dtype="object")
899 df = filterDiaSources.apply(_weightedMean).astype(diaObjects.dtypes[[meanName, errName, nDataName]])
900 # TODO DM-53254: Remove the force_int_to_float hack.
901 typeSafePandasAssignment(diaObjects, df, [meanName, errName, nDataName], force_int_to_float=True)
902
903
905 percentiles = pexConfig.ListField(
906 dtype=int,
907 default=[5, 25, 50, 75, 95],
908 doc="Percentiles to calculate to compute values for. Should be "
909 "integer values."
910 )
911
912
913@register("ap_percentileFlux")
915 """Compute percentiles of diaSource fluxes.
916 """
917
918 ConfigClass = PercentileDiaPsfFluxConfig
919 # Output columns are created upon instantiation of the class.
920 outputCols = []
921 plugType = "multi"
922 needsFilter = True
923
924 def __init__(self, config, name, metadata, **kwargs):
925 DiaObjectCalculationPlugin.__init__(self,
926 config,
927 name,
928 metadata,
929 **kwargs)
930 self.outputCols = ["psfFluxPercentile{:02d}".format(percent)
931 for percent in self.config.percentiles]
932
933 @classmethod
936
937 @catchWarnings(warns=["All-NaN slice encountered"])
938 def calculate(self,
939 diaObjects,
940 diaSources,
941 filterDiaSources,
942 band,
943 **kwargs):
944 """Compute the percentile fluxes of the point source flux.
945
946 Parameters
947 ----------
948 diaObject : `dict`
949 Summary object to store values in.
950 diaSources : `pandas.DataFrame`
951 DataFrame representing all diaSources associated with this
952 diaObject.
953 filterDiaSources : `pandas.DataFrame`
954 DataFrame representing diaSources associated with this
955 diaObject that are observed in the band pass ``band``.
956 band : `str`
957 Simple, string name of the filter for the flux being calculated.
958 **kwargs
959 Any additional keyword arguments that may be passed to the plugin.
960 """
961 pTileNames = []
962 for tilePercent in self.config.percentiles:
963 pTileName = "{}_psfFluxPercentile{:02d}".format(band,
964 tilePercent)
965 pTileNames.append(pTileName)
966 if pTileName not in diaObjects.columns:
967 diaObjects[pTileName] = np.nan
968
969 def _fluxPercentiles(df):
970 pTiles = np.nanpercentile(df["psfFlux"], self.config.percentiles)
971 return pd.Series(
972 dict((tileName, pTile)
973 for tileName, pTile in zip(pTileNames, pTiles)))
974
976 diaObjects,
977 filterDiaSources.apply(_fluxPercentiles),
978 pTileNames,
979 )
980
981
983 pass
984
985
986@register("ap_sigmaFlux")
988 """Compute scatter of diaSource fluxes.
989 """
990
991 ConfigClass = SigmaDiaPsfFluxConfig
992 # Output columns are created upon instantiation of the class.
993 outputCols = ["psfFluxSigma"]
994 plugType = "multi"
995 needsFilter = True
996
997 @classmethod
1000
1001 def calculate(self,
1002 diaObjects,
1003 diaSources,
1004 filterDiaSources,
1005 band,
1006 **kwargs):
1007 """Compute the sigma fluxes of the point source flux.
1008
1009 Parameters
1010 ----------
1011 diaObject : `dict`
1012 Summary object to store values in.
1013 diaSources : `pandas.DataFrame`
1014 DataFrame representing all diaSources associated with this
1015 diaObject.
1016 filterDiaSources : `pandas.DataFrame`
1017 DataFrame representing diaSources associated with this
1018 diaObject that are observed in the band pass ``band``.
1019 band : `str`
1020 Simple, string name of the filter for the flux being calculated.
1021 **kwargs
1022 Any additional keyword arguments that may be passed to the plugin.
1023 """
1024 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1025 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1026 column = "{}_psfFluxSigma".format(band)
1027
1029 diaObjects,
1030 filterDiaSources.psfFlux.std(),
1031 [column],
1032 )
1033
1034
1036 pass
1037
1038
1039@register("ap_chi2Flux")
1041 """Compute chi2 of diaSource fluxes.
1042 """
1043
1044 ConfigClass = Chi2DiaPsfFluxConfig
1045
1046 # Required input Cols
1047 inputCols = ["psfFluxMean"]
1048 # Output columns are created upon instantiation of the class.
1049 outputCols = ["psfFluxChi2"]
1050 plugType = "multi"
1051 needsFilter = True
1052
1053 @classmethod
1055 return cls.FLUX_MOMENTS_CALCULATED
1056
1057 @catchWarnings(warns=["All-NaN slice encountered"])
1058 def calculate(self,
1059 diaObjects,
1060 diaSources,
1061 filterDiaSources,
1062 band,
1063 **kwargs):
1064 """Compute the chi2 of the point source fluxes.
1065
1066 Parameters
1067 ----------
1068 diaObject : `dict`
1069 Summary object to store values in.
1070 diaSources : `pandas.DataFrame`
1071 DataFrame representing all diaSources associated with this
1072 diaObject.
1073 filterDiaSources : `pandas.DataFrame`
1074 DataFrame representing diaSources associated with this
1075 diaObject that are observed in the band pass ``band``.
1076 band : `str`
1077 Simple, string name of the filter for the flux being calculated.
1078 **kwargs
1079 Any additional keyword arguments that may be passed to the plugin.
1080 """
1081 meanName = "{}_psfFluxMean".format(band)
1082 column = "{}_psfFluxChi2".format(band)
1083
1084 def _chi2(df):
1085 delta = (df["psfFlux"]
1086 - diaObjects.at[df.diaObjectId.iat[0], meanName])
1087 return np.nansum((delta / df["psfFluxErr"]) ** 2)
1088
1090 diaObjects,
1091 filterDiaSources.apply(_chi2),
1092 [column],
1093 )
1094
1095
1097 pass
1098
1099
1100@register("ap_madFlux")
1102 """Compute median absolute deviation of diaSource fluxes.
1103 """
1104
1105 ConfigClass = MadDiaPsfFluxConfig
1106
1107 # Required input Cols
1108 # Output columns are created upon instantiation of the class.
1109 outputCols = ["psfFluxMAD"]
1110 plugType = "multi"
1111 needsFilter = True
1112
1113 @classmethod
1116
1117 @catchWarnings(warns=["All-NaN slice encountered"])
1118 def calculate(self,
1119 diaObjects,
1120 diaSources,
1121 filterDiaSources,
1122 band,
1123 **kwargs):
1124 """Compute the median absolute deviation of the point source fluxes.
1125
1126 Parameters
1127 ----------
1128 diaObject : `dict`
1129 Summary object to store values in.
1130 diaSources : `pandas.DataFrame`
1131 DataFrame representing all diaSources associated with this
1132 diaObject.
1133 filterDiaSources : `pandas.DataFrame`
1134 DataFrame representing diaSources associated with this
1135 diaObject that are observed in the band pass ``band``.
1136 band : `str`
1137 Simple, string name of the filter for the flux being calculated.
1138 **kwargs
1139 Any additional keyword arguments that may be passed to the plugin.
1140 """
1141 column = "{}_psfFluxMAD".format(band)
1142
1144 diaObjects,
1145 filterDiaSources.psfFlux.apply(median_absolute_deviation, ignore_nan=True),
1146 [column],
1147 )
1148
1149
1151 pass
1152
1153
1154@register("ap_skewFlux")
1156 """Compute the skew of diaSource fluxes.
1157 """
1158
1159 ConfigClass = SkewDiaPsfFluxConfig
1160
1161 # Required input Cols
1162 # Output columns are created upon instantiation of the class.
1163 outputCols = ["psfFluxSkew"]
1164 plugType = "multi"
1165 needsFilter = True
1166
1167 @classmethod
1170
1171 def calculate(self,
1172 diaObjects,
1173 diaSources,
1174 filterDiaSources,
1175 band,
1176 **kwargs):
1177 """Compute the skew of the point source fluxes.
1178
1179 Parameters
1180 ----------
1181 diaObject : `dict`
1182 Summary object to store values in.
1183 diaSources : `pandas.DataFrame`
1184 DataFrame representing all diaSources associated with this
1185 diaObject.
1186 filterDiaSources : `pandas.DataFrame`
1187 DataFrame representing diaSources associated with this
1188 diaObject that are observed in the band pass ``band``.
1189 band : `str`
1190 Simple, string name of the filter for the flux being calculated.
1191 **kwargs
1192 Any additional keyword arguments that may be passed to the plugin.
1193 """
1194 column = "{}_psfFluxSkew".format(band)
1195
1197 diaObjects,
1198 filterDiaSources.psfFlux.skew(),
1199 [column],
1200 )
1201
1202
1204 pass
1205
1206
1207@register("ap_minMaxFlux")
1209 """Compute min/max of diaSource fluxes.
1210 """
1211
1212 ConfigClass = MinMaxDiaPsfFluxConfig
1213
1214 # Required input Cols
1215 # Output columns are created upon instantiation of the class.
1216 outputCols = ["psfFluxMin", "psfFluxMax"]
1217 plugType = "multi"
1218 needsFilter = True
1219
1220 @classmethod
1223
1224 def calculate(self,
1225 diaObjects,
1226 diaSources,
1227 filterDiaSources,
1228 band,
1229 **kwargs):
1230 """Compute min/max of the point source fluxes.
1231
1232 Parameters
1233 ----------
1234 diaObject : `dict`
1235 Summary object to store values in.
1236 diaSources : `pandas.DataFrame`
1237 DataFrame representing all diaSources associated with this
1238 diaObject.
1239 filterDiaSources : `pandas.DataFrame`
1240 DataFrame representing diaSources associated with this
1241 diaObject that are observed in the band pass ``band``.
1242 band : `str`
1243 Simple, string name of the filter for the flux being calculated.
1244 **kwargs
1245 Any additional keyword arguments that may be passed to the plugin.
1246 """
1247 minName = "{}_psfFluxMin".format(band)
1248 if minName not in diaObjects.columns:
1249 diaObjects[minName] = np.nan
1250 maxName = "{}_psfFluxMax".format(band)
1251 if maxName not in diaObjects.columns:
1252 diaObjects[maxName] = np.nan
1253
1255 diaObjects,
1256 filterDiaSources.psfFlux.min(),
1257 [minName],
1258 )
1260 diaObjects,
1261 filterDiaSources.psfFlux.max(),
1262 [maxName],
1263 )
1264
1265
1267 pass
1268
1269
1270@register("ap_maxSlopeFlux")
1272 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
1273 """
1274
1275 ConfigClass = MinMaxDiaPsfFluxConfig
1276
1277 # Required input Cols
1278 # Output columns are created upon instantiation of the class.
1279 outputCols = ["psfFluxMaxSlope"]
1280 plugType = "multi"
1281 needsFilter = True
1282
1283 @classmethod
1286
1287 def calculate(self,
1288 diaObjects,
1289 diaSources,
1290 filterDiaSources,
1291 band,
1292 **kwargs):
1293 """Compute the maximum ratio time ordered deltaFlux / deltaTime.
1294
1295 Parameters
1296 ----------
1297 diaObject : `dict`
1298 Summary object to store values in.
1299 diaSources : `pandas.DataFrame`
1300 DataFrame representing all diaSources associated with this
1301 diaObject.
1302 filterDiaSources : `pandas.DataFrame`
1303 DataFrame representing diaSources associated with this
1304 diaObject that are observed in the band pass ``band``.
1305 band : `str`
1306 Simple, string name of the filter for the flux being calculated.
1307 **kwargs
1308 Any additional keyword arguments that may be passed to the plugin.
1309 """
1310
1311 def _maxSlope(df):
1312 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
1313 np.isnan(df["midpointMjdTai"]))]
1314 if len(tmpDf) < 2:
1315 return np.nan
1316 times = tmpDf["midpointMjdTai"].to_numpy()
1317 timeArgs = times.argsort()
1318 times = times[timeArgs]
1319 fluxes = tmpDf["psfFlux"].to_numpy()[timeArgs]
1320 return (np.diff(fluxes) / np.diff(times)).max()
1321
1322 column = "{}_psfFluxMaxSlope".format(band)
1323
1325 diaObjects,
1326 filterDiaSources.apply(_maxSlope),
1327 [column],
1328 )
1329
1330
1332 pass
1333
1334
1335@register("ap_meanErrFlux")
1337 """Compute the mean of the dia source errors.
1338 """
1339
1340 ConfigClass = ErrMeanDiaPsfFluxConfig
1341
1342 # Required input Cols
1343 # Output columns are created upon instantiation of the class.
1344 outputCols = ["psfFluxErrMean"]
1345 plugType = "multi"
1346 needsFilter = True
1347
1348 @classmethod
1351
1352 def calculate(self,
1353 diaObjects,
1354 diaSources,
1355 filterDiaSources,
1356 band,
1357 **kwargs):
1358 """Compute the mean of the dia source errors.
1359
1360 Parameters
1361 ----------
1362 diaObject : `dict`
1363 Summary object to store values in.
1364 diaSources : `pandas.DataFrame`
1365 DataFrame representing all diaSources associated with this
1366 diaObject.
1367 filterDiaSources : `pandas.DataFrame`
1368 DataFrame representing diaSources associated with this
1369 diaObject that are observed in the band pass ``band``.
1370 band : `str`
1371 Simple, string name of the filter for the flux being calculated.
1372 **kwargs
1373 Any additional keyword arguments that may be passed to the plugin.
1374 """
1375 column = "{}_psfFluxErrMean".format(band)
1376
1378 diaObjects,
1379 filterDiaSources.psfFluxErr.mean(),
1380 [column],
1381 # Note that the schemas expect this to be single-precision.
1382 default_dtype=np.float32,
1383 )
1384
1385
1387 pass
1388
1389
1390@register("ap_linearFit")
1392 """Compute fit a linear model to flux vs time.
1393 """
1394
1395 ConfigClass = LinearFitDiaPsfFluxConfig
1396
1397 # Required input Cols
1398 # Output columns are created upon instantiation of the class.
1399 outputCols = ["psfFluxLinearSlope", "psfFluxLinearIntercept"]
1400 plugType = "multi"
1401 needsFilter = True
1402
1403 @classmethod
1406
1407 def calculate(self,
1408 diaObjects,
1409 diaSources,
1410 filterDiaSources,
1411 band,
1412 **kwargs):
1413 """Compute fit a linear model to flux vs time.
1414
1415 Parameters
1416 ----------
1417 diaObject : `dict`
1418 Summary object to store values in.
1419 diaSources : `pandas.DataFrame`
1420 DataFrame representing all diaSources associated with this
1421 diaObject.
1422 filterDiaSources : `pandas.DataFrame`
1423 DataFrame representing diaSources associated with this
1424 diaObject that are observed in the band pass ``band``.
1425 band : `str`
1426 Simple, string name of the filter for the flux being calculated.
1427 **kwargs
1428 Any additional keyword arguments that may be passed to the plugin.
1429 """
1430
1431 mName = "{}_psfFluxLinearSlope".format(band)
1432 if mName not in diaObjects.columns:
1433 diaObjects[mName] = np.nan
1434 bName = "{}_psfFluxLinearIntercept".format(band)
1435 if bName not in diaObjects.columns:
1436 diaObjects[bName] = np.nan
1437 dtype = diaObjects[mName].dtype
1438
1439 def _linearFit(df):
1440 tmpDf = df[~np.logical_or(
1441 np.isnan(df["psfFlux"]),
1442 np.logical_or(np.isnan(df["psfFluxErr"]),
1443 np.isnan(df["midpointMjdTai"])))]
1444 if len(tmpDf) < 2:
1445 return pd.Series({mName: np.nan, bName: np.nan})
1446 fluxes = tmpDf["psfFlux"].to_numpy()
1447 errors = tmpDf["psfFluxErr"].to_numpy()
1448 times = tmpDf["midpointMjdTai"].to_numpy()
1449 A = np.array([times / errors, 1 / errors]).transpose()
1450 m, b = lsq_linear(A, fluxes / errors).x
1451 return pd.Series({mName: m, bName: b}, dtype=dtype)
1452
1454 diaObjects,
1455 filterDiaSources.apply(_linearFit),
1456 [mName, bName],
1457 )
1458
1459
1461 pass
1462
1463
1464@register("ap_stetsonJ")
1466 """Compute the StetsonJ statistic on the DIA point source fluxes.
1467 """
1468
1469 ConfigClass = LinearFitDiaPsfFluxConfig
1470
1471 # Required input Cols
1472 inputCols = ["psfFluxMean"]
1473 # Output columns are created upon instantiation of the class.
1474 outputCols = ["psfFluxStetsonJ"]
1475 plugType = "multi"
1476 needsFilter = True
1477
1478 @classmethod
1480 return cls.FLUX_MOMENTS_CALCULATED
1481
1482 def calculate(self,
1483 diaObjects,
1484 diaSources,
1485 filterDiaSources,
1486 band,
1487 **kwargs):
1488 """Compute the StetsonJ statistic on the DIA point source fluxes.
1489
1490 Parameters
1491 ----------
1492 diaObject : `dict`
1493 Summary object to store values in.
1494 diaSources : `pandas.DataFrame`
1495 DataFrame representing all diaSources associated with this
1496 diaObject.
1497 filterDiaSources : `pandas.DataFrame`
1498 DataFrame representing diaSources associated with this
1499 diaObject that are observed in the band pass ``band``.
1500 band : `str`
1501 Simple, string name of the filter for the flux being calculated.
1502 **kwargs
1503 Any additional keyword arguments that may be passed to the plugin.
1504 """
1505 meanName = "{}_psfFluxMean".format(band)
1506
1507 def _stetsonJ(df):
1508 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]),
1509 np.isnan(df["psfFluxErr"]))]
1510 if len(tmpDf) < 2:
1511 return np.nan
1512 fluxes = tmpDf["psfFlux"].to_numpy()
1513 errors = tmpDf["psfFluxErr"].to_numpy()
1514
1515 return self._stetson_J(
1516 fluxes,
1517 errors,
1518 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName])
1519
1520 column = "{}_psfFluxStetsonJ".format(band)
1522 diaObjects,
1523 filterDiaSources.apply(_stetsonJ),
1524 [column],
1525 )
1526
1527 def _stetson_J(self, fluxes, errors, mean=None):
1528 """Compute the single band stetsonJ statistic.
1529
1530 Parameters
1531 ----------
1532 fluxes : `numpy.ndarray` (N,)
1533 Calibrated lightcurve flux values.
1534 errors : `numpy.ndarray` (N,)
1535 Errors on the calibrated lightcurve fluxes.
1536 mean : `float`
1537 Starting mean from previous plugin.
1538
1539 Returns
1540 -------
1541 stetsonJ : `float`
1542 stetsonJ statistic for the input fluxes and errors.
1543
1544 References
1545 ----------
1546 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
1547 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
1548 """
1549 n_points = len(fluxes)
1550 flux_mean = self._stetson_mean(fluxes, errors, mean)
1551 delta_val = (
1552 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors)
1553 p_k = delta_val ** 2 - 1
1554
1555 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k)))
1556
1558 values,
1559 errors,
1560 mean=None,
1561 alpha=2.,
1562 beta=2.,
1563 n_iter=20,
1564 tol=1e-6):
1565 """Compute the stetson mean of the fluxes which down-weights outliers.
1566
1567 Weighted biased on an error weighted difference scaled by a constant
1568 (1/``a``) and raised to the power beta. Higher betas more harshly
1569 penalize outliers and ``a`` sets the number of sigma where a weighted
1570 difference of 1 occurs.
1571
1572 Parameters
1573 ----------
1574 values : `numpy.dnarray`, (N,)
1575 Input values to compute the mean of.
1576 errors : `numpy.ndarray`, (N,)
1577 Errors on the input values.
1578 mean : `float`
1579 Starting mean value or None.
1580 alpha : `float`
1581 Scalar down-weighting of the fractional difference. lower->more
1582 clipping. (Default value is 2.)
1583 beta : `float`
1584 Power law slope of the used to down-weight outliers. higher->more
1585 clipping. (Default value is 2.)
1586 n_iter : `int`
1587 Number of iterations of clipping.
1588 tol : `float`
1589 Fractional and absolute tolerance goal on the change in the mean
1590 before exiting early. (Default value is 1e-6)
1591
1592 Returns
1593 -------
1594 mean : `float`
1595 Weighted stetson mean result.
1596
1597 References
1598 ----------
1599 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve
1600 Parameters for Cepheid Variables", PASP, 108, 851S, 1996
1601 """
1602 n_points = len(values)
1603 n_factor = np.sqrt(n_points / (n_points - 1))
1604 inv_var = 1 / errors ** 2
1605
1606 if mean is None:
1607 mean = np.average(values, weights=inv_var)
1608 for iter_idx in range(n_iter):
1609 chi = np.fabs(n_factor * (values - mean) / errors)
1610 tmp_mean = np.average(
1611 values,
1612 weights=inv_var / (1 + (chi / alpha) ** beta))
1613 diff = np.fabs(tmp_mean - mean)
1614 mean = tmp_mean
1615 if diff / mean < tol and diff < tol:
1616 break
1617 return mean
1618
1619
1621 pass
1622
1623
1624@register("ap_meanTotFlux")
1626 """Compute the weighted mean and mean error on the point source fluxes
1627 forced photometered at the DiaSource location in the calibrated image.
1628
1629 Additionally store number of usable data points.
1630 """
1631
1632 ConfigClass = WeightedMeanDiaPsfFluxConfig
1633 outputCols = ["scienceFluxMean", "scienceFluxMeanErr"]
1634 plugType = "multi"
1635 needsFilter = True
1636
1637 @classmethod
1640
1641 @catchWarnings(warns=["invalid value encountered",
1642 "divide by zero"])
1643 def calculate(self,
1644 diaObjects,
1645 diaSources,
1646 filterDiaSources,
1647 band,
1648 **kwargs):
1649 """Compute the weighted mean and mean error of the point source flux.
1650
1651 Parameters
1652 ----------
1653 diaObject : `dict`
1654 Summary object to store values in.
1655 diaSources : `pandas.DataFrame`
1656 DataFrame representing all diaSources associated with this
1657 diaObject.
1658 filterDiaSources : `pandas.DataFrame`
1659 DataFrame representing diaSources associated with this
1660 diaObject that are observed in the band pass ``band``.
1661 band : `str`
1662 Simple, string name of the filter for the flux being calculated.
1663 **kwargs
1664 Any additional keyword arguments that may be passed to the plugin.
1665 """
1666 totMeanName = "{}_scienceFluxMean".format(band)
1667 if totMeanName not in diaObjects.columns:
1668 diaObjects[totMeanName] = np.nan
1669 totErrName = "{}_scienceFluxMeanErr".format(band)
1670 if totErrName not in diaObjects.columns:
1671 diaObjects[totErrName] = np.nan
1672
1673 def _meanFlux(df):
1674 tmpDf = df[~np.logical_or(np.isnan(df["scienceFlux"]),
1675 np.isnan(df["scienceFluxErr"]))]
1676 tot_weight = np.nansum(1 / tmpDf["scienceFluxErr"] ** 2)
1677 fluxMean = np.nansum(tmpDf["scienceFlux"]
1678 / tmpDf["scienceFluxErr"] ** 2)
1679 fluxMean /= tot_weight
1680 fluxMeanErr = np.sqrt(1 / tot_weight)
1681
1682 return pd.Series({totMeanName: fluxMean,
1683 totErrName: fluxMeanErr})
1684
1685 df = filterDiaSources.apply(_meanFlux).astype(diaObjects.dtypes[[totMeanName, totErrName]])
1686 typeSafePandasAssignment(diaObjects, df, [totMeanName, totErrName])
1687
1688
1690 pass
1691
1692
1693@register("ap_sigmaTotFlux")
1695 """Compute scatter of diaSource fluxes.
1696 """
1697
1698 ConfigClass = SigmaDiaPsfFluxConfig
1699 # Output columns are created upon instantiation of the class.
1700 outputCols = ["scienceFluxSigma"]
1701 plugType = "multi"
1702 needsFilter = True
1703
1704 @classmethod
1707
1708 def calculate(self,
1709 diaObjects,
1710 diaSources,
1711 filterDiaSources,
1712 band,
1713 **kwargs):
1714 """Compute the sigma fluxes of the point source flux measured on the
1715 calibrated image.
1716
1717 Parameters
1718 ----------
1719 diaObject : `dict`
1720 Summary object to store values in.
1721 diaSources : `pandas.DataFrame`
1722 DataFrame representing all diaSources associated with this
1723 diaObject.
1724 filterDiaSources : `pandas.DataFrame`
1725 DataFrame representing diaSources associated with this
1726 diaObject that are observed in the band pass ``band``.
1727 band : `str`
1728 Simple, string name of the filter for the flux being calculated.
1729 **kwargs
1730 Any additional keyword arguments that may be passed to the plugin.
1731 """
1732 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased
1733 # estimator of scatter (i.e. 'N - 1' instead of 'N').
1734 column = "{}_scienceFluxSigma".format(band)
1735 typeSafePandasAssignment(diaObjects, filterDiaSources.scienceFlux.std(), [column])
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
_stetson_mean(self, values, errors, mean=None, alpha=2., beta=2., n_iter=20, tol=1e-6)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
calculate(self, diaObjects, diaSources, filterDiaSources, band, **kwargs)
typeSafePandasAssignment(target, source, columns, default_dtype=np.float64, int_fill_value=0, force_int_to_float=False)
compute_optimized_periodogram_grid(x0, oversampling_factor=5, nyquist_factor=100)
register(name, shouldApCorr=False, apCorrList=())