Coverage for python/lsst/meas/base/diaCalculationPlugins.py: 59%

378 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-01 12:04 +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/>. 

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.sphgeom as sphgeom 

39 

40from .diaCalculation import ( 

41 DiaObjectCalculationPluginConfig, 

42 DiaObjectCalculationPlugin) 

43from .pluginRegistry import register 

44 

45__all__ = ("MeanDiaPositionConfig", "MeanDiaPosition", 

46 "HTMIndexDiaPosition", "HTMIndexDiaPositionConfig", 

47 "NumDiaSourcesDiaPlugin", "NumDiaSourcesDiaPluginConfig", 

48 "SimpleSourceFlagDiaPlugin", "SimpleSourceFlagDiaPluginConfig", 

49 "WeightedMeanDiaPsfFluxConfig", "WeightedMeanDiaPsfFlux", 

50 "PercentileDiaPsfFlux", "PercentileDiaPsfFluxConfig", 

51 "SigmaDiaPsfFlux", "SigmaDiaPsfFluxConfig", 

52 "Chi2DiaPsfFlux", "Chi2DiaPsfFluxConfig", 

53 "MadDiaPsfFlux", "MadDiaPsfFluxConfig", 

54 "SkewDiaPsfFlux", "SkewDiaPsfFluxConfig", 

55 "MinMaxDiaPsfFlux", "MinMaxDiaPsfFluxConfig", 

56 "MaxSlopeDiaPsfFlux", "MaxSlopeDiaPsfFluxConfig", 

57 "ErrMeanDiaPsfFlux", "ErrMeanDiaPsfFluxConfig", 

58 "LinearFitDiaPsfFlux", "LinearFitDiaPsfFluxConfig", 

59 "StetsonJDiaPsfFlux", "StetsonJDiaPsfFluxConfig", 

60 "WeightedMeanDiaTotFlux", "WeightedMeanDiaTotFluxConfig", 

61 "SigmaDiaTotFlux", "SigmaDiaTotFluxConfig") 

62 

63 

64def catchWarnings(_func=None, *, warns=[]): 

65 """Decorator for generically catching numpy warnings. 

66 """ 

67 def decoratorCatchWarnings(func): 

68 @functools.wraps(func) 

69 def wrapperCatchWarnings(*args, **kwargs): 

70 with warnings.catch_warnings(): 

71 for val in warns: 

72 warnings.filterwarnings("ignore", val) 

73 return func(*args, **kwargs) 

74 return wrapperCatchWarnings 

75 

76 if _func is None: 76 ↛ 79line 76 didn't jump to line 79, because the condition on line 76 was never false

77 return decoratorCatchWarnings 

78 else: 

79 return decoratorCatchWarnings(_func) 

80 

81 

82class MeanDiaPositionConfig(DiaObjectCalculationPluginConfig): 

83 pass 

84 

85 

86@register("ap_meanPosition") 

87class MeanDiaPosition(DiaObjectCalculationPlugin): 

88 """Compute the mean position of a DiaObject given a set of DiaSources. 

89 """ 

90 

91 ConfigClass = MeanDiaPositionConfig 

92 

93 plugType = 'multi' 

94 

95 outputCols = ["ra", "dec", "radecMjdTai"] 

96 needsFilter = False 

97 

98 @classmethod 

99 def getExecutionOrder(cls): 

100 return cls.DEFAULT_CATALOGCALCULATION 

101 

102 def calculate(self, diaObjects, diaSources, **kwargs): 

103 """Compute the mean ra/dec position of the diaObject given the 

104 diaSource locations. 

105 

106 Parameters 

107 ---------- 

108 diaObjects : `pandas.DataFrame` 

109 Summary objects to store values in. 

110 diaSources : `pandas.DataFrame` or `pandas.DataFrameGroupBy` 

111 Catalog of DiaSources summarized by this DiaObject. 

112 **kwargs 

113 Any additional keyword arguments that may be passed to the plugin. 

114 """ 

115 for outCol in self.outputCols: 

116 if outCol not in diaObjects.columns: 

117 diaObjects[outCol] = np.nan 

118 

119 def _computeMeanPos(df): 

120 aveCoord = geom.averageSpherePoint( 

121 list(geom.SpherePoint(src["ra"], src["dec"], geom.degrees) 

122 for idx, src in df.iterrows())) 

123 ra = aveCoord.getRa().asDegrees() 

124 dec = aveCoord.getDec().asDegrees() 

125 if np.isnan(ra) or np.isnan(dec): 

126 radecMjdTai = np.nan 

127 else: 

128 radecMjdTai = df["midpointMjdTai"].max() 

129 

130 return pd.Series({"ra": aveCoord.getRa().asDegrees(), 

131 "dec": aveCoord.getDec().asDegrees(), 

132 "radecMjdTai": radecMjdTai}) 

133 

134 ans = diaSources.apply(_computeMeanPos) 

135 diaObjects.loc[:, ["ra", "dec", "radecMjdTai"]] = ans 

136 

137 

138class HTMIndexDiaPositionConfig(DiaObjectCalculationPluginConfig): 

139 

