Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 lsst.meas.base.pluginRegistry import register 

44 

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

46 "HTMIndexDiaPosition", "HTMIndexDiaPositionConfig", 

47 "NumDiaSourcesDiaPlugin", "NumDiaSourcesDiaPluginConfig", 

48 "SimpleSourceFlagDiaPlugin", "SimpleSourceFlagDiaPluginConfig", 

49 "WeightedMeanDiaPsFluxConfig", "WeightedMeanDiaPsFlux", 

50 "PercentileDiaPsFlux", "PercentileDiaPsFluxConfig", 

51 "SigmaDiaPsFlux", "SigmaDiaPsFluxConfig", 

52 "Chi2DiaPsFlux", "Chi2DiaPsFluxConfig", 

53 "MadDiaPsFlux", "MadDiaPsFluxConfig", 

54 "SkewDiaPsFlux", "SkewDiaPsFluxConfig", 

55 "MinMaxDiaPsFlux", "MinMaxDiaPsFluxConfig", 

56 "MaxSlopeDiaPsFlux", "MaxSlopeDiaPsFluxConfig", 

57 "ErrMeanDiaPsFlux", "ErrMeanDiaPsFluxConfig", 

58 "LinearFitDiaPsFlux", "LinearFitDiaPsFluxConfig", 

59 "StetsonJDiaPsFlux", "StetsonJDiaPsFluxConfig", 

60 "WeightedMeanDiaTotFlux", "WeightedMeanDiaTotFluxConfig", 

61 "SigmaDiaTotFlux", "SigmaDiaTotFluxConfig") 

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", "decl", "radecTai"] 

96 

97 @classmethod 

98 def getExecutionOrder(cls): 

99 return cls.DEFAULT_CATALOGCALCULATION 

100 

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

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

103 diaSource locations. 

104 

105 Parameters 

106 ---------- 

107 diaObjects : `pandas.DataFrame` 

108 Summary objects to store values in. 

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

110 Catalog of DiaSources summarized by this DiaObject. 

111 **kwargs 

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

113 """ 

114 for outCol in self.outputCols: 

115 if outCol not in diaObjects.columns: 

116 diaObjects[outCol] = np.nan 

117 

118 def _computeMeanPos(df): 

119 aveCoord = geom.averageSpherePoint( 

120 list(geom.SpherePoint(src["ra"], src["decl"], geom.degrees) 

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

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

123 decl = aveCoord.getDec().asDegrees() 

124 if np.isnan(ra) or np.isnan(decl): 

125 radecTai = np.nan 

126 else: 

127 radecTai = df["midPointTai"].max() 

128 

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

130 "decl": aveCoord.getDec().asDegrees(), 

131 "radecTai": radecTai}) 

132 

133 ans = diaSources.apply(_computeMeanPos) 

134 diaObjects.loc[:, ["ra", "decl", "radecTai"]] = ans 

135 

136 

137class HTMIndexDiaPositionConfig(DiaObjectCalculationPluginConfig): 

138 

139 htmLevel = pexConfig.Field( 

140 dtype=int, 

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

142 default=20, 

143 ) 

144 

145 

146@register("ap_HTMIndex") 

147class HTMIndexDiaPosition(DiaObjectCalculationPlugin): 

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

149 """ 

150 ConfigClass = HTMIndexDiaPositionConfig 

151 

152 plugType = 'single' 

153 

154 inputCols = ["ra", "decl"] 

155 outputCols = ["pixelId"] 

156 

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

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

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

160 

161 @classmethod 

162 def getExecutionOrder(cls): 

163 return cls.FLUX_MOMENTS_CALCULATED 

164 

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

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

167 

168 Parameters 

169 ---------- 

170 diaObjects : `pandas.dataFrame` 

171 Summary objects to store values in and read ra/decl from. 

172 diaObjectId : `int` 

173 Id of the diaObject to update. 

174 **kwargs 

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

176 """ 

177 sphPoint = geom.SpherePoint( 

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

179 diaObjects.at[diaObjectId, "decl"] * geom.degrees) 

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

181 sphPoint.getVector()) 

182 

183 

184class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig): 

185 pass 

186 

187 

188@register("ap_nDiaSources") 

189class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin): 

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

191 """ 

192 

193 ConfigClass = NumDiaSourcesDiaPluginConfig 

194 outputCols = ["nDiaSources"] 

