Coverage for tests/test_diaCalculationPlugins.py: 99%
573 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-22 08:34 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-22 08:34 +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/>.
22import warnings
24from astropy.stats import median_absolute_deviation
25import numpy as np
26import pandas as pd
27from scipy.stats import skew
28import unittest
30from lsst.meas.base import (
31 MeanDiaPosition, MeanDiaPositionConfig,
32 HTMIndexDiaPosition, HTMIndexDiaPositionConfig,
33 NumDiaSourcesDiaPlugin, NumDiaSourcesDiaPluginConfig,
34 SimpleSourceFlagDiaPlugin, SimpleSourceFlagDiaPluginConfig,
35 WeightedMeanDiaPsfFlux, WeightedMeanDiaPsfFluxConfig,
36 PercentileDiaPsfFlux, PercentileDiaPsfFluxConfig,
37 SigmaDiaPsfFlux, SigmaDiaPsfFluxConfig,
38 Chi2DiaPsfFlux, Chi2DiaPsfFluxConfig,
39 MadDiaPsfFlux, MadDiaPsfFluxConfig,
40 SkewDiaPsfFlux, SkewDiaPsfFluxConfig,
41 MinMaxDiaPsfFlux, MinMaxDiaPsfFluxConfig,
42 MaxSlopeDiaPsfFlux, MaxSlopeDiaPsfFluxConfig,
43 ErrMeanDiaPsfFlux, ErrMeanDiaPsfFluxConfig,
44 LinearFitDiaPsfFlux, LinearFitDiaPsfFluxConfig,
45 StetsonJDiaPsfFlux, StetsonJDiaPsfFluxConfig,
46 WeightedMeanDiaTotFlux, WeightedMeanDiaTotFluxConfig,
47 SigmaDiaTotFlux, SigmaDiaTotFluxConfig,
48 LombScarglePeriodogram, LombScarglePeriodogramConfig,
49 LombScarglePeriodogramMulti, LombScarglePeriodogramMultiConfig,
50 UnphysicalDiaSourceSeparation)
51import lsst.utils.tests
54def run_single_plugin(diaObjectCat,
55 diaObjectId,
56 diaSourceCat,
57 band,
58 plugin):
59 """Wrapper for running single plugins.
61 Reproduces some of the behavior of `lsst.ap.association.DiaCalcuation.run`
63 Parameters
64 ----------
65 diaObjectCat : `pandas.DataFrame`
66 Input object catalog to store data into and read from.
67 diaSourcesCat : `pandas.DataFrame`
68 DiaSource catalog to read data from and groupby on.
69 fitlerName : `str`
70 String name of the filter to process.
71 plugin : `lsst.ap.association.DiaCalculationPlugin`
72 Plugin to run.
73 """
74 diaObjectCat.set_index("diaObjectId", inplace=True, drop=False)
75 diaSourceCat.set_index(
76 ["diaObjectId", "band", "diaSourceId"],
77 inplace=True,
78 drop=False)
80 objDiaSources = diaSourceCat.loc[diaObjectId]
81 updatingFilterDiaSources = diaSourceCat.loc[
82 (diaObjectId, band), :
83 ]
85 plugin.calculate(diaObjects=diaObjectCat,
86 diaObjectId=diaObjectId,
87 diaSources=objDiaSources,
88 filterDiaSources=updatingFilterDiaSources,
89 band=band)
92def run_multi_plugin(diaObjectCat, diaSourceCat, band, plugin):
93 """Wrapper for running multi plugins.
95 Reproduces some of the behavior of `lsst.ap.association.DiaCalcuation.run`
97 Parameters
98 ----------
99 diaObjectCat : `pandas.DataFrame`
100 Input object catalog to store data into and read from.
101 diaSourcesCat : `pandas.DataFrame`
102 DiaSource catalog to read data from and groupby on.
103 filterName : `str`
104 String name of the filter to process.
105 plugin : `lsst.ap.association.DiaCalculationPlugin`
106 Plugin to run.
107 """
108 diaObjectCat.set_index("diaObjectId", inplace=True, drop=False)
109 diaSourceCat.set_index(
110 ["diaObjectId", "band", "diaSourceId"],
111 inplace=True,
112 drop=False)
114 updatingFilterDiaSources = diaSourceCat.loc[
115 (slice(None), band), :
116 ]
118 diaSourcesGB = diaSourceCat.groupby(level=0)
119 filterDiaSourcesGB = updatingFilterDiaSources.groupby(level=0)
121 plugin.calculate(diaObjects=diaObjectCat,
122 diaSources=diaSourcesGB,
123 filterDiaSources=filterDiaSourcesGB,
124 band=band)
127def run_multiband_plugin(diaObjectCat, diaSourceCat, plugin):
128 """Wrapper for running multi plugins.
130 Reproduces some of the behavior of `lsst.ap.association.DiaCalcuation.run`
132 Parameters
133 ----------
134 diaObjectCat : `pandas.DataFrame`
135 Input object catalog to store data into and read from.
136 diaSourcesCat : `pandas.DataFrame`
137 DiaSource catalog to read data from and groupby on.
138 plugin : `lsst.ap.association.DiaCalculationPlugin`
139 Plugin to run.
140 """
141 diaObjectCat.set_index("diaObjectId", inplace=True, drop=False)
142 diaSourceCat.set_index(
143 ["diaObjectId", "band", "diaSourceId"],
144 inplace=True,
145 drop=False)
147 diaSourcesGB = diaSourceCat.groupby(level=0)
149 plugin.calculate(diaObjects=diaObjectCat,
150 diaSources=diaSourcesGB,
151 )
154def make_diaObject_table(objId, plugin, default_value=np.nan, band=None):
155 """Create a minimal diaObject table with columns required for the plugin
157 Parameters
158 ----------
159 objId : `int`
160 The diaObjectId
161 plugin : `lsst.ap.association.DiaCalculationPlugin`
162 The plugin that will be run.
163 default_value : `float` or `int`, optional
164 Value to set new columns to.
165 band : `str`, optional
166 Band designation to append to the plugin columns.
168 Returns
169 -------
170 diaObjects : `pandas.DataFrame`
171 Output catalog with the required columns for the plugin.
172 """
173 # Add an extra empty diaObject here. This ensures that
174 # we properly test the source/object matching implicit
175 # in the plugin calculations.
176 diaObjects = {"diaObjectId": [objId, objId + 1]}
177 for col in plugin.outputCols:
178 if band is not None:
179 diaObjects[f"{band}_{col}"] = default_value
180 else:
181 diaObjects[col] = default_value
182 return pd.DataFrame(diaObjects)
185class TestMeanPosition(unittest.TestCase):
187 def testCalculate(self):
188 """Test mean position calculation.
190 DiaSources here are constructed without per-source coordinate
191 errors, so each ``run_multi_plugin`` call legitimately triggers
192 the no-errors warning from the plugin. Suppress it here so the
193 signal of an unexpected warning elsewhere is not drowned out.
194 """
195 n_sources = 10
196 objId = 0
198 # configure a 2 degree max separation
199 plug = MeanDiaPosition(MeanDiaPositionConfig(MaxAllowedDiaSourceSeparation=7200.0),
200 "ap_meanPosition",
201 None)
203 warnings.filterwarnings("ignore",
204 message="No DiaSources with finite coordinate errors",
205 category=UserWarning)
206 self.addCleanup(warnings.resetwarnings)
208 # Test expected means in RA.
209 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
210 diaSources = pd.DataFrame(data={"ra": np.linspace(-1, 1, n_sources),
211 "dec": np.zeros(n_sources),
212 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
213 "diaObjectId": n_sources * [objId],
214 "band": n_sources * ["g"],
215 "diaSourceId": np.arange(n_sources,
216 dtype=int)})
217 run_multi_plugin(diaObjects, diaSources, "g", plug)
219 self.assertAlmostEqual(diaObjects.loc[objId, "ra"], 0.0)
220 self.assertAlmostEqual(diaObjects.loc[objId, "dec"], 0.0)
222 # Test expected means in DEC.
223 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
224 diaSources = pd.DataFrame(data={"ra": np.zeros(n_sources),
225 "dec": np.linspace(-1, 1, n_sources),
226 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
227 "diaObjectId": n_sources * [objId],
228 "band": n_sources * ["g"],
229 "diaSourceId": np.arange(n_sources,
230 dtype=int)})
231 run_multi_plugin(diaObjects, diaSources, "g", plug)
233 self.assertAlmostEqual(diaObjects.loc[objId, "ra"], 0.0)
234 self.assertAlmostEqual(diaObjects.loc[objId, "dec"], 0.0)
236 # Test failure mode RA is nan.
237 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
238 diaSources = pd.DataFrame(data={"ra": np.full(n_sources, np.nan),
239 "dec": np.zeros(n_sources),
240 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
241 "diaObjectId": n_sources * [objId],
242 "band": n_sources * ["g"],
243 "diaSourceId": np.arange(n_sources,
244 dtype=int)})
245 run_multi_plugin(diaObjects, diaSources, "g", plug)
247 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra"]))
248 self.assertTrue(np.isnan(diaObjects.loc[objId, "dec"]))
250 # Test failure mode DEC is nan.
251 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
252 diaSources = pd.DataFrame(data={"ra": np.zeros(n_sources),
253 "dec": np.full(n_sources, np.nan),
254 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
255 "diaObjectId": n_sources * [objId],
256 "band": n_sources * ["g"],
257 "diaSourceId": np.arange(n_sources,
258 dtype=int)})
259 run_multi_plugin(diaObjects, diaSources, "g", plug)
261 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra"]))
262 self.assertTrue(np.isnan(diaObjects.loc[objId, "dec"]))
264 # configure the default 3 arcsecond separation
265 plug = MeanDiaPosition(MeanDiaPositionConfig(MaxAllowedDiaSourceSeparation=3.0),
266 "ap_meanPosition",
267 None)
269 # These 1 degree separations should raise
270 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
271 diaSources = pd.DataFrame(data={"ra": np.linspace(-1, 1, n_sources),
272 "dec": np.zeros(n_sources),
273 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
274 "diaObjectId": n_sources * [objId],
275 "band": n_sources * ["g"],
276 "diaSourceId": np.arange(n_sources,
277 dtype=int)})
278 with self.assertRaises(UnphysicalDiaSourceSeparation):
279 run_multi_plugin(diaObjects, diaSources, "g", plug)
281 # 1 arcsecond separations should not raise
282 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
283 diaSources = pd.DataFrame(data={"ra": np.linspace(-1/3600., 1/3600., n_sources),
284 "dec": np.zeros(n_sources),
285 "midpointMjdTai": np.linspace(0, n_sources, n_sources),
286 "diaObjectId": n_sources * [objId],
287 "band": n_sources * ["g"],
288 "diaSourceId": np.arange(n_sources,
289 dtype=int)})
290 run_multi_plugin(diaObjects, diaSources, "g", plug)
292 def _makeDiaSourcesWithUncertainties(self, raErr, decErr, ra_dec_Cov, ra=None, dec=None, objId=0):
293 """Build a tiny DiaSource DataFrame with per-source uncertainties.
294 """
295 n = len(raErr)
296 if ra is None:
297 ra = np.zeros(n)
298 if dec is None:
299 dec = np.zeros(n)
300 return pd.DataFrame(data={
301 "ra": ra,
302 "dec": dec,
303 "raErr": raErr,
304 "decErr": decErr,
305 "ra_dec_Cov": ra_dec_Cov,
306 "midpointMjdTai": np.arange(n, dtype=float),
307 "diaObjectId": n * [objId],
308 "band": n * ["g"],
309 "diaSourceId": np.arange(n, dtype=int),
310 })
312 def testUncertaintyDiagonalOnly(self):
313 """Two coincident sources, no off-diagonal: chi^2 = 0, scale
314 factor is 1, so the output is the diagonal inverse-variance
315 weighted-mean covariance.
316 """
317 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
318 objId = 0
319 raErr = np.array([1e-6, 2e-6])
320 decErr = np.array([1e-6, 2e-6])
321 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
322 diaSources = self._makeDiaSourcesWithUncertainties(
323 raErr=raErr,
324 decErr=decErr,
325 ra_dec_Cov=np.array([np.nan, np.nan]),
326 )
327 run_multi_plugin(diaObjects, diaSources, "g", plug)
329 # Var(weighted mean) = 1 / sum(1/sigma^2) per axis.
330 expectedRaErr = 1.0/np.sqrt(np.sum(1.0/raErr**2))
331 expectedDecErr = 1.0/np.sqrt(np.sum(1.0/decErr**2))
332 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], expectedRaErr)
333 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], expectedDecErr)
334 # No per-source ra_dec_Cov, so output ra_dec_Cov is NaN.
335 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra_dec_Cov"]))
337 def testUncertaintyFullCovariance(self):
338 """Two coincident sources with off-diagonal covariance: chi^2 = 0,
339 so the output is C_formal = (sum_i inv(C_i))^-1.
340 """
341 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
342 objId = 0
343 raErr = np.array([1e-6, 2e-6])
344 decErr = np.array([1.5e-6, 1.0e-6])
345 rho = np.array([0.3, -0.2]) # correlation coefficient
346 raDecCov = rho * raErr * decErr
348 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
349 diaSources = self._makeDiaSourcesWithUncertainties(
350 raErr=raErr, decErr=decErr, ra_dec_Cov=raDecCov)
351 run_multi_plugin(diaObjects, diaSources, "g", plug)
353 n = len(raErr)
354 cov = np.zeros((n, 2, 2))
355 cov[:, 0, 0] = raErr**2
356 cov[:, 1, 1] = decErr**2
357 cov[:, 0, 1] = raDecCov
358 cov[:, 1, 0] = raDecCov
359 covObj = np.linalg.inv(np.linalg.inv(cov).sum(axis=0))
361 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], np.sqrt(covObj[0, 0]))
362 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], np.sqrt(covObj[1, 1]))
363 self.assertAlmostEqual(diaObjects.loc[objId, "ra_dec_Cov"], covObj[0, 1])
365 def testUncertaintySingleSourceCopiesThrough(self):
366 """A single-source group: outputs equal the source's uncertainties.
367 """
368 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
369 objId = 0
370 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
371 diaSources = self._makeDiaSourcesWithUncertainties(
372 raErr=np.array([1.5e-6]),
373 decErr=np.array([0.8e-6]),
374 ra_dec_Cov=np.array([2e-13]),
375 )
376 run_multi_plugin(diaObjects, diaSources, "g", plug)
378 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], 1.5e-6)
379 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], 0.8e-6)
380 self.assertAlmostEqual(diaObjects.loc[objId, "ra_dec_Cov"], 2e-13)
382 def testUncertaintyMissingColumnsFallsBackToScatter(self):
383 """No raErr/decErr columns + N>=2 spread-out sources -> the
384 weighted-mean path is unusable, so the DiaObject falls back to
385 the unweighted mean position with the scatter-only SEM, and a
386 warning is emitted.
387 """
388 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
389 objId = 0
390 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
391 n = 3
392 ra = np.linspace(-1e-4, 1e-4, n)
393 dec = np.zeros(n)
394 diaSources = pd.DataFrame(data={
395 "ra": ra,
396 "dec": dec,
397 "midpointMjdTai": np.arange(n, dtype=float),
398 "diaObjectId": n * [objId],
399 "band": n * ["g"],
400 "diaSourceId": np.arange(n, dtype=int),
401 })
402 with self.assertWarnsRegex(UserWarning, "No DiaSources with finite coordinate errors"):
403 run_multi_plugin(diaObjects, diaSources, "g", plug)
405 self.assertAlmostEqual(diaObjects.loc[objId, "ra"], 0.0)
406 self.assertAlmostEqual(diaObjects.loc[objId, "dec"], 0.0)
408 # Expected scatter term: at dec=0, the tangent-plane east offsets
409 # equal the RA deltas (in degrees) to high precision, so the
410 # standard error of the mean in RA is sample_std(ra, ddof=1) /
411 # sqrt(N). Dec is identically zero so its scatter is zero.
412 expectedRaErr = np.std(ra, ddof=1)/np.sqrt(n)
413 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], expectedRaErr)
414 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], 0.0)
415 self.assertAlmostEqual(diaObjects.loc[objId, "ra_dec_Cov"], 0.0)
417 def testUncertaintyMissingColumnsSingleSourceEmitsNaN(self):
418 """With a single source and no raErr/decErr the uncertainty is
419 undefined but the mean position is still computed; a warning is
420 emitted that the per-source errors are unusable.
421 """
422 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
423 objId = 0
424 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
425 diaSources = pd.DataFrame(data={
426 "ra": [0.0],
427 "dec": [0.0],
428 "midpointMjdTai": [0.0],
429 "diaObjectId": [objId],
430 "band": ["g"],
431 "diaSourceId": [0],
432 })
433 with self.assertWarnsRegex(UserWarning, "No DiaSources with finite coordinate errors"):
434 run_multi_plugin(diaObjects, diaSources, "g", plug)
436 self.assertAlmostEqual(diaObjects.loc[objId, "ra"], 0.0)
437 self.assertAlmostEqual(diaObjects.loc[objId, "dec"], 0.0)
438 self.assertTrue(np.isnan(diaObjects.loc[objId, "raErr"]))
439 self.assertTrue(np.isnan(diaObjects.loc[objId, "decErr"]))
440 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra_dec_Cov"]))
442 def testUncertaintyPartialCovarianceFallsBackToDiagonal(self):
443 """Some sources have NaN ra_dec_Cov but valid raErr/decErr.
445 When at least one included source lacks a finite ra_dec_Cov,
446 diagonal weights are used for all included sources, the off-
447 diagonal output is NaN, and the chi-squared falls back to its
448 diagonal form. Sources are coincident here, so chi^2 = 0 and
449 there is no rescaling.
450 """
451 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
452 objId = 0
453 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
454 raErr = np.array([1e-6, 2e-6, 3e-6])
455 decErr = np.array([1e-6, 2e-6, 3e-6])
456 # Only one source has a finite covariance
457 raDecCov = np.array([1e-13, np.nan, np.nan])
458 diaSources = self._makeDiaSourcesWithUncertainties(raErr=raErr, decErr=decErr, ra_dec_Cov=raDecCov)
459 run_multi_plugin(diaObjects, diaSources, "g", plug)
461 # Diagonal inverse-variance weighted mean covariance.
462 expectedRaErr = 1.0/np.sqrt(np.sum(1.0/raErr**2))
463 expectedDecErr = 1.0/np.sqrt(np.sum(1.0/decErr**2))
464 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], expectedRaErr)
465 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], expectedDecErr)
466 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra_dec_Cov"]))
468 def testUncertaintyScaleFactorInflatesWhenScatterExceedsErrors(self):
469 """Two sources whose positional separation is much larger than
470 their per-source errors: chi^2 >> dof, so the chi-squared scale
471 factor inflates the formal covariance by chi^2 / dof.
472 """
473 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
474 objId = 0
475 # Two sources symmetric around RA = 0, Dec = 0. Spread of
476 # 0.36 arcsec on each axis (within MaxAllowedDiaSourceSeparation
477 # of 3 arcsec), still 100x the per-source sigma of 1e-6 deg = 3.6 mas,
478 # so chi^2 / dof ~ 4e4 / 2 ~ 2e4.
479 delta = 1e-4 # degrees
480 ra = np.array([-delta, delta])
481 dec = np.array([-delta, delta])
482 raErr = np.array([1e-6, 1e-6])
483 decErr = np.array([1e-6, 1e-6])
484 raDecCov = np.array([np.nan, np.nan])
486 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
487 diaSources = self._makeDiaSourcesWithUncertainties(
488 raErr=raErr, decErr=decErr, ra_dec_Cov=raDecCov, ra=ra, dec=dec)
489 run_multi_plugin(diaObjects, diaSources, "g", plug)
491 # With equal per-source errors the weighted mean equals the
492 # unweighted mean = (0, 0), so the tangent-plane residuals are
493 # essentially (ra, dec).
494 # chi^2 = sum_i (r_east^2/sigma_ra^2 + r_north^2/sigma_dec^2)
495 # dof = 2 * (N - 1) = 2.
496 chi2 = np.sum(ra**2/raErr**2 + dec**2/decErr**2)
497 dof = 2*(len(raErr) - 1)
498 scale = max(1.0, chi2/dof)
499 # Diagonal weighted-mean variance: 1/sum(1/sigma^2) per axis.
500 varRaFormal = 1.0/np.sum(1.0/raErr**2)
501 varDecFormal = 1.0/np.sum(1.0/decErr**2)
502 expectedRaErr = np.sqrt(scale*varRaFormal)
503 expectedDecErr = np.sqrt(scale*varDecFormal)
505 # The inflation should be large (scale ~ 1e6):
506 self.assertGreater(scale, 1e3)
508 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"] / expectedRaErr, 1.0, places=5)
509 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"] / expectedDecErr, 1.0, places=5)
510 self.assertTrue(np.isnan(diaObjects.loc[objId, "ra_dec_Cov"]))
512 def testUncertaintyScaleFactorNoInflationWhenConsistent(self):
513 """When per-source positions are coincident, chi^2 = 0 and the
514 chi-squared scale factor is exactly 1: the output equals
515 C_formal = (sum_i inv(C_i))^-1.
516 """
517 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
518 objId = 0
519 raErr = np.array([1e-6, 2e-6, 1.5e-6])
520 decErr = np.array([1e-6, 1.5e-6, 2e-6])
521 raDecCov = np.array([2e-13, -1e-13, 3e-14])
523 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
524 diaSources = self._makeDiaSourcesWithUncertainties(raErr=raErr, decErr=decErr, ra_dec_Cov=raDecCov)
525 run_multi_plugin(diaObjects, diaSources, "g", plug)
527 n = len(raErr)
528 cov = np.zeros((n, 2, 2))
529 cov[:, 0, 0] = raErr**2
530 cov[:, 1, 1] = decErr**2
531 cov[:, 0, 1] = raDecCov
532 cov[:, 1, 0] = raDecCov
533 covObj = np.linalg.inv(np.linalg.inv(cov).sum(axis=0))
535 self.assertAlmostEqual(diaObjects.loc[objId, "raErr"], np.sqrt(covObj[0, 0]))
536 self.assertAlmostEqual(diaObjects.loc[objId, "decErr"], np.sqrt(covObj[1, 1]))
537 self.assertAlmostEqual(diaObjects.loc[objId, "ra_dec_Cov"], covObj[0, 1])
539 def testWeightedMeanPositionDiffersFromUnweighted(self):
540 """Two sources at asymmetric (ra) positions with very unequal
541 per-source errors: the reported position is the inverse-variance
542 weighted mean, which is much closer to the more-precise source
543 than to the unweighted midpoint.
544 """
545 plug = MeanDiaPosition(MeanDiaPositionConfig(), "ap_meanPosition", None)
546 objId = 0
547 d = 1e-5 # degrees; well inside MaxAllowedDiaSourceSeparation.
548 ra = np.array([-d, d])
549 dec = np.array([0.0, 0.0])
550 # Source 0 is 100x more precise than source 1.
551 raErr = np.array([1e-6, 1e-4])
552 decErr = np.array([1e-6, 1e-4])
553 raDecCov = np.array([np.nan, np.nan])
555 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
556 diaSources = self._makeDiaSourcesWithUncertainties(
557 raErr=raErr, decErr=decErr, ra_dec_Cov=raDecCov, ra=ra, dec=dec)
558 run_multi_plugin(diaObjects, diaSources, "g", plug)
560 # Diagonal weighted mean of (ra_0, ra_1) with weights w_i = 1/raErr_i^2.
561 w = 1.0/raErr**2
562 expectedRa = np.sum(w*ra)/np.sum(w)
563 # Output RA is wrapped to [0, 360); normalize back to a signed
564 # offset near zero before comparing.
565 outRa = ((diaObjects.loc[objId, "ra"] + 180.0) % 360.0) - 180.0
566 # The unweighted mean would be 0; the weighted mean should be
567 # very close to source 0 at ra = -d.
568 self.assertAlmostEqual(outRa, expectedRa)
569 self.assertLess(abs(outRa - (-d)), 0.01*d)
570 self.assertAlmostEqual(diaObjects.loc[objId, "dec"], 0.0)
573class TestHTMIndexPosition(unittest.TestCase):
575 def testCalculate(self):
576 """Test HTMPixel assignment calculation.
577 """
578 # Test expected pixelId at RA, DEC = 0
579 objId = 0
580 n_sources = 10
581 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
582 diaObjects.loc[objId, "ra"] = 0.
583 diaObjects.loc[objId, "dec"] = 0.
584 diaSources = pd.DataFrame(
585 data={"diaObjectId": n_sources * [objId],
586 "band": n_sources * ["g"],
587 "diaSourceId": np.arange(n_sources, dtype=int)})
588 plug = HTMIndexDiaPosition(HTMIndexDiaPositionConfig(),
589 "ap_HTMIndex",
590 None)
592 run_single_plugin(diaObjectCat=diaObjects,
593 diaObjectId=objId,
594 diaSourceCat=diaSources,
595 band="g",
596 plugin=plug)
597 self.assertEqual(diaObjects.at[objId, "pixelId"],
598 17042430230528)
600 # Test expected pixelId at some value of RA and DEC.
601 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
602 diaObjects.loc[objId, "ra"] = 45.37
603 diaObjects.loc[objId, "dec"] = 13.67
604 diaSources = pd.DataFrame(
605 data={"diaObjectId": n_sources * [objId],
606 "band": n_sources * ["g"],
607 "diaSourceId": np.arange(n_sources, dtype=int)})
608 run_single_plugin(diaObjectCat=diaObjects,
609 diaObjectId=objId,
610 diaSourceCat=diaSources,
611 band="g",
612 plugin=plug)
613 self.assertEqual(diaObjects.at[objId, "pixelId"],
614 17450571968473)
617class TestNDiaSourcesDiaPlugin(unittest.TestCase):
619 def testCalculate(self):
620 """Test that the number of DiaSources is correct.
621 """
623 for n_sources in [1, 8, 10]:
624 # Test expected number of sources per object.
625 objId = 0
626 diaSources = pd.DataFrame(
627 data={"diaObjectId": n_sources * [objId],
628 "band": n_sources * ["g"],
629 "diaSourceId": np.arange(n_sources, dtype=int)})
630 plug = NumDiaSourcesDiaPlugin(NumDiaSourcesDiaPluginConfig(),
631 "ap_nDiaSources",
632 None)
633 diaObjects = make_diaObject_table(objId, plug, default_value=int(-1))
634 run_multi_plugin(diaObjects, diaSources, "g", plug)
636 self.assertEqual(n_sources, diaObjects.at[objId, "nDiaSources"])
637 self.assertEqual(diaObjects["nDiaSources"].dtype, np.int64)
640class TestSimpleSourceFlagDiaPlugin(unittest.TestCase):
642 def testCalculate(self):
643 """Test that DiaObject flags are set.
644 """
645 objId = 0
646 n_sources = 10
648 # Test expected flags, no flags set.
649 diaSources = pd.DataFrame(
650 data={"diaObjectId": n_sources * [objId],
651 "band": n_sources * ["g"],
652 "diaSourceId": np.arange(n_sources, dtype=int),
653 "flags": np.zeros(n_sources, dtype=np.uint64)})
654 plug = SimpleSourceFlagDiaPlugin(SimpleSourceFlagDiaPluginConfig(),
655 "ap_diaObjectFlag",
656 None)
658 diaObjects = make_diaObject_table(objId, plug, default_value=np.uint64(0))
659 run_multi_plugin(diaObjects, diaSources, "g", plug)
660 self.assertEqual(diaObjects.at[objId, "flags"], 0)
661 self.assertEqual(diaObjects["flags"].dtype, np.uint64)
663 # Test expected flags, all flags set.
664 diaSources = pd.DataFrame(
665 data={"diaObjectId": n_sources * [objId],
666 "band": n_sources * ["g"],
667 "diaSourceId": np.arange(n_sources, dtype=int),
668 "flags": np.ones(n_sources, dtype=np.uint64)})
669 diaObjects = make_diaObject_table(objId, plug, default_value=np.uint64(0))
670 run_multi_plugin(diaObjects, diaSources, "g", plug)
671 self.assertEqual(diaObjects.at[objId, "flags"], 1)
672 self.assertEqual(diaObjects["flags"].dtype, np.uint64)
674 # Test expected flags, random flags.
675 diaSources = pd.DataFrame(
676 data={"diaObjectId": n_sources * [objId],
677 "band": n_sources * ["g"],
678 "diaSourceId": np.arange(n_sources, dtype=int),
679 "flags": np.random.randint(0, 2 ** 16, size=n_sources)})
681 diaObjects = make_diaObject_table(objId, plug, default_value=np.uint64(0))
682 run_multi_plugin(diaObjects, diaSources, "g", plug)
683 self.assertEqual(diaObjects.at[objId, "flags"], 1)
684 self.assertEqual(diaObjects["flags"].dtype, np.uint64)
686 # Test expected flags, one flag set.
687 flag_array = np.zeros(n_sources, dtype=np.uint64)
688 flag_array[4] = 256
689 diaSources = pd.DataFrame(
690 data={"diaObjectId": n_sources * [objId],
691 "band": n_sources * ["g"],
692 "diaSourceId": np.arange(n_sources, dtype=int),
693 "flags": flag_array})
694 diaObjects = make_diaObject_table(objId, plug, default_value=np.uint64(0))
695 run_multi_plugin(diaObjects, diaSources, "g", plug)
696 self.assertEqual(diaObjects.at[objId, "flags"], 1)
697 self.assertEqual(diaObjects["flags"].dtype, np.uint64)
700class TestWeightedMeanDiaPsfFlux(unittest.TestCase):
702 def testCalculate(self):
703 """Test mean value calculation.
704 """
705 n_sources = 10
706 objId = 0
708 # Test expected mean.
709 # In the first test, we have only one object.
710 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
711 diaSources = pd.DataFrame(
712 data={"diaObjectId": n_sources * [objId],
713 "band": n_sources * ["u"],
714 "diaSourceId": np.arange(n_sources, dtype=int),
715 "psfFlux": np.linspace(-1, 1, n_sources),
716 "psfFluxErr": np.ones(n_sources)})
718 plug = WeightedMeanDiaPsfFlux(WeightedMeanDiaPsfFluxConfig(),
719 "ap_meanFlux",
720 None)
721 run_multi_plugin(diaObjects, diaSources, "u", plug)
723 self.assertAlmostEqual(diaObjects.loc[objId, "u_psfFluxMean"], 0.0)
724 self.assertAlmostEqual(diaObjects.loc[objId, "u_psfFluxMeanErr"],
725 np.sqrt(1 / n_sources))
726 self.assertEqual(diaObjects.loc[objId, "u_psfFluxNdata"], n_sources)
727 # We expect this to be converted to float.
728 # TODO DM-53254: This should be an integer (and should be checked
729 # to be an integer).
730 self.assertEqual(diaObjects["u_psfFluxNdata"].dtype, np.float64)
732 # Test expected mean with a nan value.
733 # In the second test, we have two objects (one empty).
734 diaObjects = pd.DataFrame({"diaObjectId": [objId, objId + 1]})
735 fluxes = np.linspace(-1, 1, n_sources)
736 fluxes[4] = np.nan
737 diaSources = pd.DataFrame(
738 data={"diaObjectId": n_sources * [objId],
739 "band": n_sources * ["r"],
740 "diaSourceId": np.arange(n_sources, dtype=int),
741 "psfFlux": fluxes,
742 "psfFluxErr": np.ones(n_sources)})
743 run_multi_plugin(diaObjects, diaSources, "r", plug)
745 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxMean"],
746 np.nanmean(fluxes))
747 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxMeanErr"],
748 np.sqrt(1 / (n_sources - 1)))
749 self.assertEqual(diaObjects.loc[objId, "r_psfFluxNdata"], n_sources - 1)
750 # We expect this to be converted to float.
751 # TODO DM-53254: This should be an integer (and should be checked
752 # to be an integer).
753 self.assertEqual(diaObjects["r_psfFluxNdata"].dtype, np.float64)
756class TestPercentileDiaPsfFlux(unittest.TestCase):
758 def testCalculate(self):
759 """Test flux percentile calculation.
760 """
761 n_sources = 10
762 objId = 0
764 # Test expected percentile values.
765 fluxes = np.linspace(-1, 1, n_sources)
766 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
767 diaSources = pd.DataFrame(
768 data={"diaObjectId": n_sources * [objId],
769 "band": n_sources * ["u"],
770 "diaSourceId": np.arange(n_sources, dtype=int),
771 "psfFlux": fluxes,
772 "psfFluxErr": np.ones(n_sources)})
774 plug = PercentileDiaPsfFlux(PercentileDiaPsfFluxConfig(),
775 "ap_percentileFlux",
776 None)
777 run_multi_plugin(diaObjects, diaSources, "u", plug)
778 for pTile, testVal in zip(plug.config.percentiles,
779 np.nanpercentile(
780 fluxes,
781 plug.config.percentiles)):
782 self.assertAlmostEqual(
783 diaObjects.at[objId, "u_psfFluxPercentile{:02d}".format(pTile)],
784 testVal)
786 # Test expected percentile values with a nan value.
787 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
788 fluxes[4] = np.nan
789 diaSources = pd.DataFrame(
790 data={"diaObjectId": n_sources * [objId],
791 "band": n_sources * ["r"],
792 "diaSourceId": np.arange(n_sources, dtype=int),
793 "psfFlux": fluxes,
794 "psfFluxErr": np.ones(n_sources)})
795 run_multi_plugin(diaObjects, diaSources, "r", plug)
796 for pTile, testVal in zip(plug.config.percentiles,
797 np.nanpercentile(
798 fluxes,
799 plug.config.percentiles)):
800 self.assertAlmostEqual(
801 diaObjects.at[objId, "r_psfFluxPercentile{:02d}".format(pTile)],
802 testVal)
805class TestSigmaDiaPsfFlux(unittest.TestCase):
807 def testCalculate(self):
808 """Test flux scatter calculation.
809 """
810 n_sources = 10
811 objId = 0
813 # Test expected sigma scatter of fluxes.
814 fluxes = np.linspace(-1, 1, n_sources)
815 diaSources = pd.DataFrame(
816 data={"diaObjectId": n_sources * [objId],
817 "band": n_sources * ["u"],
818 "diaSourceId": np.arange(n_sources, dtype=int),
819 "psfFlux": fluxes,
820 "psfFluxErr": np.ones(n_sources)})
822 plug = SigmaDiaPsfFlux(SigmaDiaPsfFluxConfig(),
823 "ap_sigmaFlux",
824 None)
825 diaObjects = make_diaObject_table(objId, plug, band='u')
826 run_multi_plugin(diaObjects, diaSources, "u", plug)
827 self.assertAlmostEqual(diaObjects.at[objId, "u_psfFluxSigma"],
828 np.nanstd(fluxes, ddof=1))
830 # test one input, returns nan.
831 diaSources = pd.DataFrame(
832 data={"diaObjectId": 1 * [objId],
833 "band": 1 * ["g"],
834 "diaSourceId": [0],
835 "psfFlux": [fluxes[0]],
836 "psfFluxErr": [1.]})
838 diaObjects = make_diaObject_table(objId, plug, band='g')
839 run_multi_plugin(diaObjects, diaSources, "g", plug)
840 self.assertTrue(np.isnan(diaObjects.at[objId, "g_psfFluxSigma"]))
842 # Test expected sigma scatter of fluxes with a nan value.
843 fluxes[4] = np.nan
844 diaSources = pd.DataFrame(
845 data={"diaObjectId": n_sources * [objId],
846 "band": n_sources * ["r"],
847 "diaSourceId": np.arange(n_sources, dtype=int),
848 "psfFlux": fluxes,
849 "psfFluxErr": np.ones(n_sources)})
851 diaObjects = make_diaObject_table(objId, plug, band='r')
852 run_multi_plugin(diaObjects, diaSources, "r", plug)
853 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxSigma"],
854 np.nanstd(fluxes, ddof=1))
857class TestChi2DiaPsfFlux(unittest.TestCase):
859 def testCalculate(self):
860 """Test flux chi2 calculation.
861 """
862 n_sources = 10
863 objId = 0
865 # Test expected chi^2 value.
866 fluxes = np.linspace(-1, 1, n_sources)
867 diaObjects = pd.DataFrame({"diaObjectId": [objId],
868 "u_psfFluxMean": [0.0]})
869 diaSources = pd.DataFrame(
870 data={"diaObjectId": n_sources * [objId],
871 "band": n_sources * ["u"],
872 "diaSourceId": np.arange(n_sources, dtype=int),
873 "psfFlux": fluxes,
874 "psfFluxErr": np.ones(n_sources)})
876 plug = Chi2DiaPsfFlux(Chi2DiaPsfFluxConfig(),
877 "ap_chi2Flux",
878 None)
879 run_multi_plugin(diaObjects, diaSources, "u", plug)
880 self.assertAlmostEqual(
881 diaObjects.loc[objId, "u_psfFluxChi2"],
882 np.nansum(((diaSources["psfFlux"]
883 - np.nanmean(diaSources["psfFlux"]))
884 / diaSources["psfFluxErr"]) ** 2))
886 # Test expected chi^2 value with a nan value set.
887 fluxes[4] = np.nan
888 diaObjects = pd.DataFrame({"diaObjectId": [objId],
889 "r_psfFluxMean": [np.nanmean(fluxes)]})
890 diaSources = pd.DataFrame(
891 data={"diaObjectId": n_sources * [objId],
892 "band": n_sources * ["r"],
893 "diaSourceId": np.arange(n_sources, dtype=int),
894 "psfFlux": fluxes,
895 "psfFluxErr": np.ones(n_sources)})
896 run_multi_plugin(diaObjects, diaSources, "r", plug)
897 self.assertAlmostEqual(
898 diaObjects.loc[objId, "r_psfFluxChi2"],
899 np.nansum(((diaSources["psfFlux"]
900 - np.nanmean(diaSources["psfFlux"]))
901 / diaSources["psfFluxErr"]) ** 2))
904class TestMadDiaPsfFlux(unittest.TestCase):
906 def testCalculate(self):
907 """Test flux median absolute deviation calculation.
908 """
909 n_sources = 10
910 objId = 0
912 # Test expected MAD value.
913 fluxes = np.linspace(-1, 1, n_sources)
914 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
915 diaSources = pd.DataFrame(
916 data={"diaObjectId": n_sources * [objId],
917 "band": n_sources * ["u"],
918 "diaSourceId": np.arange(n_sources, dtype=int),
919 "psfFlux": fluxes,
920 "psfFluxErr": np.ones(n_sources)})
922 plug = MadDiaPsfFlux(MadDiaPsfFluxConfig(),
923 "ap_madFlux",
924 None)
925 run_multi_plugin(diaObjects, diaSources, "u", plug)
926 self.assertAlmostEqual(diaObjects.at[objId, "u_psfFluxMAD"],
927 median_absolute_deviation(fluxes,
928 ignore_nan=True))
930 # Test expected MAD value with a nan set.
931 fluxes[4] = np.nan
932 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
933 diaSources = pd.DataFrame(
934 data={"diaObjectId": n_sources * [objId],
935 "band": n_sources * ["r"],
936 "diaSourceId": np.arange(n_sources, dtype=int),
937 "psfFlux": fluxes,
938 "psfFluxErr": np.ones(n_sources)})
939 run_multi_plugin(diaObjects, diaSources, "r", plug)
940 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxMAD"],
941 median_absolute_deviation(fluxes,
942 ignore_nan=True))
945class TestSkewDiaPsfFlux(unittest.TestCase):
947 def testCalculate(self):
948 """Test flux skew calculation.
949 """
950 n_sources = 10
951 objId = 0
953 # Test expected skew value.
954 fluxes = np.linspace(-1, 1, n_sources)
955 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
956 diaSources = pd.DataFrame(
957 data={"diaObjectId": n_sources * [objId],
958 "band": n_sources * ["u"],
959 "diaSourceId": np.arange(n_sources, dtype=int),
960 "psfFlux": fluxes,
961 "psfFluxErr": np.ones(n_sources)})
963 plug = SkewDiaPsfFlux(SkewDiaPsfFluxConfig(),
964 "ap_skewFlux",
965 None)
966 run_multi_plugin(diaObjects, diaSources, "u", plug)
967 self.assertAlmostEqual(
968 diaObjects.loc[objId, "u_psfFluxSkew"],
969 skew_wrapper(fluxes))
971 # Test expected skew value with a nan set.
972 fluxes[4] = np.nan
973 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
974 diaSources = pd.DataFrame(
975 data={"diaObjectId": n_sources * [objId],
976 "band": n_sources * ["r"],
977 "diaSourceId": np.arange(n_sources, dtype=int),
978 "psfFlux": fluxes,
979 "psfFluxErr": np.ones(n_sources)})
980 run_multi_plugin(diaObjects, diaSources, "r", plug)
982 self.assertAlmostEqual(
983 diaObjects.at[objId, "r_psfFluxSkew"],
984 skew_wrapper(fluxes))
987class TestMinMaxDiaPsfFlux(unittest.TestCase):
989 def testCalculate(self):
990 """Test flux min/max calculation.
991 """
992 n_sources = 10
993 objId = 0
995 # Test expected MinMax fluxes.
996 fluxes = np.linspace(-1, 1, n_sources)
997 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
998 diaSources = pd.DataFrame(
999 data={"diaObjectId": n_sources * [objId],
1000 "band": n_sources * ["u"],
1001 "diaSourceId": np.arange(n_sources, dtype=int),
1002 "psfFlux": fluxes,
1003 "psfFluxErr": np.ones(n_sources)})
1005 plug = MinMaxDiaPsfFlux(MinMaxDiaPsfFluxConfig(),
1006 "ap_minMaxFlux",
1007 None)
1008 run_multi_plugin(diaObjects, diaSources, "u", plug)
1009 self.assertEqual(diaObjects.loc[objId, "u_psfFluxMin"], -1)
1010 self.assertEqual(diaObjects.loc[objId, "u_psfFluxMax"], 1)
1012 # Test expected MinMax fluxes with a nan set.
1013 fluxes[4] = np.nan
1014 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1015 diaSources = pd.DataFrame(
1016 data={"diaObjectId": n_sources * [objId],
1017 "band": n_sources * ["r"],
1018 "diaSourceId": np.arange(n_sources, dtype=int),
1019 "psfFlux": fluxes,
1020 "psfFluxErr": np.ones(n_sources)})
1021 run_multi_plugin(diaObjects, diaSources, "r", plug)
1022 self.assertEqual(diaObjects.loc[objId, "r_psfFluxMin"], -1)
1023 self.assertEqual(diaObjects.loc[objId, "r_psfFluxMax"], 1)
1026class TestMaxSlopeDiaPsfFlux(unittest.TestCase):
1028 def testCalculate(self):
1029 """Test flux maximum slope.
1030 """
1031 n_sources = 10
1032 objId = 0
1034 # Test max slope value.
1035 fluxes = np.linspace(-1, 1, n_sources)
1036 times = np.concatenate([np.linspace(0, 1, n_sources)[:-1], [1 - 1/90]])
1037 diaSources = pd.DataFrame(
1038 data={"diaObjectId": n_sources * [objId],
1039 "band": n_sources * ["u"],
1040 "diaSourceId": np.arange(n_sources, dtype=int),
1041 "psfFlux": fluxes,
1042 "psfFluxErr": np.ones(n_sources),
1043 "midpointMjdTai": times})
1045 plug = MaxSlopeDiaPsfFlux(MaxSlopeDiaPsfFluxConfig(),
1046 "ap_maxSlopeFlux",
1047 None)
1048 diaObjects = make_diaObject_table(objId, plug, band='u')
1049 run_multi_plugin(diaObjects, diaSources, "u", plug)
1050 self.assertAlmostEqual(diaObjects.at[objId, "u_psfFluxMaxSlope"], 2 + 2/9)
1052 # Test max slope value returns nan on 1 input.
1053 diaSources = pd.DataFrame(
1054 data={"diaObjectId": 1 * [objId],
1055 "band": 1 * ["g"],
1056 "diaSourceId": np.arange(1, dtype=int),
1057 "psfFlux": fluxes[0],
1058 "psfFluxErr": np.ones(1),
1059 "midpointMjdTai": times[0]})
1060 diaObjects = make_diaObject_table(objId, plug, band='g')
1061 run_multi_plugin(diaObjects, diaSources, "g", plug)
1062 self.assertTrue(np.isnan(diaObjects.at[objId, "g_psfFluxMaxSlope"]))
1064 # Test max slope value inputing nan values.
1065 fluxes[4] = np.nan
1066 times[7] = np.nan
1067 diaSources = pd.DataFrame(
1068 data={"diaObjectId": n_sources * [objId],
1069 "band": n_sources * ["r"],
1070 "diaSourceId": np.arange(n_sources, dtype=int),
1071 "psfFlux": fluxes,
1072 "psfFluxErr": np.ones(n_sources),
1073 "midpointMjdTai": times})
1074 diaObjects = make_diaObject_table(objId, plug, band='r')
1075 run_multi_plugin(diaObjects, diaSources, "r", plug)
1076 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxMaxSlope"], 2 + 2 / 9)
1079class TestErrMeanDiaPsfFlux(unittest.TestCase):
1081 def testCalculate(self):
1082 """Test error mean calculation.
1083 """
1084 n_sources = 10
1085 objId = 0
1087 # Test mean of the errors.
1088 fluxes = np.linspace(-1, 1, n_sources)
1089 errors = np.linspace(1, 2, n_sources)
1090 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1091 diaSources = pd.DataFrame(
1092 data={"diaObjectId": n_sources * [objId],
1093 "band": n_sources * ["u"],
1094 "diaSourceId": np.arange(n_sources, dtype=int),
1095 "psfFlux": fluxes,
1096 "psfFluxErr": errors})
1098 plug = ErrMeanDiaPsfFlux(ErrMeanDiaPsfFluxConfig(),
1099 "ap_errMeanFlux",
1100 None)
1101 run_multi_plugin(diaObjects, diaSources, "u", plug)
1102 self.assertAlmostEqual(diaObjects.at[objId, "u_psfFluxErrMean"],
1103 np.nanmean(errors).astype(np.float32))
1105 # Test mean of the errors with input nan value.
1106 errors[4] = np.nan
1107 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1108 diaSources = pd.DataFrame(
1109 data={"diaObjectId": n_sources * [objId],
1110 "band": n_sources * ["r"],
1111 "diaSourceId": np.arange(n_sources, dtype=int),
1112 "psfFlux": fluxes,
1113 "psfFluxErr": errors})
1114 run_multi_plugin(diaObjects, diaSources, "r", plug)
1115 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxErrMean"],
1116 np.nanmean(errors).astype(np.float32))
1119class TestLinearFitDiaPsfFlux(unittest.TestCase):
1121 def testCalculate(self):
1122 """Test a linear fit to flux vs time.
1123 """
1124 n_sources = 10
1125 objId = 0
1127 # Test best fit linear model.
1128 fluxes = np.linspace(-1, 1, n_sources)
1129 errors = np.linspace(1, 2, n_sources)
1130 times = np.linspace(0, 1, n_sources)
1131 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1132 diaSources = pd.DataFrame(
1133 data={"diaObjectId": n_sources * [objId],
1134 "band": n_sources * ["u"],
1135 "diaSourceId": np.arange(n_sources, dtype=int),
1136 "psfFlux": fluxes,
1137 "psfFluxErr": errors,
1138 "midpointMjdTai": times})
1140 plug = LinearFitDiaPsfFlux(LinearFitDiaPsfFluxConfig(),
1141 "ap_LinearFit",
1142 None)
1143 run_multi_plugin(diaObjects, diaSources, "u", plug)
1144 self.assertAlmostEqual(diaObjects.loc[objId, "u_psfFluxLinearSlope"],
1145 2.)
1146 self.assertAlmostEqual(diaObjects.loc[objId, "u_psfFluxLinearIntercept"],
1147 -1.)
1149 # Test best fit linear model with input nans.
1150 fluxes[7] = np.nan
1151 errors[4] = np.nan
1152 times[2] = np.nan
1153 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1154 diaSources = pd.DataFrame(
1155 data={"diaObjectId": n_sources * [objId],
1156 "band": n_sources * ["r"],
1157 "diaSourceId": np.arange(n_sources, dtype=int),
1158 "psfFlux": fluxes,
1159 "psfFluxErr": errors,
1160 "midpointMjdTai": times})
1161 run_multi_plugin(diaObjects, diaSources, "r", plug)
1162 self.assertAlmostEqual(diaObjects.loc[objId, "r_psfFluxLinearSlope"], 2.)
1163 self.assertAlmostEqual(diaObjects.loc[objId, "r_psfFluxLinearIntercept"],
1164 -1.)
1167class TestStetsonJDiaPsfFlux(unittest.TestCase):
1169 def testCalculate(self):
1170 """Test the stetsonJ statistic.
1171 """
1172 n_sources = 10
1173 objId = 0
1175 # Test stetsonJ calculation.
1176 fluxes = np.linspace(-1, 1, n_sources)
1177 errors = np.ones(n_sources)
1178 diaObjects = pd.DataFrame({"diaObjectId": [objId],
1179 "u_psfFluxMean": [np.nanmean(fluxes)]})
1180 diaSources = pd.DataFrame(
1181 data={"diaObjectId": n_sources * [objId],
1182 "band": n_sources * ["u"],
1183 "diaSourceId": np.arange(n_sources, dtype=int),
1184 "psfFlux": fluxes,
1185 "psfFluxErr": errors})
1187 plug = StetsonJDiaPsfFlux(StetsonJDiaPsfFluxConfig(),
1188 "ap_StetsonJ",
1189 None)
1190 run_multi_plugin(diaObjects, diaSources, "u", plug)
1191 # Expected StetsonJ for the values created. Confirmed using Cesimum's
1192 # implementation. http://github.com/cesium-ml/cesium
1193 self.assertAlmostEqual(diaObjects.loc[objId, "u_psfFluxStetsonJ"],
1194 -0.5958393936080928)
1196 # Test stetsonJ calculation returns nan on single input.
1197 diaObjects = pd.DataFrame({"diaObjectId": [objId],
1198 "g_psfFluxMean": [np.nanmean(fluxes)]})
1199 diaSources = pd.DataFrame(
1200 data={"diaObjectId": 1 * [objId],
1201 "band": 1 * ["g"],
1202 "diaSourceId": np.arange(1, dtype=int),
1203 "psfFlux": fluxes[0],
1204 "psfFluxErr": errors[0]})
1205 run_multi_plugin(diaObjects, diaSources, "g", plug)
1206 self.assertTrue(np.isnan(diaObjects.at[objId, "g_psfFluxStetsonJ"]))
1208 # Test stetsonJ calculation returns when nans are input.
1209 fluxes[7] = np.nan
1210 errors[4] = np.nan
1211 nonNanMask = np.logical_and(~np.isnan(fluxes),
1212 ~np.isnan(errors))
1213 diaObjects = pd.DataFrame(
1214 {"diaObjectId": [objId],
1215 "r_psfFluxMean": [np.average(fluxes[nonNanMask],
1216 weights=errors[nonNanMask])]})
1217 diaSources = pd.DataFrame(
1218 data={"diaObjectId": n_sources * [objId],
1219 "band": n_sources * ["r"],
1220 "diaSourceId": np.arange(n_sources, dtype=int),
1221 "psfFlux": fluxes,
1222 "psfFluxErr": errors})
1223 run_multi_plugin(diaObjects, diaSources, "r", plug)
1224 self.assertAlmostEqual(diaObjects.at[objId, "r_psfFluxStetsonJ"],
1225 -0.5412797916187173)
1228class TestWeightedMeanDiaTotFlux(unittest.TestCase):
1230 def testCalculate(self):
1231 """Test mean value calculation.
1232 """
1233 n_sources = 10
1234 objId = 0
1236 # Test test mean on scienceFlux.
1237 fluxes = np.linspace(-1, 1, n_sources)
1238 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1239 diaSources = pd.DataFrame(
1240 data={"diaObjectId": n_sources * [objId],
1241 "band": n_sources * ["u"],
1242 "diaSourceId": np.arange(n_sources, dtype=int),
1243 "scienceFlux": fluxes,
1244 "scienceFluxErr": np.ones(n_sources)})
1246 plug = WeightedMeanDiaTotFlux(WeightedMeanDiaTotFluxConfig(),
1247 "ap_meanTotFlux",
1248 None)
1249 run_multi_plugin(diaObjects, diaSources, "u", plug)
1251 self.assertAlmostEqual(diaObjects.at[objId, "u_scienceFluxMean"], 0.0)
1252 self.assertAlmostEqual(diaObjects.at[objId, "u_scienceFluxMeanErr"],
1253 np.sqrt(1 / n_sources))
1255 # Test test mean on scienceFlux with input nans
1256 fluxes[4] = np.nan
1257 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1258 diaSources = pd.DataFrame(
1259 data={"diaObjectId": n_sources * [objId],
1260 "band": n_sources * ["r"],
1261 "diaSourceId": np.arange(n_sources, dtype=int),
1262 "scienceFlux": fluxes,
1263 "scienceFluxErr": np.ones(n_sources)})
1264 run_multi_plugin(diaObjects, diaSources, "r", plug)
1266 self.assertAlmostEqual(diaObjects.at[objId, "r_scienceFluxMean"],
1267 np.nanmean(fluxes))
1268 self.assertAlmostEqual(diaObjects.at[objId, "r_scienceFluxMeanErr"],
1269 np.sqrt(1 / (n_sources - 1)))
1272def generatePeriodicData(n=10, period=10):
1273 """Generate noisy, sinusoidally-varying periodic data for testing Lomb-
1274 Scargle Periodogram.
1276 The returned fluxes will have, within the errors, the passed-in period and
1277 a power close to 1, because the fluxes are purely sinusoidal.
1279 Parameters
1280 ----------
1281 n : int
1282 Number of data points to generate.
1283 period : float
1284 Period of the periodic signal.
1286 Returns
1287 -------
1288 t : np.ndarray
1289 Time values.
1290 y_obs : np.ndarray
1291 Observed flux values.
1292 """
1293 np.random.seed(42)
1295 t = np.linspace(-2*np.pi, 2*np.pi, n) + 100*np.random.random(n)
1296 y = 10 + np.sin(2 * np.pi * t / period)
1297 y_obs = np.random.normal(y, 0.001)
1299 return t, y_obs
1302class TestMultiLombScarglePeriodogram(lsst.utils.tests.TestCase):
1304 def testCalculate(self):
1305 """Test Mulitband Lomb Scargle Periodogram."""
1306 n_sources = 10
1307 objId = 0
1309 # Create synthetic multi-band data
1310 times, fluxes = generatePeriodicData(n_sources, period=10)
1311 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1312 diaSources = pd.DataFrame(
1313 data={"diaObjectId": n_sources * [objId],
1314 "band": n_sources//2 * ["u"] + n_sources//2 * ["g"],
1315 "diaSourceId": np.arange(n_sources, dtype=int),
1316 "midpointMjdTai": times,
1317 "psfFlux": fluxes,
1318 "psfFluxErr": 1e-3+np.zeros(n_sources)})
1320 plugin = LombScarglePeriodogramMulti(LombScarglePeriodogramMultiConfig(),
1321 "ap_lombScarglePeriodogramMulti",
1322 None)
1324 run_multiband_plugin(diaObjects, diaSources, plugin)
1325 self.assertAlmostEqual(diaObjects.at[objId, "multiPeriod"], 10, delta=0.04)
1326 self.assertAlmostEqual(diaObjects.at[objId, "multiPower"], 1, delta=1e-2)
1327 # This implementation of LS returns a normalized power < 1.
1328 self.assertLess(diaObjects.at[objId, "multiPower"], 1)
1329 self.assertAlmostEqual(diaObjects.at[objId, "multiFap"], 0, delta=0.04)
1330 # Note: The below values are empirical, but seem reasonable, and
1331 # test that we get values for each band.
1332 self.assertAlmostEqual(diaObjects.at[objId, "u_multiAmp"], 0.029, delta=0.01)
1333 self.assertAlmostEqual(diaObjects.at[objId, "g_multiAmp"], 0.029, delta=0.01)
1334 self.assertAlmostEqual(diaObjects.at[objId, "u_multiPhase"], -2.0, delta=0.2)
1335 self.assertAlmostEqual(diaObjects.at[objId, "g_multiPhase"], 1.0, delta=0.1)
1337 def testCalculateTwoSources(self):
1338 """Test Mulitband Lomb Scargle Periodogram with 2 sources (minimum
1339 detections = 5), which will result in NaN output."""
1340 objId = 0
1341 n_sources = 2
1342 times, fluxes = generatePeriodicData(n_sources, period=10)
1343 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1344 diaSources = pd.DataFrame(
1345 data={"diaObjectId": n_sources * [objId],
1346 "band": n_sources * ["u"],
1347 "diaSourceId": np.arange(n_sources, dtype=int),
1348 "midpointMjdTai": times,
1349 "psfFlux": fluxes,
1350 "psfFluxErr": 1e-3+np.zeros(n_sources)})
1352 plugin = LombScarglePeriodogramMulti(LombScarglePeriodogramMultiConfig(),
1353 "ap_lombScarglePeriodogramMulti",
1354 None)
1356 run_multi_plugin(diaObjects, diaSources, "u", plugin)
1357 self.assertTrue(np.isnan(diaObjects.at[objId, "multiPeriod"]))
1358 self.assertTrue(np.isnan(diaObjects.at[objId, "multiPower"]))
1359 self.assertTrue(np.isnan(diaObjects.at[objId, "multiFap"]))
1362class TestLombScarglePeriodogram(lsst.utils.tests.TestCase):
1364 def testCalculate(self):
1365 """Test Lomb Scargle Periodogram."""
1366 n_sources = 10
1367 objId = 0
1369 # Test period calculation.
1370 times, fluxes = generatePeriodicData(n_sources, period=10)
1371 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1372 diaSources = pd.DataFrame(
1373 data={"diaObjectId": n_sources * [objId],
1374 "band": n_sources * ["u"],
1375 "diaSourceId": np.arange(n_sources, dtype=int),
1376 "midpointMjdTai": times,
1377 "psfFlux": fluxes,
1378 "psfFluxErr": 1e-3+np.zeros(n_sources)})
1380 plugin = LombScarglePeriodogram(LombScarglePeriodogramConfig(),
1381 "ap_lombScarglePeriodogram",
1382 None)
1384 run_multi_plugin(diaObjects, diaSources, "u", plugin)
1385 self.assertAlmostEqual(diaObjects.at[objId, "u_period"], 10, delta=0.04)
1386 # This implementation of LS returns a normalized power < 1.
1387 self.assertAlmostEqual(diaObjects.at[objId, "u_power"], 1, delta=1e-2)
1388 self.assertLess(diaObjects.at[objId, "u_power"], 1)
1390 # Test that we get the same result with a NaN flux.
1391 diaSources.loc[4, "psfFlux"] = np.nan
1392 diaObjects = pd.DataFrame({"diaObjectId": [objId]})
1393 diaSources = pd.DataFrame(
1394 data={"diaObjectId": n_sources * [objId],
1395 "band": n_sources * ["r"],
1396 "diaSourceId": np.arange(n_sources, dtype=int),
1397 "midpointMjdTai": times,
1398 "psfFlux": fluxes,
1399 "psfFluxErr": np.ones(n_sources)})
1400 run_multi_plugin(diaObjects, diaSources, "r", plugin)
1401 self.assertAlmostEqual(diaObjects.at[objId, "r_period"], 10, delta=0.04)
1402 self.assertAlmostEqual(diaObjects.at[objId, "r_power"], 1, delta=1e-2)
1403 # This implementation of LS returns a normalized power < 1.
1404 self.assertLess(diaObjects.at[objId, "r_power"], 1)
1407class TestSigmaDiaTotFlux(unittest.TestCase):
1409 def testCalculate(self):
1410 """Test flux scatter calculation.
1411 """
1412 n_sources = 10
1413 objId = 0
1415 # Test test scatter on scienceFlux.
1416 fluxes = np.linspace(-1, 1, n_sources)
1417 diaSources = pd.DataFrame(
1418 data={"diaObjectId": n_sources * [objId],
1419 "band": n_sources * ["u"],
1420 "diaSourceId": np.arange(n_sources, dtype=int),
1421 "scienceFlux": fluxes,
1422 "scienceFluxErr": np.ones(n_sources)})
1424 plug = SigmaDiaTotFlux(SigmaDiaTotFluxConfig(),
1425 "ap_sigmaTotFlux",
1426 None)
1427 diaObjects = make_diaObject_table(objId, plug, band='u')
1428 run_multi_plugin(diaObjects, diaSources, "u", plug)
1429 self.assertAlmostEqual(diaObjects.at[objId, "u_scienceFluxSigma"],
1430 np.nanstd(fluxes, ddof=1))
1432 # Test test scatter on scienceFlux returns nan on 1 input.
1433 diaSources = pd.DataFrame(
1434 data={"diaObjectId": 1 * [objId],
1435 "band": 1 * ["g"],
1436 "diaSourceId": np.arange(1, dtype=int),
1437 "scienceFlux": fluxes[0],
1438 "scienceFluxErr": np.ones(1)})
1439 diaObjects = make_diaObject_table(objId, plug, band='g')
1440 run_multi_plugin(diaObjects, diaSources, "g", plug)
1441 self.assertTrue(np.isnan(diaObjects.at[objId, "g_scienceFluxSigma"]))
1443 # Test test scatter on scienceFlux takes input nans.
1444 fluxes[4] = np.nan
1445 diaSources = pd.DataFrame(
1446 data={"diaObjectId": n_sources * [objId],
1447 "band": n_sources * ["r"],
1448 "diaSourceId": np.arange(n_sources, dtype=int),
1449 "scienceFlux": fluxes,
1450 "scienceFluxErr": np.ones(n_sources)})
1451 diaObjects = make_diaObject_table(objId, plug, band='r')
1452 run_multi_plugin(diaObjects, diaSources, "r", plug)
1453 self.assertAlmostEqual(diaObjects.at[objId, "r_scienceFluxSigma"],
1454 np.nanstd(fluxes, ddof=1))
1457def skew_wrapper(values):
1458 """Compute scipy skew, omitting nans.
1460 This version works with both scipy<1.9 (where it erroneously returns a
1461 masked array) and scipy>=1.9 (where it correctly returns a float).
1463 Parameters
1464 ----------
1465 values : `np.ndarray`
1467 Returns
1468 -------
1469 skew_value : `float`
1470 """
1471 value = skew(values, bias=False, nan_policy="omit")
1472 if isinstance(value, np.ma.masked_array): 1472 ↛ 1473line 1472 didn't jump to line 1473 because the condition on line 1472 was never true
1473 return value.data
1474 else:
1475 return value
1478class MemoryTester(lsst.utils.tests.MemoryTestCase):
1479 pass
1482def setup_module(module):
1483 lsst.utils.tests.init()
1486if __name__ == "__main__": 1486 ↛ 1487line 1486 didn't jump to line 1487 because the condition on line 1486 was never true
1487 lsst.utils.tests.init()
1488 unittest.main()