140 htmLevel = pexConfig.Field( 

141 dtype=int, 

142 doc="Level of the HTM pixelization.", 

143 default=20, 

144 ) 

145 

146 

147@register("ap_HTMIndex") 

148class HTMIndexDiaPosition(DiaObjectCalculationPlugin): 

149 """Compute the mean position of a DiaObject given a set of DiaSources. 

150 

151 Notes 

152 ----- 

153 This plugin was implemented to satisfy requirements of old APDB interface 

154 which required ``pixelId`` column in DiaObject with HTM20 index. APDB 

155 interface had migrated to not need that information, but we keep this 

156 plugin in case it may be useful for something else. 

157 """ 

158 ConfigClass = HTMIndexDiaPositionConfig 

159 

160 plugType = 'single' 

161 

162 inputCols = ["ra", "dec"] 

163 outputCols = ["pixelId"] 

164 needsFilter = False 

165 

166 def __init__(self, config, name, metadata): 

167 DiaObjectCalculationPlugin.__init__(self, config, name, metadata) 

168 self.pixelator = sphgeom.HtmPixelization(self.config.htmLevel) 

169 

170 @classmethod 

171 def getExecutionOrder(cls): 

172 return cls.FLUX_MOMENTS_CALCULATED 

173 

174 def calculate(self, diaObjects, diaObjectId, **kwargs): 

175 """Compute the mean position of a DiaObject given a set of DiaSources 

176 

177 Parameters 

178 ---------- 

179 diaObjects : `pandas.dataFrame` 

180 Summary objects to store values in and read ra/dec from. 

181 diaObjectId : `int` 

182 Id of the diaObject to update. 

183 **kwargs 

184 Any additional keyword arguments that may be passed to the plugin. 

185 """ 

186 sphPoint = geom.SpherePoint( 

187 diaObjects.at[diaObjectId, "ra"] * geom.degrees, 

188 diaObjects.at[diaObjectId, "dec"] * geom.degrees) 

189 diaObjects.at[diaObjectId, "pixelId"] = self.pixelator.index( 

190 sphPoint.getVector()) 

191 

192 

193class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig): 

194 pass 

195 

196 

197@register("ap_nDiaSources") 

198class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin): 

199 """Compute the total number of DiaSources associated with this DiaObject. 

200 """ 

201 

202 ConfigClass = NumDiaSourcesDiaPluginConfig 

203 outputCols = ["nDiaSources"] 

204 plugType = "multi" 

205 needsFilter = False 

206 

207 @classmethod 

208 def getExecutionOrder(cls): 

209 return cls.DEFAULT_CATALOGCALCULATION 

210 

211 def calculate(self, diaObjects, diaSources, **kwargs): 

212 """Compute the total number of DiaSources associated with this DiaObject. 

213 

214 Parameters 

215 ---------- 

216 diaObject : `dict` 

217 Summary object to store values in and read ra/dec from. 

218 **kwargs 

219 Any additional keyword arguments that may be passed to the plugin. 

220 """ 

221 diaObjects.loc[:, "nDiaSources"] = diaSources.diaObjectId.count() 

222 

223 

224class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig): 

225 pass 

226 

227 

228@register("ap_diaObjectFlag") 

229class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin): 

230 """Find if any DiaSource is flagged. 

231 

232 Set the DiaObject flag if any DiaSource is flagged. 

233 """ 

234 

235 ConfigClass = NumDiaSourcesDiaPluginConfig 

236 outputCols = ["flags"] 

237 plugType = "multi" 

238 needsFilter = False 

239 

240 @classmethod 

241 def getExecutionOrder(cls): 

242 return cls.DEFAULT_CATALOGCALCULATION 

243 

244 def calculate(self, diaObjects, diaSources, **kwargs): 

245 """Find if any DiaSource is flagged. 

246 

247 Set the DiaObject flag if any DiaSource is flagged. 

248 

249 Parameters 

250 ---------- 

251 diaObject : `dict` 

252 Summary object to store values in and read ra/dec from. 

253 **kwargs 

254 Any additional keyword arguments that may be passed to the plugin. 

255 """ 

256 diaObjects.loc[:, "flags"] = diaSources.flags.any().astype(np.uint64) 

257 

258 

259class WeightedMeanDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

260 pass 

261 

262 

263@register("ap_meanFlux") 

264class WeightedMeanDiaPsfFlux(DiaObjectCalculationPlugin): 

265 """Compute the weighted mean and mean error on the point source fluxes 

266 of the DiaSource measured on the difference image. 

267 

268 Additionally store number of usable data points. 

269 """ 

270 

271 ConfigClass = WeightedMeanDiaPsfFluxConfig 

272 outputCols = ["psfFluxMean", "psfFluxMeanErr", "psfFluxNdata"] 

273 plugType = "multi" 

274 needsFilter = True 

275 

276 @classmethod 

277 def getExecutionOrder(cls): 

278 return cls.DEFAULT_CATALOGCALCULATION 

279 

280 @catchWarnings(warns=["invalid value encountered", 

281 "divide by zero"]) 

282 def calculate(self, 

283 diaObjects, 

284 diaSources, 

285 filterDiaSources, 

286 band, 

287 **kwargs): 