195 plugType = "multi" 

196 

197 @classmethod 

198 def getExecutionOrder(cls): 

199 return cls.DEFAULT_CATALOGCALCULATION 

200 

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

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

203 

204 Parameters 

205 ---------- 

206 diaObject : `dict` 

207 Summary object to store values in and read ra/decl from. 

208 **kwargs 

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

210 """ 

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

212 

213 

214class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig): 

215 pass 

216 

217 

218@register("ap_diaObjectFlag") 

219class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin): 

220 """Find if any DiaSource is flagged. 

221 

222 Set the DiaObject flag if any DiaSource is flagged. 

223 """ 

224 

225 ConfigClass = NumDiaSourcesDiaPluginConfig 

226 outputCols = ["flags"] 

227 plugType = "multi" 

228 

229 @classmethod 

230 def getExecutionOrder(cls): 

231 return cls.DEFAULT_CATALOGCALCULATION 

232 

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

234 """Find if any DiaSource is flagged. 

235 

236 Set the DiaObject flag if any DiaSource is flagged. 

237 

238 Parameters 

239 ---------- 

240 diaObject : `dict` 

241 Summary object to store values in and read ra/decl from. 

242 **kwargs 

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

244 """ 

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

246 

247 

248class WeightedMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

249 pass 

250 

251 

252@register("ap_meanFlux") 

253class WeightedMeanDiaPsFlux(DiaObjectCalculationPlugin): 

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

255 of the DiaSource measured on the difference image. 

256 

257 Additionally store number of usable data points. 

258 """ 

259 

260 ConfigClass = WeightedMeanDiaPsFluxConfig 

261 outputCols = ["PSFluxMean", "PSFluxMeanErr", "PSFluxNdata"] 

262 plugType = "multi" 

263 

264 @classmethod 

265 def getExecutionOrder(cls): 

266 return cls.DEFAULT_CATALOGCALCULATION 

267 

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

269 "divide by zero"]) 

270 def calculate(self, 

271 diaObjects, 

272 diaSources, 

273 filterDiaSources, 

274 filterName, 

275 **kwargs): 

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

277 

278 Parameters 

279 ---------- 

280 diaObject : `dict` 

281 Summary object to store values in. 

282 diaSources : `pandas.DataFrame` 

283 DataFrame representing all diaSources associated with this 

284 diaObject. 

285 filterDiaSources : `pandas.DataFrame` 

286 DataFrame representing diaSources associated with this 

287 diaObject that are observed in the band pass ``filterName``. 

288 filterName : `str` 

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

290 **kwargs 

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

292 """ 

293 meanName = "{}PSFluxMean".format(filterName) 

294 errName = "{}PSFluxMeanErr".format(filterName) 

295 nDataName = "{}PSFluxNdata".format(filterName) 

296 if meanName not in diaObjects.columns: 

297 diaObjects[meanName] = np.nan 

298 if errName not in diaObjects.columns: 

299 diaObjects[errName] = np.nan 

300 if nDataName not in diaObjects.columns: 

301 diaObjects[nDataName] = 0 

302 

303 def _weightedMean(df): 

304 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]), 

305 np.isnan(df["psFluxErr"]))] 

306 tot_weight = np.nansum(1 / tmpDf["psFluxErr"] ** 2) 

307 fluxMean = np.nansum(tmpDf["psFlux"] 

308 / tmpDf["psFluxErr"] ** 2) 

309 fluxMean /= tot_weight 

310 if tot_weight > 0: 

311 fluxMeanErr = np.sqrt(1 / tot_weight) 

312 else: 

313 fluxMeanErr = np.nan 

314 nFluxData = len(tmpDf) 

315 

316 return pd.Series({meanName: fluxMean, 

317 errName: fluxMeanErr, 

318 nDataName: nFluxData}, 

319 dtype="object") 

320 

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

322 filterDiaSources.apply(_weightedMean) 

323 

324 

325class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

326 percentiles = pexConfig.ListField( 

327 dtype=int, 

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

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

330 "integer values." 

331 ) 

332 

333 

334@register("ap_percentileFlux") 

335class PercentileDiaPsFlux(DiaObjectCalculationPlugin): 

336 """Compute percentiles of diaSource fluxes. 