288 """Compute the weighted mean and mean error of the point source flux. 

289 

290 Parameters 

291 ---------- 

292 diaObject : `dict` 

293 Summary object to store values in. 

294 diaSources : `pandas.DataFrame` 

295 DataFrame representing all diaSources associated with this 

296 diaObject. 

297 filterDiaSources : `pandas.DataFrame` 

298 DataFrame representing diaSources associated with this 

299 diaObject that are observed in the band pass ``band``. 

300 band : `str` 

301 Simple, string name of the filter for the flux being calculated. 

302 **kwargs 

303 Any additional keyword arguments that may be passed to the plugin. 

304 """ 

305 meanName = "{}_psfFluxMean".format(band) 

306 errName = "{}_psfFluxMeanErr".format(band) 

307 nDataName = "{}_psfFluxNdata".format(band) 

308 if meanName not in diaObjects.columns: 

309 diaObjects[meanName] = np.nan 

310 if errName not in diaObjects.columns: 

311 diaObjects[errName] = np.nan 

312 if nDataName not in diaObjects.columns: 

313 diaObjects[nDataName] = 0 

314 

315 def _weightedMean(df): 

316 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]), 

317 np.isnan(df["psfFluxErr"]))] 

318 tot_weight = np.nansum(1 / tmpDf["psfFluxErr"] ** 2) 

319 fluxMean = np.nansum(tmpDf["psfFlux"] 

320 / tmpDf["psfFluxErr"] ** 2) 

321 fluxMean /= tot_weight 

322 if tot_weight > 0: 

323 fluxMeanErr = np.sqrt(1 / tot_weight) 

324 else: 

325 fluxMeanErr = np.nan 

326 nFluxData = len(tmpDf) 

327 

328 return pd.Series({meanName: fluxMean, 

329 errName: fluxMeanErr, 

330 nDataName: nFluxData}, 

331 dtype="object") 

332 

333 diaObjects.loc[:, [meanName, errName, nDataName]] = \ 

334 filterDiaSources.apply(_weightedMean) 

335 

336 

337class PercentileDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

338 percentiles = pexConfig.ListField( 

339 dtype=int, 

340 default=[5, 25, 50, 75, 95], 

341 doc="Percentiles to calculate to compute values for. Should be " 

342 "integer values." 

343 ) 

344 

345 

346@register("ap_percentileFlux") 

347class PercentileDiaPsfFlux(DiaObjectCalculationPlugin): 

348 """Compute percentiles of diaSource fluxes. 

349 """ 

350 

351 ConfigClass = PercentileDiaPsfFluxConfig 

352 # Output columns are created upon instantiation of the class. 

353 outputCols = [] 

354 plugType = "multi" 

355 needsFilter = True 

356 

357 def __init__(self, config, name, metadata, **kwargs): 

358 DiaObjectCalculationPlugin.__init__(self, 

359 config, 

360 name, 

361 metadata, 

362 **kwargs) 

363 self.outputCols = ["psfFluxPercentile{:02d}".format(percent) 

364 for percent in self.config.percentiles] 

365 

366 @classmethod 

367 def getExecutionOrder(cls): 

368 return cls.DEFAULT_CATALOGCALCULATION 

369 

370 @catchWarnings(warns=["All-NaN slice encountered"]) 

371 def calculate(self, 

372 diaObjects, 

373 diaSources, 

374 filterDiaSources, 

375 band, 

376 **kwargs): 

377 """Compute the percentile fluxes of the point source flux. 

378 

379 Parameters 

380 ---------- 

381 diaObject : `dict` 

382 Summary object to store values in. 

383 diaSources : `pandas.DataFrame` 

384 DataFrame representing all diaSources associated with this 

385 diaObject. 

386 filterDiaSources : `pandas.DataFrame` 

387 DataFrame representing diaSources associated with this 

388 diaObject that are observed in the band pass ``band``. 

389 band : `str` 

390 Simple, string name of the filter for the flux being calculated. 

391 **kwargs 

392 Any additional keyword arguments that may be passed to the plugin. 

393 """ 

394 pTileNames = [] 

395 for tilePercent in self.config.percentiles: 

396 pTileName = "{}_psfFluxPercentile{:02d}".format(band, 

397 tilePercent) 

398 pTileNames.append(pTileName) 

399 if pTileName not in diaObjects.columns: 

400 diaObjects[pTileName] = np.nan 

401 

402 def _fluxPercentiles(df): 

403 pTiles = np.nanpercentile(df["psfFlux"], self.config.percentiles) 

404 return pd.Series( 

405 dict((tileName, pTile) 

406 for tileName, pTile in zip(pTileNames, pTiles))) 

407 

408 diaObjects.loc[:, pTileNames] = filterDiaSources.apply(_fluxPercentiles) 

409 

410 

411class SigmaDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

412 pass 

413 

414 

415@register("ap_sigmaFlux") 

416class SigmaDiaPsfFlux(DiaObjectCalculationPlugin): 

417 """Compute scatter of diaSource fluxes. 

418 """ 

419 

420 ConfigClass = SigmaDiaPsfFluxConfig 

421 # Output columns are created upon instantiation of the class. 

422 outputCols = ["psfFluxSigma"] 

423 plugType = "multi" 

424 needsFilter = True 

425 

426 @classmethod 

427 def getExecutionOrder(cls): 

428 return cls.DEFAULT_CATALOGCALCULATION 

429 

430 def calculate(self, 

431 diaObjects, 

432 diaSources, 

433 filterDiaSources, 

434 band, 

435 **kwargs): 

436 """Compute the sigma fluxes of the point source flux. 

437 

438 Parameters 

439 ---------- 

440 diaObject : `dict` 

441 Summary object to store values in. 

442 diaSources : `pandas.DataFrame` 

443 DataFrame representing all diaSources associated with this 

444 diaObject. 

445 filterDiaSources : `pandas.DataFrame` 

446 DataFrame representing diaSources associated with this 

447 diaObject that are observed in the band pass ``band``. 

448 band : `str` 

449 Simple, string name of the filter for the flux being calculated. 

450 **kwargs 

451 Any additional keyword arguments that may be passed to the plugin. 

452 """ 

453 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased 

454 # estimator of scatter (i.e. 'N - 1' instead of 'N'). 

455 diaObjects.loc[:, "{}_psfFluxSigma".format(band)] = \ 

456 filterDiaSources.psfFlux.std() 

457 

458 

459class Chi2DiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

460 pass 

461 

462 

463@register("ap_chi2Flux") 

464class Chi2DiaPsfFlux(DiaObjectCalculationPlugin): 

465 """Compute chi2 of diaSource fluxes. 

466 """ 

467 

468 ConfigClass = Chi2DiaPsfFluxConfig 

469 

470 # Required input Cols 

471 inputCols = ["psfFluxMean"] 

472 # Output columns are created upon instantiation of the class. 

473 outputCols = ["psfFluxChi2"] 

474 plugType = "multi" 

475 needsFilter = True 

476 

477 @classmethod 

478 def getExecutionOrder(cls): 

479 return cls.FLUX_MOMENTS_CALCULATED 

480 

481 @catchWarnings(warns=["All-NaN slice encountered"]) 

482 def calculate(self, 

483 diaObjects, 

484 diaSources, 

485 filterDiaSources, 

486 band, 

487 **kwargs): 

488 """Compute the chi2 of the point source fluxes. 

489 

490 Parameters 

491 ---------- 

492 diaObject : `dict` 

493 Summary object to store values in. 

494 diaSources : `pandas.DataFrame` 

495 DataFrame representing all diaSources associated with this 

496 diaObject. 

497 filterDiaSources : `pandas.DataFrame` 

498 DataFrame representing diaSources associated with this 

499 diaObject that are observed in the band pass ``band``. 

500 band : `str` 

501 Simple, string name of the filter for the flux being calculated. 

502 **kwargs 

503 Any additional keyword arguments that may be passed to the plugin. 

504 """ 

505 meanName = "{}_psfFluxMean".format(band) 

506 

507 def _chi2(df): 

508 delta = (df["psfFlux"] 

509 - diaObjects.at[df.diaObjectId.iat[0], meanName]) 

510 return np.nansum((delta / df["psfFluxErr"]) ** 2) 

511 

512 diaObjects.loc[:, "{}_psfFluxChi2".format(band)] = \ 

513 filterDiaSources.apply(_chi2) 

514 

515 

516class MadDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

517 pass 

518 

519 

520@register("ap_madFlux") 

521class MadDiaPsfFlux(DiaObjectCalculationPlugin): 

522 """Compute median absolute deviation of diaSource fluxes. 

523 """ 

524 

525 ConfigClass = MadDiaPsfFluxConfig 

526 

527 # Required input Cols 

528 # Output columns are created upon instantiation of the class. 

529 outputCols = ["psfFluxMAD"] 

530 plugType = "multi" 

531 needsFilter = True 

532 

533 @classmethod 

534 def getExecutionOrder(cls): 

535 return cls.DEFAULT_CATALOGCALCULATION 

536 

537 @catchWarnings(warns=["All-NaN slice encountered"]) 

538 def calculate(self, 

539 diaObjects, 

540 diaSources, 

541 filterDiaSources, 

542 band, 

543 **kwargs): 

544 """Compute the median absolute deviation of the point source fluxes. 

545 

546 Parameters 

547 ---------- 

548 diaObject : `dict` 

549 Summary object to store values in. 

550 diaSources : `pandas.DataFrame` 

551 DataFrame representing all diaSources associated with this 

552 diaObject. 

553 filterDiaSources : `pandas.DataFrame` 

554 DataFrame representing diaSources associated with this 

555 diaObject that are observed in the band pass ``band``. 

556 band : `str` 

557 Simple, string name of the filter for the flux being calculated. 

558 **kwargs 

559 Any additional keyword arguments that may be passed to the plugin. 

560 """ 

561 diaObjects.loc[:, "{}_psfFluxMAD".format(band)] = \ 

562 filterDiaSources.psfFlux.apply(median_absolute_deviation, 

563 ignore_nan=True) 

564 

565 

566class SkewDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

567 pass 

568 

569 

570@register("ap_skewFlux") 