337 """ 

338 

339 ConfigClass = PercentileDiaPsFluxConfig 

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

341 outputCols = [] 

342 plugType = "multi" 

343 

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

345 DiaObjectCalculationPlugin.__init__(self, 

346 config, 

347 name, 

348 metadata, 

349 **kwargs) 

350 self.outputCols = ["PSFluxPercentile{:02d}".format(percent) 

351 for percent in self.config.percentiles] 

352 

353 @classmethod 

354 def getExecutionOrder(cls): 

355 return cls.DEFAULT_CATALOGCALCULATION 

356 

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

358 def calculate(self, 

359 diaObjects, 

360 diaSources, 

361 filterDiaSources, 

362 filterName, 

363 **kwargs): 

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

365 

366 Parameters 

367 ---------- 

368 diaObject : `dict` 

369 Summary object to store values in. 

370 diaSources : `pandas.DataFrame` 

371 DataFrame representing all diaSources associated with this 

372 diaObject. 

373 filterDiaSources : `pandas.DataFrame` 

374 DataFrame representing diaSources associated with this 

375 diaObject that are observed in the band pass ``filterName``. 

376 filterName : `str` 

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

378 **kwargs 

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

380 """ 

381 pTileNames = [] 

382 for tilePercent in self.config.percentiles: 

383 pTileName = "{}PSFluxPercentile{:02d}".format(filterName, 

384 tilePercent) 

385 pTileNames.append(pTileName) 

386 if pTileName not in diaObjects.columns: 

387 diaObjects[pTileName] = np.nan 

388 

389 def _fluxPercentiles(df): 

390 pTiles = np.nanpercentile(df["psFlux"], self.config.percentiles) 