571class SkewDiaPsfFlux(DiaObjectCalculationPlugin): 

572 """Compute the skew of diaSource fluxes. 

573 """ 

574 

575 ConfigClass = SkewDiaPsfFluxConfig 

576 

577 # Required input Cols 

578 # Output columns are created upon instantiation of the class. 

579 outputCols = ["psfFluxSkew"] 

580 plugType = "multi" 

581 needsFilter = True 

582 

583 @classmethod 

584 def getExecutionOrder(cls): 

585 return cls.DEFAULT_CATALOGCALCULATION 

586 

587 def calculate(self, 

588 diaObjects, 

589 diaSources, 

590 filterDiaSources, 

591 band, 

592 **kwargs): 

593 """Compute the skew of the point source fluxes. 

594 

595 Parameters 

596 ---------- 

597 diaObject : `dict` 

598 Summary object to store values in. 

599 diaSources : `pandas.DataFrame` 

600 DataFrame representing all diaSources associated with this 

601 diaObject. 

602 filterDiaSources : `pandas.DataFrame` 

603 DataFrame representing diaSources associated with this 

604 diaObject that are observed in the band pass ``band``. 

605 band : `str` 

606 Simple, string name of the filter for the flux being calculated. 

607 **kwargs 

608 Any additional keyword arguments that may be passed to the plugin. 

609 """ 

610 diaObjects.loc[:, "{}_psfFluxSkew".format(band)] = \ 

611 filterDiaSources.psfFlux.skew() 

612 

613 

614class MinMaxDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

615 pass 

616 

617 

618@register("ap_minMaxFlux") 

619class MinMaxDiaPsfFlux(DiaObjectCalculationPlugin): 

620 """Compute min/max of diaSource fluxes. 

621 """ 

622 

623 ConfigClass = MinMaxDiaPsfFluxConfig 

624 

625 # Required input Cols 

626 # Output columns are created upon instantiation of the class. 

627 outputCols = ["psfFluxMin", "psfFluxMax"] 

628 plugType = "multi" 

629 needsFilter = True 

630 

631 @classmethod 

632 def getExecutionOrder(cls): 

633 return cls.DEFAULT_CATALOGCALCULATION 

634 

635 def calculate(self, 

636 diaObjects, 

637 diaSources, 

638 filterDiaSources, 

639 band, 

640 **kwargs): 

641 """Compute min/max of the point source fluxes. 

642 

643 Parameters 

644 ---------- 

645 diaObject : `dict` 

646 Summary object to store values in. 

647 diaSources : `pandas.DataFrame` 

648 DataFrame representing all diaSources associated with this 

649 diaObject. 

650 filterDiaSources : `pandas.DataFrame` 

651 DataFrame representing diaSources associated with this 

652 diaObject that are observed in the band pass ``band``. 

653 band : `str` 

654 Simple, string name of the filter for the flux being calculated. 

655 **kwargs 

656 Any additional keyword arguments that may be passed to the plugin. 

657 """ 

658 minName = "{}_psfFluxMin".format(band) 

659 if minName not in diaObjects.columns: 

660 diaObjects[minName] = np.nan 

661 maxName = "{}_psfFluxMax".format(band) 

662 if maxName not in diaObjects.columns: 

663 diaObjects[maxName] = np.nan 

664 

665 diaObjects.loc[:, minName] = filterDiaSources.psfFlux.min() 

666 diaObjects.loc[:, maxName] = filterDiaSources.psfFlux.max() 

667 

668 

669class MaxSlopeDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

670 pass 

671 

672 

673@register("ap_maxSlopeFlux") 

674class MaxSlopeDiaPsfFlux(DiaObjectCalculationPlugin): 

675 """Compute the maximum ratio time ordered deltaFlux / deltaTime. 

676 """ 

677 

678 ConfigClass = MinMaxDiaPsfFluxConfig 

679 

680 # Required input Cols 

681 # Output columns are created upon instantiation of the class. 

682 outputCols = ["psfFluxMaxSlope"] 

683 plugType = "multi" 

684 needsFilter = True 

685 

686 @classmethod 

687 def getExecutionOrder(cls): 

688 return cls.DEFAULT_CATALOGCALCULATION 

689 

690 def calculate(self, 

691 diaObjects, 

692 diaSources, 

693 filterDiaSources, 

694 band, 

695 **kwargs): 

696 """Compute the maximum ratio time ordered deltaFlux / deltaTime. 

697 

698 Parameters 

699 ---------- 

700 diaObject : `dict` 

701 Summary object to store values in. 

702 diaSources : `pandas.DataFrame` 

703 DataFrame representing all diaSources associated with this 

704 diaObject. 

705 filterDiaSources : `pandas.DataFrame` 

706 DataFrame representing diaSources associated with this 

707 diaObject that are observed in the band pass ``band``. 

708 band : `str` 

709 Simple, string name of the filter for the flux being calculated. 

710 **kwargs 

711 Any additional keyword arguments that may be passed to the plugin. 

712 """ 

713 

714 def _maxSlope(df): 

715 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]), 

716 np.isnan(df["midpointMjdTai"]))] 

717 if len(tmpDf) < 2: 

718 return np.nan 

719 times = tmpDf["midpointMjdTai"].to_numpy() 

720 timeArgs = times.argsort() 

721 times = times[timeArgs] 

722 fluxes = tmpDf["psfFlux"].to_numpy()[timeArgs] 

723 return (np.diff(fluxes) / np.diff(times)).max() 

724 

725 diaObjects.loc[:, "{}_psfFluxMaxSlope".format(band)] = \ 

726 filterDiaSources.apply(_maxSlope) 

727 

728 

729class ErrMeanDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

730 pass 

731 

732 

733@register("ap_meanErrFlux") 

734class ErrMeanDiaPsfFlux(DiaObjectCalculationPlugin): 

735 """Compute the mean of the dia source errors. 

736 """ 

737 

738 ConfigClass = ErrMeanDiaPsfFluxConfig 

739 

740 # Required input Cols 

741 # Output columns are created upon instantiation of the class. 

742 outputCols = ["psfFluxErrMean"] 

743 plugType = "multi" 

744 needsFilter = True 

745 

746 @classmethod 

747 def getExecutionOrder(cls): 

748 return cls.DEFAULT_CATALOGCALCULATION 

749 

750 def calculate(self, 

751 diaObjects, 

752 diaSources, 

753 filterDiaSources, 

754 band, 

755 **kwargs): 

756 """Compute the mean of the dia source errors. 

757 

758 Parameters 

759 ---------- 

760 diaObject : `dict` 

761 Summary object to store values in. 

762 diaSources : `pandas.DataFrame` 

763 DataFrame representing all diaSources associated with this 

764 diaObject. 

765 filterDiaSources : `pandas.DataFrame` 

766 DataFrame representing diaSources associated with this 

767 diaObject that are observed in the band pass ``band``. 

768 band : `str` 

769 Simple, string name of the filter for the flux being calculated. 

770 **kwargs 

771 Any additional keyword arguments that may be passed to the plugin. 

772 """ 

773 diaObjects.loc[:, "{}_psfFluxErrMean".format(band)] = \ 

774 filterDiaSources.psfFluxErr.mean() 

775 

776 

777class LinearFitDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

778 pass 

779 

780 

781@register("ap_linearFit") 

782class LinearFitDiaPsfFlux(DiaObjectCalculationPlugin): 

783 """Compute fit a linear model to flux vs time. 

784 """ 

785 

786 ConfigClass = LinearFitDiaPsfFluxConfig 

787 

788 # Required input Cols 

789 # Output columns are created upon instantiation of the class. 

790 outputCols = ["psfFluxLinearSlope", "psfFluxLinearIntercept"] 

791 plugType = "multi" 

792 needsFilter = True 

793 

794 @classmethod 

795 def getExecutionOrder(cls): 

796 return cls.DEFAULT_CATALOGCALCULATION 

797 

798 def calculate(self, 

799 diaObjects, 

800 diaSources, 

801 filterDiaSources, 

802 band, 

803 **kwargs): 

804 """Compute fit a linear model to flux vs time. 

805 

806 Parameters 

807 ---------- 

808 diaObject : `dict` 

809 Summary object to store values in. 

810 diaSources : `pandas.DataFrame` 

811 DataFrame representing all diaSources associated with this 

812 diaObject. 

813 filterDiaSources : `pandas.DataFrame` 

814 DataFrame representing diaSources associated with this 

815 diaObject that are observed in the band pass ``band``. 

816 band : `str` 

817 Simple, string name of the filter for the flux being calculated. 

818 **kwargs 

819 Any additional keyword arguments that may be passed to the plugin. 

820 """ 

821 

822 mName = "{}_psfFluxLinearSlope".format(band) 

823 if mName not in diaObjects.columns: 

824 diaObjects[mName] = np.nan 

825 bName = "{}_psfFluxLinearIntercept".format(band) 

826 if bName not in diaObjects.columns: 

827 diaObjects[bName] = np.nan 

828 

829 def _linearFit(df): 

830 tmpDf = df[~np.logical_or( 

831 np.isnan(df["psfFlux"]), 

832 np.logical_or(np.isnan(df["psfFluxErr"]), 

833 np.isnan(df["midpointMjdTai"])))] 

834 if len(tmpDf) < 2: 

835 return pd.Series({mName: np.nan, bName: np.nan}) 

836 fluxes = tmpDf["psfFlux"].to_numpy() 

837 errors = tmpDf["psfFluxErr"].to_numpy() 

838 times = tmpDf["midpointMjdTai"].to_numpy() 

839 A = np.array([times / errors, 1 / errors]).transpose() 

840 m, b = lsq_linear(A, fluxes / errors).x 

841 return pd.Series({mName: m, bName: b}) 

842 

843 diaObjects.loc[:, [mName, bName]] = filterDiaSources.apply(_linearFit) 

844 

845 

846class StetsonJDiaPsfFluxConfig(DiaObjectCalculationPluginConfig): 

847 pass 

848 

849 

850@register("ap_stetsonJ") 

851class StetsonJDiaPsfFlux(DiaObjectCalculationPlugin): 