391 return pd.Series( 

392 dict((tileName, pTile) 

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

394 

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

396 

397 

398class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

399 pass 

400 

401 

402@register("ap_sigmaFlux") 

403class SigmaDiaPsFlux(DiaObjectCalculationPlugin): 

404 """Compute scatter of diaSource fluxes. 

405 """ 

406 

407 ConfigClass = SigmaDiaPsFluxConfig 

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

409 outputCols = ["PSFluxSigma"] 

410 plugType = "multi" 

411 

412 @classmethod 

413 def getExecutionOrder(cls): 

414 return cls.DEFAULT_CATALOGCALCULATION 

415 

416 def calculate(self, 

417 diaObjects, 

418 diaSources, 

419 filterDiaSources, 

420 filterName, 

421 **kwargs): 

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

423 

424 Parameters 

425 ---------- 

426 diaObject : `dict` 

427 Summary object to store values in. 

428 diaSources : `pandas.DataFrame` 

429 DataFrame representing all diaSources associated with this 

430 diaObject. 

431 filterDiaSources : `pandas.DataFrame` 

432 DataFrame representing diaSources associated with this 

433 diaObject that are observed in the band pass ``filterName``. 

434 filterName : `str` 

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

436 **kwargs 

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

438 """ 

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

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

441 diaObjects.loc[:, "{}PSFluxSigma".format(filterName)] = \ 

442 filterDiaSources.psFlux.std() 

443 

444 

445class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

446 pass 

447 

448 

449@register("ap_chi2Flux") 

450class Chi2DiaPsFlux(DiaObjectCalculationPlugin): 

451 """Compute chi2 of diaSource fluxes. 

452 """ 

453 

454 ConfigClass = Chi2DiaPsFluxConfig 

455 

456 # Required input Cols 

457 inputCols = ["PSFluxMean"] 

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

459 outputCols = ["PSFluxChi2"] 

460 plugType = "multi" 

461 

462 @classmethod 

463 def getExecutionOrder(cls): 

464 return cls.FLUX_MOMENTS_CALCULATED 

465 

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

467 def calculate(self, 

468 diaObjects, 

469 diaSources, 

470 filterDiaSources, 

471 filterName, 

472 **kwargs): 

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

474 

475 Parameters 

476 ---------- 

477 diaObject : `dict` 

478 Summary object to store values in. 

479 diaSources : `pandas.DataFrame` 

480 DataFrame representing all diaSources associated with this 

481 diaObject. 

482 filterDiaSources : `pandas.DataFrame` 

483 DataFrame representing diaSources associated with this 

484 diaObject that are observed in the band pass ``filterName``. 

485 filterName : `str` 

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

487 **kwargs 

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

489 """ 

490 meanName = "{}PSFluxMean".format(filterName) 

491 

492 def _chi2(df): 

493 delta = (df["psFlux"] 

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

495 return np.nansum((delta / df["psFluxErr"]) ** 2) 

496 

497 diaObjects.loc[:, "{}PSFluxChi2".format(filterName)] = \ 

498 filterDiaSources.apply(_chi2) 

499 

500 

501class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

502 pass 

503 

504 

505@register("ap_madFlux") 

506class MadDiaPsFlux(DiaObjectCalculationPlugin): 

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

508 """ 

509 

510 ConfigClass = MadDiaPsFluxConfig 

511 

512 # Required input Cols 

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

514 outputCols = ["PSFluxMAD"] 

515 plugType = "multi" 

516 

517 @classmethod 

518 def getExecutionOrder(cls): 

519 return cls.DEFAULT_CATALOGCALCULATION 

520 

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

522 def calculate(self, 

523 diaObjects, 

524 diaSources, 

525 filterDiaSources, 

526 filterName, 

527 **kwargs): 

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

529 

530 Parameters 

531 ---------- 

532 diaObject : `dict` 

533 Summary object to store values in. 

534 diaSources : `pandas.DataFrame` 

535 DataFrame representing all diaSources associated with this 

536 diaObject. 

537 filterDiaSources : `pandas.DataFrame` 

538 DataFrame representing diaSources associated with this 

539 diaObject that are observed in the band pass ``filterName``. 

540 filterName : `str` 

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

542 **kwargs 

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

544 """ 

545 diaObjects.loc[:, "{}PSFluxMAD".format(filterName)] = \ 

546 filterDiaSources.psFlux.apply(median_absolute_deviation, 

547 ignore_nan=True) 

548 

549 

550class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

551 pass 

552 

553 

554@register("ap_skewFlux") 

555class SkewDiaPsFlux(DiaObjectCalculationPlugin): 

556 """Compute the skew of diaSource fluxes. 

557 """ 

558 

559 ConfigClass = SkewDiaPsFluxConfig 

560 

561 # Required input Cols 

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

563 outputCols = ["PSFluxSkew"] 

564 plugType = "multi" 

565 

566 @classmethod 

567 def getExecutionOrder(cls): 

568 return cls.DEFAULT_CATALOGCALCULATION 

569 

570 def calculate(self, 

571 diaObjects, 

572 diaSources, 

573 filterDiaSources, 

574 filterName, 

575 **kwargs): 

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

577 

578 Parameters 

579 ---------- 

580 diaObject : `dict` 

581 Summary object to store values in. 

582 diaSources : `pandas.DataFrame` 

583 DataFrame representing all diaSources associated with this 

584 diaObject. 

585 filterDiaSources : `pandas.DataFrame` 

586 DataFrame representing diaSources associated with this 

587 diaObject that are observed in the band pass ``filterName``. 

588 filterName : `str` 

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

590 **kwargs 

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

592 """ 

593 diaObjects.loc[:, "{}PSFluxSkew".format(filterName)] = \ 

594 filterDiaSources.psFlux.skew() 

595 

596 

597class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

598 pass 

599 

600 

601@register("ap_minMaxFlux") 

602class MinMaxDiaPsFlux(DiaObjectCalculationPlugin): 

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

604 """ 

605 

606 ConfigClass = MinMaxDiaPsFluxConfig 

607 

608 # Required input Cols 

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

610 outputCols = ["PSFluxMin", "PSFluxMax"] 

611 plugType = "multi" 

612 

613 @classmethod 

614 def getExecutionOrder(cls): 

615 return cls.DEFAULT_CATALOGCALCULATION 

616 

617 def calculate(self, 

618 diaObjects, 

619 diaSources, 

620 filterDiaSources, 

621 filterName, 

622 **kwargs): 

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

624 

625 Parameters 

626 ---------- 

627 diaObject : `dict` 

628 Summary object to store values in. 

629 diaSources : `pandas.DataFrame` 

630 DataFrame representing all diaSources associated with this 

631 diaObject. 

632 filterDiaSources : `pandas.DataFrame` 

633 DataFrame representing diaSources associated with this 

634 diaObject that are observed in the band pass ``filterName``. 

635 filterName : `str` 

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

637 **kwargs 

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

639 """ 

640 minName = "{}PSFluxMin".format(filterName) 

641 if minName not in diaObjects.columns: 

642 diaObjects[minName] = np.nan 

643 maxName = "{}PSFluxMax".format(filterName) 

644 if maxName not in diaObjects.columns: 

645 diaObjects[maxName] = np.nan 

646 

647 diaObjects.loc[:, minName] = filterDiaSources.psFlux.min() 

648 diaObjects.loc[:, maxName] = filterDiaSources.psFlux.max() 

649 

650 

651class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

652 pass 

653 

654 

655@register("ap_maxSlopeFlux") 

656class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin): 

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

658 """ 

659 

660 ConfigClass = MinMaxDiaPsFluxConfig 

661 

662 # Required input Cols 

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

664 outputCols = ["PSFluxMaxSlope"] 

665 plugType = "multi" 

666 

667 @classmethod 

668 def getExecutionOrder(cls): 

669 return cls.DEFAULT_CATALOGCALCULATION 

670 

671 def calculate(self, 

672 diaObjects, 

673 diaSources, 

674 filterDiaSources, 

675 filterName, 

676 **kwargs): 

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

678 

679 Parameters 

680 ---------- 

681 diaObject : `dict` 

682 Summary object to store values in. 

683 diaSources : `pandas.DataFrame` 

684 DataFrame representing all diaSources associated with this 

685 diaObject. 

686 filterDiaSources : `pandas.DataFrame` 

687 DataFrame representing diaSources associated with this 

688 diaObject that are observed in the band pass ``filterName``. 

689 filterName : `str` 

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

691 **kwargs 

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

693 """ 

694 

695 def _maxSlope(df): 

696 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]), 

697 np.isnan(df["midPointTai"]))] 

698 if len(tmpDf) < 2: 

699 return np.nan 

700 times = tmpDf["midPointTai"].to_numpy() 

701 timeArgs = times.argsort() 

702 times = times[timeArgs] 

703 fluxes = tmpDf["psFlux"].to_numpy()[timeArgs] 

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

705 

706 diaObjects.loc[:, "{}PSFluxMaxSlope".format(filterName)] = \ 

707 filterDiaSources.apply(_maxSlope) 

708 

709 

710class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

711 pass 

712 

713 

714@register("ap_meanErrFlux") 

715class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin): 

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

717 """ 

718 

719 ConfigClass = ErrMeanDiaPsFluxConfig 

720 

721 # Required input Cols 

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

723 outputCols = ["PSFluxErrMean"] 

724 plugType = "multi" 

725 

726 @classmethod 

727 def getExecutionOrder(cls): 

728 return cls.DEFAULT_CATALOGCALCULATION 

729 

730 def calculate(self, 

731 diaObjects, 

732 diaSources, 

733 filterDiaSources, 

734 filterName, 

735 **kwargs): 

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

737 

738 Parameters 

739 ---------- 

740 diaObject : `dict` 

741 Summary object to store values in. 

742 diaSources : `pandas.DataFrame` 

743 DataFrame representing all diaSources associated with this 

744 diaObject. 

745 filterDiaSources : `pandas.DataFrame` 

746 DataFrame representing diaSources associated with this 

747 diaObject that are observed in the band pass ``filterName``. 

748 filterName : `str` 

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

750 **kwargs 

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

752 """ 

753 diaObjects.loc[:, "{}PSFluxErrMean".format(filterName)] = \ 

754 filterDiaSources.psFluxErr.mean() 

755 

756 

757class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

758 pass 

759 

760 

761@register("ap_linearFit") 

762class LinearFitDiaPsFlux(DiaObjectCalculationPlugin): 

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

764 """ 

765 

766 ConfigClass = LinearFitDiaPsFluxConfig 

767 

768 # Required input Cols 

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

770 outputCols = ["PSFluxLinearSlope", "PSFluxLinearIntercept"] 

771 plugType = "multi" 

772 

773 @classmethod 

774 def getExecutionOrder(cls): 

775 return cls.DEFAULT_CATALOGCALCULATION 

776 

777 def calculate(self, 

778 diaObjects, 

779 diaSources, 

780 filterDiaSources, 

781 filterName, 

782 **kwargs): 

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

784 

785 Parameters 

786 ---------- 

787 diaObject : `dict` 

788 Summary object to store values in. 

789 diaSources : `pandas.DataFrame` 

790 DataFrame representing all diaSources associated with this 

791 diaObject. 

792 filterDiaSources : `pandas.DataFrame` 

793 DataFrame representing diaSources associated with this 

794 diaObject that are observed in the band pass ``filterName``. 

795 filterName : `str` 

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

797 **kwargs 

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

799 """ 

800 

801 mName = "{}PSFluxLinearSlope".format(filterName) 

802 if mName not in diaObjects.columns: 

803 diaObjects[mName] = np.nan 

804 bName = "{}PSFluxLinearIntercept".format(filterName) 

805 if bName not in diaObjects.columns: 

806 diaObjects[bName] = np.nan 

807 

808 def _linearFit(df): 

809 tmpDf = df[~np.logical_or( 

810 np.isnan(df["psFlux"]), 

811 np.logical_or(np.isnan(df["psFluxErr"]), 

812 np.isnan(df["midPointTai"])))] 

813 if len(tmpDf) < 2: 

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

815 fluxes = tmpDf["psFlux"].to_numpy() 

816 errors = tmpDf["psFluxErr"].to_numpy() 

817 times = tmpDf["midPointTai"].to_numpy() 

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

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

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

821 

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

823 

824 

825class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

826 pass 

827 

828 

829@register("ap_stetsonJ") 

830class StetsonJDiaPsFlux(DiaObjectCalculationPlugin): 

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

832 """ 

833 

834 ConfigClass = LinearFitDiaPsFluxConfig 

835 

836 # Required input Cols 

837 inputCols = ["PSFluxMean"] 

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

839 outputCols = ["PSFluxStetsonJ"] 

840 plugType = "multi" 

841 

842 @classmethod 

843 def getExecutionOrder(cls): 

844 return cls.FLUX_MOMENTS_CALCULATED 

845 

846 def calculate(self, 

847 diaObjects, 

848 diaSources, 

849 filterDiaSources, 

850 filterName, 

851 **kwargs): 

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

853 

854 Parameters 

855 ---------- 

856 diaObject : `dict` 

857 Summary object to store values in. 

858 diaSources : `pandas.DataFrame` 

859 DataFrame representing all diaSources associated with this 

860 diaObject. 

861 filterDiaSources : `pandas.DataFrame` 

862 DataFrame representing diaSources associated with this 

863 diaObject that are observed in the band pass ``filterName``. 

864 filterName : `str` 

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

866 **kwargs 

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

868 """ 

869 meanName = "{}PSFluxMean".format(filterName) 

870 

871 def _stetsonJ(df): 

872 tmpDf = df[~np.logical_or(np.isnan(df["psFlux"]), 

873 np.isnan(df["psFluxErr"]))] 

874 if len(tmpDf) < 2: 

875 return np.nan 

876 fluxes = tmpDf["psFlux"].to_numpy() 

877 errors = tmpDf["psFluxErr"].to_numpy() 

878 

879 return self._stetson_J( 

880 fluxes, 

881 errors, 

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

883 

884 diaObjects.loc[:, "{}PSFluxStetsonJ".format(filterName)] = \ 

885 filterDiaSources.apply(_stetsonJ) 

886 

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

888 """Compute the single band stetsonJ statistic. 

889 

890 Parameters 

891 ---------- 

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

893 Calibrated lightcurve flux values. 

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

895 Errors on the calibrated lightcurve fluxes. 

896 mean : `float` 

897 Starting mean from previous plugin. 

898 

899 Returns 

900 ------- 

901 stetsonJ : `float` 

902 stetsonJ statistic for the input fluxes and errors. 

903 

904 References 

905 ---------- 

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

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

908 """ 

909 n_points = len(fluxes) 

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

911 delta_val = ( 

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

913 p_k = delta_val ** 2 - 1 

914 

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

916 

917 def _stetson_mean(self, 

918 values, 

919 errors, 

920 mean=None, 

921 alpha=2., 

922 beta=2., 

923 n_iter=20, 

924 tol=1e-6): 

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

926 

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

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

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

930 difference of 1 occurs. 

931 

932 Parameters 

933 ---------- 

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

935 Input values to compute the mean of. 

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

937 Errors on the input values. 

938 mean : `float` 

939 Starting mean value or None. 

940 alpha : `float` 

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

942 clipping. (Default value is 2.) 

943 beta : `float` 

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

945 clipping. (Default value is 2.) 

946 n_iter : `int` 

947 Number of iterations of clipping. 

948 tol : `float` 

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

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

951 

952 Returns 

953 ------- 

954 mean : `float` 

955 Weighted stetson mean result. 

956 

957 References 

958 ---------- 

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

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

961 """ 

962 n_points = len(values) 

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

964 inv_var = 1 / errors ** 2 

965 

966 if mean is None: 

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

968 for iter_idx in range(n_iter): 

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

970 tmp_mean = np.average( 

971 values, 

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

973 diff = np.fabs(tmp_mean - mean) 

974 mean = tmp_mean 

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

976 break 

977 return mean 

978 

979 

980class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

981 pass 

982 

983 

984@register("ap_meanTotFlux") 

985class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin): 

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

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

988 

989 Additionally store number of usable data points. 

990 """ 

991 

992 ConfigClass = WeightedMeanDiaPsFluxConfig 

993 outputCols = ["TOTFluxMean", "TOTFluxMeanErr"] 

994 plugType = "multi" 

995 

996 @classmethod 

997 def getExecutionOrder(cls): 

998 return cls.DEFAULT_CATALOGCALCULATION 

999 

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

1001 "divide by zero"]) 

1002 def calculate(self, 

1003 diaObjects, 

1004 diaSources, 

1005 filterDiaSources, 

1006 filterName, 

1007 **kwargs): 

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

1009 

1010 Parameters 

1011 ---------- 

1012 diaObject : `dict` 

1013 Summary object to store values in. 

1014 diaSources : `pandas.DataFrame` 

1015 DataFrame representing all diaSources associated with this 

1016 diaObject. 

1017 filterDiaSources : `pandas.DataFrame` 

1018 DataFrame representing diaSources associated with this 

1019 diaObject that are observed in the band pass ``filterName``. 

1020 filterName : `str` 

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

1022 **kwargs 

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

1024 """ 

1025 totMeanName = "{}TOTFluxMean".format(filterName) 

1026 if totMeanName not in diaObjects.columns: 

1027 diaObjects[totMeanName] = np.nan 

1028 totErrName = "{}TOTFluxMeanErr".format(filterName) 

1029 if totErrName not in diaObjects.columns: 

1030 diaObjects[totErrName] = np.nan 

1031 

1032 def _meanFlux(df): 

1033 tmpDf = df[~np.logical_or(np.isnan(df["totFlux"]), 

1034 np.isnan(df["totFluxErr"]))] 

1035 tot_weight = np.nansum(1 / tmpDf["totFluxErr"] ** 2) 

1036 fluxMean = np.nansum(tmpDf["totFlux"] 

1037 / tmpDf["totFluxErr"] ** 2) 

1038 fluxMean /= tot_weight 

1039 fluxMeanErr = np.sqrt(1 / tot_weight) 

1040 

1041 return pd.Series({totMeanName: fluxMean, 

1042 totErrName: fluxMeanErr}) 

1043 

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

1045 filterDiaSources.apply(_meanFlux) 

1046 

1047 

1048class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

1049 pass 

1050 

1051 

1052@register("ap_sigmaTotFlux") 

1053class SigmaDiaTotFlux(DiaObjectCalculationPlugin): 

1054 """Compute scatter of diaSource fluxes. 

1055 """ 

1056 

1057 ConfigClass = SigmaDiaPsFluxConfig 

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

1059 outputCols = ["TOTFluxSigma"] 

1060 plugType = "multi" 

1061 

1062 @classmethod 

1063 def getExecutionOrder(cls): 

1064 return cls.DEFAULT_CATALOGCALCULATION 

1065 

1066 def calculate(self, 

1067 diaObjects, 

1068 diaSources, 

1069 filterDiaSources, 

1070 filterName, 

1071 **kwargs): 

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

1073 calibrated image. 

1074 

1075 Parameters 

1076 ---------- 

1077 diaObject : `dict` 

1078 Summary object to store values in. 

1079 diaSources : `pandas.DataFrame` 

1080 DataFrame representing all diaSources associated with this 

1081 diaObject. 

1082 filterDiaSources : `pandas.DataFrame` 

1083 DataFrame representing diaSources associated with this 

1084 diaObject that are observed in the band pass ``filterName``. 

1085 filterName : `str` 

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

1087 **kwargs 

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

1089 """ 

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

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

1092 diaObjects.loc[:, "{}TOTFluxSigma".format(filterName)] = \ 

1093 filterDiaSources.totFlux.std()