852 """Compute the StetsonJ statistic on the DIA point source fluxes. 

853 """ 

854 

855 ConfigClass = LinearFitDiaPsfFluxConfig 

856 

857 # Required input Cols 

858 inputCols = ["psfFluxMean"] 

859 # Output columns are created upon instantiation of the class. 

860 outputCols = ["psfFluxStetsonJ"] 

861 plugType = "multi" 

862 needsFilter = True 

863 

864 @classmethod 

865 def getExecutionOrder(cls): 

866 return cls.FLUX_MOMENTS_CALCULATED 

867 

868 def calculate(self, 

869 diaObjects, 

870 diaSources, 

871 filterDiaSources, 

872 band, 

873 **kwargs): 

874 """Compute the StetsonJ statistic on the DIA point source fluxes. 

875 

876 Parameters 

877 ---------- 

878 diaObject : `dict` 

879 Summary object to store values in. 

880 diaSources : `pandas.DataFrame` 

881 DataFrame representing all diaSources associated with this 

882 diaObject. 

883 filterDiaSources : `pandas.DataFrame` 

884 DataFrame representing diaSources associated with this 

885 diaObject that are observed in the band pass ``band``. 

886 band : `str` 

887 Simple, string name of the filter for the flux being calculated. 

888 **kwargs 

889 Any additional keyword arguments that may be passed to the plugin. 

890 """ 

891 meanName = "{}_psfFluxMean".format(band) 

892 

893 def _stetsonJ(df): 

894 tmpDf = df[~np.logical_or(np.isnan(df["psfFlux"]), 

895 np.isnan(df["psfFluxErr"]))] 

896 if len(tmpDf) < 2: 

897 return np.nan 

898 fluxes = tmpDf["psfFlux"].to_numpy() 

899 errors = tmpDf["psfFluxErr"].to_numpy() 

900 

901 return self._stetson_J( 

902 fluxes, 

903 errors, 

904 diaObjects.at[tmpDf.diaObjectId.iat[0], meanName]) 

905 

906 diaObjects.loc[:, "{}_psfFluxStetsonJ".format(band)] = \ 

907 filterDiaSources.apply(_stetsonJ) 

908 

909 def _stetson_J(self, fluxes, errors, mean=None): 

910 """Compute the single band stetsonJ statistic. 

911 

912 Parameters 

913 ---------- 

914 fluxes : `numpy.ndarray` (N,) 

915 Calibrated lightcurve flux values. 

916 errors : `numpy.ndarray` (N,) 

917 Errors on the calibrated lightcurve fluxes. 

918 mean : `float` 

919 Starting mean from previous plugin. 

920 

921 Returns 

922 ------- 

923 stetsonJ : `float` 

924 stetsonJ statistic for the input fluxes and errors. 

925 

926 References 

927 ---------- 

928 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve 

929 Parameters for Cepheid Variables", PASP, 108, 851S, 1996 

930 """ 

931 n_points = len(fluxes) 

932 flux_mean = self._stetson_mean(fluxes, errors, mean) 

933 delta_val = ( 

934 np.sqrt(n_points / (n_points - 1)) * (fluxes - flux_mean) / errors) 

935 p_k = delta_val ** 2 - 1 

936 

937 return np.mean(np.sign(p_k) * np.sqrt(np.fabs(p_k))) 

938 

939 def _stetson_mean(self, 

940 values, 

941 errors, 

942 mean=None, 

943 alpha=2., 

944 beta=2., 

945 n_iter=20, 

946 tol=1e-6): 

947 """Compute the stetson mean of the fluxes which down-weights outliers. 

948 

949 Weighted biased on an error weighted difference scaled by a constant 

950 (1/``a``) and raised to the power beta. Higher betas more harshly 

951 penalize outliers and ``a`` sets the number of sigma where a weighted 

952 difference of 1 occurs. 

953 

954 Parameters 

955 ---------- 

956 values : `numpy.dnarray`, (N,) 

957 Input values to compute the mean of. 

958 errors : `numpy.ndarray`, (N,) 

959 Errors on the input values. 

960 mean : `float` 

961 Starting mean value or None. 

962 alpha : `float` 

963 Scalar down-weighting of the fractional difference. lower->more 

964 clipping. (Default value is 2.) 

965 beta : `float` 

966 Power law slope of the used to down-weight outliers. higher->more 

967 clipping. (Default value is 2.) 

968 n_iter : `int` 

969 Number of iterations of clipping. 

970 tol : `float` 

971 Fractional and absolute tolerance goal on the change in the mean 

972 before exiting early. (Default value is 1e-6) 

973 

974 Returns 

975 ------- 

976 mean : `float` 

977 Weighted stetson mean result. 

978 

979 References 

980 ---------- 

981 .. [1] Stetson, P. B., "On the Automatic Determination of Light-Curve 

982 Parameters for Cepheid Variables", PASP, 108, 851S, 1996 

983 """ 

984 n_points = len(values) 

985 n_factor = np.sqrt(n_points / (n_points - 1)) 

986 inv_var = 1 / errors ** 2 

987 

988 if mean is None: 

989 mean = np.average(values, weights=inv_var) 

990 for iter_idx in range(n_iter): 

991 chi = np.fabs(n_factor * (values - mean) / errors) 

992 tmp_mean = np.average( 

993 values, 

994 weights=inv_var / (1 + (chi / alpha) ** beta)) 

995 diff = np.fabs(tmp_mean - mean) 

996 mean = tmp_mean 

997 if diff / mean < tol and diff < tol: 

998 break 

999 return mean 

1000 

1001 

1002class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

1003 pass 

1004 

1005 

1006@register("ap_meanTotFlux") 

1007class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin): 

1008 """Compute the weighted mean and mean error on the point source fluxes 

1009 forced photometered at the DiaSource location in the calibrated image. 

1010 

1011 Additionally store number of usable data points. 

1012 """ 

1013 

1014 ConfigClass = WeightedMeanDiaPsfFluxConfig 

1015 outputCols = ["scienceFluxMean", "scienceFluxMeanErr"] 

1016 plugType = "multi" 

1017 needsFilter = True 

1018 

1019 @classmethod 

1020 def getExecutionOrder(cls): 

1021 return cls.DEFAULT_CATALOGCALCULATION 

1022 

1023 @catchWarnings(warns=["invalid value encountered", 

1024 "divide by zero"]) 

1025 def calculate(self, 

1026 diaObjects, 

1027 diaSources, 

1028 filterDiaSources, 

1029 band, 

1030 **kwargs): 

1031 """Compute the weighted mean and mean error of the point source flux. 

1032 

1033 Parameters 

1034 ---------- 

1035 diaObject : `dict` 

1036 Summary object to store values in. 

1037 diaSources : `pandas.DataFrame` 

1038 DataFrame representing all diaSources associated with this 

1039 diaObject. 

1040 filterDiaSources : `pandas.DataFrame` 

1041 DataFrame representing diaSources associated with this 

1042 diaObject that are observed in the band pass ``band``. 

1043 band : `str` 

1044 Simple, string name of the filter for the flux being calculated. 

1045 **kwargs 

1046 Any additional keyword arguments that may be passed to the plugin. 

1047 """ 

1048 totMeanName = "{}_scienceFluxMean".format(band) 

1049 if totMeanName not in diaObjects.columns: 

1050 diaObjects[totMeanName] = np.nan 

1051 totErrName = "{}_scienceFluxMeanErr".format(band) 

1052 if totErrName not in diaObjects.columns: 

1053 diaObjects[totErrName] = np.nan 

1054 

1055 def _meanFlux(df): 

1056 tmpDf = df[~np.logical_or(np.isnan(df["scienceFlux"]), 

1057 np.isnan(df["scienceFluxErr"]))] 

1058 tot_weight = np.nansum(1 / tmpDf["scienceFluxErr"] ** 2) 

1059 fluxMean = np.nansum(tmpDf["scienceFlux"] 

1060 / tmpDf["scienceFluxErr"] ** 2) 

1061 fluxMean /= tot_weight 

1062 fluxMeanErr = np.sqrt(1 / tot_weight) 

1063 

1064 return pd.Series({totMeanName: fluxMean, 

1065 totErrName: fluxMeanErr}) 

1066 

1067 diaObjects.loc[:, [totMeanName, totErrName]] = \ 

1068 filterDiaSources.apply(_meanFlux) 

1069 

1070 

1071class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

1072 pass 

1073 

1074 

1075@register("ap_sigmaTotFlux") 

1076class SigmaDiaTotFlux(DiaObjectCalculationPlugin): 

1077 """Compute scatter of diaSource fluxes. 

1078 """ 

1079 

1080 ConfigClass = SigmaDiaPsfFluxConfig 

1081 # Output columns are created upon instantiation of the class. 

1082 outputCols = ["scienceFluxSigma"] 

1083 plugType = "multi" 

1084 needsFilter = True 

1085 

1086 @classmethod 

1087 def getExecutionOrder(cls): 

1088 return cls.DEFAULT_CATALOGCALCULATION 

1089 

1090 def calculate(self, 

1091 diaObjects, 

1092 diaSources, 

1093 filterDiaSources, 

1094 band, 

1095 **kwargs): 

1096 """Compute the sigma fluxes of the point source flux measured on the 

1097 calibrated image. 

1098 

1099 Parameters 

1100 ---------- 

1101 diaObject : `dict` 

1102 Summary object to store values in. 

1103 diaSources : `pandas.DataFrame` 

1104 DataFrame representing all diaSources associated with this 

1105 diaObject. 

1106 filterDiaSources : `pandas.DataFrame` 

1107 DataFrame representing diaSources associated with this 

1108 diaObject that are observed in the band pass ``band``. 

1109 band : `str` 

1110 Simple, string name of the filter for the flux being calculated. 

1111 **kwargs 

1112 Any additional keyword arguments that may be passed to the plugin. 

1113 """ 

1114 # Set "delta degrees of freedom (ddf)" to 1 to calculate the unbiased 

1115 # estimator of scatter (i.e. 'N - 1' instead of 'N'). 

1116 diaObjects.loc[:, "{}_scienceFluxSigma".format(band)] = \ 

1117 filterDiaSources.scienceFlux.std()