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

Shortcuts 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

378 statements  

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 "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 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["decl"], geom.degrees) 

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

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

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

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

126 radecTai = np.nan 

127 else: 

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

129 

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

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

132 "radecTai": radecTai}) 

133 

134 ans = diaSources.apply(_computeMeanPos) 

135 diaObjects.loc[:, ["ra", "decl", "radecTai"]] = 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 ConfigClass = HTMIndexDiaPositionConfig 

152 

153 plugType = 'single' 

154 

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

156 outputCols = ["pixelId"] 

157 needsFilter = False 

158 

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

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

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

162 

163 @classmethod 

164 def getExecutionOrder(cls): 

165 return cls.FLUX_MOMENTS_CALCULATED 

166 

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

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

169 

170 Parameters 

171 ---------- 

172 diaObjects : `pandas.dataFrame` 

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

174 diaObjectId : `int` 

175 Id of the diaObject to update. 

176 **kwargs 

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

178 """ 

179 sphPoint = geom.SpherePoint( 

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

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

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

183 sphPoint.getVector()) 

184 

185 

186class NumDiaSourcesDiaPluginConfig(DiaObjectCalculationPluginConfig): 

187 pass 

188 

189 

190@register("ap_nDiaSources") 

191class NumDiaSourcesDiaPlugin(DiaObjectCalculationPlugin): 

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

193 """ 

194 

195 ConfigClass = NumDiaSourcesDiaPluginConfig 

196 outputCols = ["nDiaSources"] 

197 plugType = "multi" 

198 needsFilter = False 

199 

200 @classmethod 

201 def getExecutionOrder(cls): 

202 return cls.DEFAULT_CATALOGCALCULATION 

203 

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

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

206 

207 Parameters 

208 ---------- 

209 diaObject : `dict` 

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

211 **kwargs 

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

213 """ 

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

215 

216 

217class SimpleSourceFlagDiaPluginConfig(DiaObjectCalculationPluginConfig): 

218 pass 

219 

220 

221@register("ap_diaObjectFlag") 

222class SimpleSourceFlagDiaPlugin(DiaObjectCalculationPlugin): 

223 """Find if any DiaSource is flagged. 

224 

225 Set the DiaObject flag if any DiaSource is flagged. 

226 """ 

227 

228 ConfigClass = NumDiaSourcesDiaPluginConfig 

229 outputCols = ["flags"] 

230 plugType = "multi" 

231 needsFilter = False 

232 

233 @classmethod 

234 def getExecutionOrder(cls): 

235 return cls.DEFAULT_CATALOGCALCULATION 

236 

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

238 """Find if any DiaSource is flagged. 

239 

240 Set the DiaObject flag if any DiaSource is flagged. 

241 

242 Parameters 

243 ---------- 

244 diaObject : `dict` 

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

246 **kwargs 

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

248 """ 

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

250 

251 

252class WeightedMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

253 pass 

254 

255 

256@register("ap_meanFlux") 

257class WeightedMeanDiaPsFlux(DiaObjectCalculationPlugin): 

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

259 of the DiaSource measured on the difference image. 

260 

261 Additionally store number of usable data points. 

262 """ 

263 

264 ConfigClass = WeightedMeanDiaPsFluxConfig 

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

266 plugType = "multi" 

267 needsFilter = True 

268 

269 @classmethod 

270 def getExecutionOrder(cls): 

271 return cls.DEFAULT_CATALOGCALCULATION 

272 

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

274 "divide by zero"]) 

275 def calculate(self, 

276 diaObjects, 

277 diaSources, 

278 filterDiaSources, 

279 filterName, 

280 **kwargs): 

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

282 

283 Parameters 

284 ---------- 

285 diaObject : `dict` 

286 Summary object to store values in. 

287 diaSources : `pandas.DataFrame` 

288 DataFrame representing all diaSources associated with this 

289 diaObject. 

290 filterDiaSources : `pandas.DataFrame` 

291 DataFrame representing diaSources associated with this 

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

293 filterName : `str` 

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

295 **kwargs 

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

297 """ 

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

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

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

301 if meanName not in diaObjects.columns: 

302 diaObjects[meanName] = np.nan 

303 if errName not in diaObjects.columns: 

304 diaObjects[errName] = np.nan 

305 if nDataName not in diaObjects.columns: 

306 diaObjects[nDataName] = 0 

307 

308 def _weightedMean(df): 

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

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

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

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

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

314 fluxMean /= tot_weight 

315 if tot_weight > 0: 

316 fluxMeanErr = np.sqrt(1 / tot_weight) 

317 else: 

318 fluxMeanErr = np.nan 

319 nFluxData = len(tmpDf) 

320 

321 return pd.Series({meanName: fluxMean, 

322 errName: fluxMeanErr, 

323 nDataName: nFluxData}, 

324 dtype="object") 

325 

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

327 filterDiaSources.apply(_weightedMean) 

328 

329 

330class PercentileDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

331 percentiles = pexConfig.ListField( 

332 dtype=int, 

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

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

335 "integer values." 

336 ) 

337 

338 

339@register("ap_percentileFlux") 

340class PercentileDiaPsFlux(DiaObjectCalculationPlugin): 

341 """Compute percentiles of diaSource fluxes. 

342 """ 

343 

344 ConfigClass = PercentileDiaPsFluxConfig 

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

346 outputCols = [] 

347 plugType = "multi" 

348 needsFilter = True 

349 

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

351 DiaObjectCalculationPlugin.__init__(self, 

352 config, 

353 name, 

354 metadata, 

355 **kwargs) 

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

357 for percent in self.config.percentiles] 

358 

359 @classmethod 

360 def getExecutionOrder(cls): 

361 return cls.DEFAULT_CATALOGCALCULATION 

362 

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

364 def calculate(self, 

365 diaObjects, 

366 diaSources, 

367 filterDiaSources, 

368 filterName, 

369 **kwargs): 

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

371 

372 Parameters 

373 ---------- 

374 diaObject : `dict` 

375 Summary object to store values in. 

376 diaSources : `pandas.DataFrame` 

377 DataFrame representing all diaSources associated with this 

378 diaObject. 

379 filterDiaSources : `pandas.DataFrame` 

380 DataFrame representing diaSources associated with this 

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

382 filterName : `str` 

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

384 **kwargs 

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

386 """ 

387 pTileNames = [] 

388 for tilePercent in self.config.percentiles: 

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

390 tilePercent) 

391 pTileNames.append(pTileName) 

392 if pTileName not in diaObjects.columns: 

393 diaObjects[pTileName] = np.nan 

394 

395 def _fluxPercentiles(df): 

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

397 return pd.Series( 

398 dict((tileName, pTile) 

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

400 

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

402 

403 

404class SigmaDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

405 pass 

406 

407 

408@register("ap_sigmaFlux") 

409class SigmaDiaPsFlux(DiaObjectCalculationPlugin): 

410 """Compute scatter of diaSource fluxes. 

411 """ 

412 

413 ConfigClass = SigmaDiaPsFluxConfig 

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

415 outputCols = ["PSFluxSigma"] 

416 plugType = "multi" 

417 needsFilter = True 

418 

419 @classmethod 

420 def getExecutionOrder(cls): 

421 return cls.DEFAULT_CATALOGCALCULATION 

422 

423 def calculate(self, 

424 diaObjects, 

425 diaSources, 

426 filterDiaSources, 

427 filterName, 

428 **kwargs): 

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

430 

431 Parameters 

432 ---------- 

433 diaObject : `dict` 

434 Summary object to store values in. 

435 diaSources : `pandas.DataFrame` 

436 DataFrame representing all diaSources associated with this 

437 diaObject. 

438 filterDiaSources : `pandas.DataFrame` 

439 DataFrame representing diaSources associated with this 

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

441 filterName : `str` 

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

443 **kwargs 

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

445 """ 

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

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

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

449 filterDiaSources.psFlux.std() 

450 

451 

452class Chi2DiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

453 pass 

454 

455 

456@register("ap_chi2Flux") 

457class Chi2DiaPsFlux(DiaObjectCalculationPlugin): 

458 """Compute chi2 of diaSource fluxes. 

459 """ 

460 

461 ConfigClass = Chi2DiaPsFluxConfig 

462 

463 # Required input Cols 

464 inputCols = ["PSFluxMean"] 

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

466 outputCols = ["PSFluxChi2"] 

467 plugType = "multi" 

468 needsFilter = True 

469 

470 @classmethod 

471 def getExecutionOrder(cls): 

472 return cls.FLUX_MOMENTS_CALCULATED 

473 

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

475 def calculate(self, 

476 diaObjects, 

477 diaSources, 

478 filterDiaSources, 

479 filterName, 

480 **kwargs): 

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

482 

483 Parameters 

484 ---------- 

485 diaObject : `dict` 

486 Summary object to store values in. 

487 diaSources : `pandas.DataFrame` 

488 DataFrame representing all diaSources associated with this 

489 diaObject. 

490 filterDiaSources : `pandas.DataFrame` 

491 DataFrame representing diaSources associated with this 

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

493 filterName : `str` 

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

495 **kwargs 

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

497 """ 

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

499 

500 def _chi2(df): 

501 delta = (df["psFlux"] 

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

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

504 

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

506 filterDiaSources.apply(_chi2) 

507 

508 

509class MadDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

510 pass 

511 

512 

513@register("ap_madFlux") 

514class MadDiaPsFlux(DiaObjectCalculationPlugin): 

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

516 """ 

517 

518 ConfigClass = MadDiaPsFluxConfig 

519 

520 # Required input Cols 

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

522 outputCols = ["PSFluxMAD"] 

523 plugType = "multi" 

524 needsFilter = True 

525 

526 @classmethod 

527 def getExecutionOrder(cls): 

528 return cls.DEFAULT_CATALOGCALCULATION 

529 

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

531 def calculate(self, 

532 diaObjects, 

533 diaSources, 

534 filterDiaSources, 

535 filterName, 

536 **kwargs): 

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

538 

539 Parameters 

540 ---------- 

541 diaObject : `dict` 

542 Summary object to store values in. 

543 diaSources : `pandas.DataFrame` 

544 DataFrame representing all diaSources associated with this 

545 diaObject. 

546 filterDiaSources : `pandas.DataFrame` 

547 DataFrame representing diaSources associated with this 

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

549 filterName : `str` 

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

551 **kwargs 

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

553 """ 

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

555 filterDiaSources.psFlux.apply(median_absolute_deviation, 

556 ignore_nan=True) 

557 

558 

559class SkewDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

560 pass 

561 

562 

563@register("ap_skewFlux") 

564class SkewDiaPsFlux(DiaObjectCalculationPlugin): 

565 """Compute the skew of diaSource fluxes. 

566 """ 

567 

568 ConfigClass = SkewDiaPsFluxConfig 

569 

570 # Required input Cols 

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

572 outputCols = ["PSFluxSkew"] 

573 plugType = "multi" 

574 needsFilter = True 

575 

576 @classmethod 

577 def getExecutionOrder(cls): 

578 return cls.DEFAULT_CATALOGCALCULATION 

579 

580 def calculate(self, 

581 diaObjects, 

582 diaSources, 

583 filterDiaSources, 

584 filterName, 

585 **kwargs): 

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

587 

588 Parameters 

589 ---------- 

590 diaObject : `dict` 

591 Summary object to store values in. 

592 diaSources : `pandas.DataFrame` 

593 DataFrame representing all diaSources associated with this 

594 diaObject. 

595 filterDiaSources : `pandas.DataFrame` 

596 DataFrame representing diaSources associated with this 

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

598 filterName : `str` 

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

600 **kwargs 

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

602 """ 

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

604 filterDiaSources.psFlux.skew() 

605 

606 

607class MinMaxDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

608 pass 

609 

610 

611@register("ap_minMaxFlux") 

612class MinMaxDiaPsFlux(DiaObjectCalculationPlugin): 

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

614 """ 

615 

616 ConfigClass = MinMaxDiaPsFluxConfig 

617 

618 # Required input Cols 

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

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

621 plugType = "multi" 

622 needsFilter = True 

623 

624 @classmethod 

625 def getExecutionOrder(cls): 

626 return cls.DEFAULT_CATALOGCALCULATION 

627 

628 def calculate(self, 

629 diaObjects, 

630 diaSources, 

631 filterDiaSources, 

632 filterName, 

633 **kwargs): 

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

635 

636 Parameters 

637 ---------- 

638 diaObject : `dict` 

639 Summary object to store values in. 

640 diaSources : `pandas.DataFrame` 

641 DataFrame representing all diaSources associated with this 

642 diaObject. 

643 filterDiaSources : `pandas.DataFrame` 

644 DataFrame representing diaSources associated with this 

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

646 filterName : `str` 

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

648 **kwargs 

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

650 """ 

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

652 if minName not in diaObjects.columns: 

653 diaObjects[minName] = np.nan 

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

655 if maxName not in diaObjects.columns: 

656 diaObjects[maxName] = np.nan 

657 

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

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

660 

661 

662class MaxSlopeDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

663 pass 

664 

665 

666@register("ap_maxSlopeFlux") 

667class MaxSlopeDiaPsFlux(DiaObjectCalculationPlugin): 

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

669 """ 

670 

671 ConfigClass = MinMaxDiaPsFluxConfig 

672 

673 # Required input Cols 

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

675 outputCols = ["PSFluxMaxSlope"] 

676 plugType = "multi" 

677 needsFilter = True 

678 

679 @classmethod 

680 def getExecutionOrder(cls): 

681 return cls.DEFAULT_CATALOGCALCULATION 

682 

683 def calculate(self, 

684 diaObjects, 

685 diaSources, 

686 filterDiaSources, 

687 filterName, 

688 **kwargs): 

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

690 

691 Parameters 

692 ---------- 

693 diaObject : `dict` 

694 Summary object to store values in. 

695 diaSources : `pandas.DataFrame` 

696 DataFrame representing all diaSources associated with this 

697 diaObject. 

698 filterDiaSources : `pandas.DataFrame` 

699 DataFrame representing diaSources associated with this 

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

701 filterName : `str` 

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

703 **kwargs 

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

705 """ 

706 

707 def _maxSlope(df): 

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

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

710 if len(tmpDf) < 2: 

711 return np.nan 

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

713 timeArgs = times.argsort() 

714 times = times[timeArgs] 

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

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

717 

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

719 filterDiaSources.apply(_maxSlope) 

720 

721 

722class ErrMeanDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

723 pass 

724 

725 

726@register("ap_meanErrFlux") 

727class ErrMeanDiaPsFlux(DiaObjectCalculationPlugin): 

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

729 """ 

730 

731 ConfigClass = ErrMeanDiaPsFluxConfig 

732 

733 # Required input Cols 

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

735 outputCols = ["PSFluxErrMean"] 

736 plugType = "multi" 

737 needsFilter = True 

738 

739 @classmethod 

740 def getExecutionOrder(cls): 

741 return cls.DEFAULT_CATALOGCALCULATION 

742 

743 def calculate(self, 

744 diaObjects, 

745 diaSources, 

746 filterDiaSources, 

747 filterName, 

748 **kwargs): 

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

750 

751 Parameters 

752 ---------- 

753 diaObject : `dict` 

754 Summary object to store values in. 

755 diaSources : `pandas.DataFrame` 

756 DataFrame representing all diaSources associated with this 

757 diaObject. 

758 filterDiaSources : `pandas.DataFrame` 

759 DataFrame representing diaSources associated with this 

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

761 filterName : `str` 

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

763 **kwargs 

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

765 """ 

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

767 filterDiaSources.psFluxErr.mean() 

768 

769 

770class LinearFitDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

771 pass 

772 

773 

774@register("ap_linearFit") 

775class LinearFitDiaPsFlux(DiaObjectCalculationPlugin): 

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

777 """ 

778 

779 ConfigClass = LinearFitDiaPsFluxConfig 

780 

781 # Required input Cols 

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

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

784 plugType = "multi" 

785 needsFilter = True 

786 

787 @classmethod 

788 def getExecutionOrder(cls): 

789 return cls.DEFAULT_CATALOGCALCULATION 

790 

791 def calculate(self, 

792 diaObjects, 

793 diaSources, 

794 filterDiaSources, 

795 filterName, 

796 **kwargs): 

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

798 

799 Parameters 

800 ---------- 

801 diaObject : `dict` 

802 Summary object to store values in. 

803 diaSources : `pandas.DataFrame` 

804 DataFrame representing all diaSources associated with this 

805 diaObject. 

806 filterDiaSources : `pandas.DataFrame` 

807 DataFrame representing diaSources associated with this 

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

809 filterName : `str` 

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

811 **kwargs 

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

813 """ 

814 

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

816 if mName not in diaObjects.columns: 

817 diaObjects[mName] = np.nan 

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

819 if bName not in diaObjects.columns: 

820 diaObjects[bName] = np.nan 

821 

822 def _linearFit(df): 

823 tmpDf = df[~np.logical_or( 

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

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

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

827 if len(tmpDf) < 2: 

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

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

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

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

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

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

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

835 

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

837 

838 

839class StetsonJDiaPsFluxConfig(DiaObjectCalculationPluginConfig): 

840 pass 

841 

842 

843@register("ap_stetsonJ") 

844class StetsonJDiaPsFlux(DiaObjectCalculationPlugin): 

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

846 """ 

847 

848 ConfigClass = LinearFitDiaPsFluxConfig 

849 

850 # Required input Cols 

851 inputCols = ["PSFluxMean"] 

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

853 outputCols = ["PSFluxStetsonJ"] 

854 plugType = "multi" 

855 needsFilter = True 

856 

857 @classmethod 

858 def getExecutionOrder(cls): 

859 return cls.FLUX_MOMENTS_CALCULATED 

860 

861 def calculate(self, 

862 diaObjects, 

863 diaSources, 

864 filterDiaSources, 

865 filterName, 

866 **kwargs): 

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

868 

869 Parameters 

870 ---------- 

871 diaObject : `dict` 

872 Summary object to store values in. 

873 diaSources : `pandas.DataFrame` 

874 DataFrame representing all diaSources associated with this 

875 diaObject. 

876 filterDiaSources : `pandas.DataFrame` 

877 DataFrame representing diaSources associated with this 

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

879 filterName : `str` 

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

881 **kwargs 

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

883 """ 

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

885 

886 def _stetsonJ(df): 

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

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

889 if len(tmpDf) < 2: 

890 return np.nan 

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

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

893 

894 return self._stetson_J( 

895 fluxes, 

896 errors, 

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

898 

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

900 filterDiaSources.apply(_stetsonJ) 

901 

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

903 """Compute the single band stetsonJ statistic. 

904 

905 Parameters 

906 ---------- 

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

908 Calibrated lightcurve flux values. 

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

910 Errors on the calibrated lightcurve fluxes. 

911 mean : `float` 

912 Starting mean from previous plugin. 

913 

914 Returns 

915 ------- 

916 stetsonJ : `float` 

917 stetsonJ statistic for the input fluxes and errors. 

918 

919 References 

920 ---------- 

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

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

923 """ 

924 n_points = len(fluxes) 

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

926 delta_val = ( 

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

928 p_k = delta_val ** 2 - 1 

929 

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

931 

932 def _stetson_mean(self, 

933 values, 

934 errors, 

935 mean=None, 

936 alpha=2., 

937 beta=2., 

938 n_iter=20, 

939 tol=1e-6): 

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

941 

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

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

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

945 difference of 1 occurs. 

946 

947 Parameters 

948 ---------- 

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

950 Input values to compute the mean of. 

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

952 Errors on the input values. 

953 mean : `float` 

954 Starting mean value or None. 

955 alpha : `float` 

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

957 clipping. (Default value is 2.) 

958 beta : `float` 

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

960 clipping. (Default value is 2.) 

961 n_iter : `int` 

962 Number of iterations of clipping. 

963 tol : `float` 

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

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

966 

967 Returns 

968 ------- 

969 mean : `float` 

970 Weighted stetson mean result. 

971 

972 References 

973 ---------- 

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

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

976 """ 

977 n_points = len(values) 

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

979 inv_var = 1 / errors ** 2 

980 

981 if mean is None: 

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

983 for iter_idx in range(n_iter): 

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

985 tmp_mean = np.average( 

986 values, 

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

988 diff = np.fabs(tmp_mean - mean) 

989 mean = tmp_mean 

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

991 break 

992 return mean 

993 

994 

995class WeightedMeanDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

996 pass 

997 

998 

999@register("ap_meanTotFlux") 

1000class WeightedMeanDiaTotFlux(DiaObjectCalculationPlugin): 

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

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

1003 

1004 Additionally store number of usable data points. 

1005 """ 

1006 

1007 ConfigClass = WeightedMeanDiaPsFluxConfig 

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

1009 plugType = "multi" 

1010 needsFilter = True 

1011 

1012 @classmethod 

1013 def getExecutionOrder(cls): 

1014 return cls.DEFAULT_CATALOGCALCULATION 

1015 

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

1017 "divide by zero"]) 

1018 def calculate(self, 

1019 diaObjects, 

1020 diaSources, 

1021 filterDiaSources, 

1022 filterName, 

1023 **kwargs): 

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

1025 

1026 Parameters 

1027 ---------- 

1028 diaObject : `dict` 

1029 Summary object to store values in. 

1030 diaSources : `pandas.DataFrame` 

1031 DataFrame representing all diaSources associated with this 

1032 diaObject. 

1033 filterDiaSources : `pandas.DataFrame` 

1034 DataFrame representing diaSources associated with this 

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

1036 filterName : `str` 

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

1038 **kwargs 

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

1040 """ 

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

1042 if totMeanName not in diaObjects.columns: 

1043 diaObjects[totMeanName] = np.nan 

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

1045 if totErrName not in diaObjects.columns: 

1046 diaObjects[totErrName] = np.nan 

1047 

1048 def _meanFlux(df): 

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

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

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

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

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

1054 fluxMean /= tot_weight 

1055 fluxMeanErr = np.sqrt(1 / tot_weight) 

1056 

1057 return pd.Series({totMeanName: fluxMean, 

1058 totErrName: fluxMeanErr}) 

1059 

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

1061 filterDiaSources.apply(_meanFlux) 

1062 

1063 

1064class SigmaDiaTotFluxConfig(DiaObjectCalculationPluginConfig): 

1065 pass 

1066 

1067 

1068@register("ap_sigmaTotFlux") 

1069class SigmaDiaTotFlux(DiaObjectCalculationPlugin): 

1070 """Compute scatter of diaSource fluxes. 

1071 """ 

1072 

1073 ConfigClass = SigmaDiaPsFluxConfig 

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

1075 outputCols = ["TOTFluxSigma"] 

1076 plugType = "multi" 

1077 needsFilter = True 

1078 

1079 @classmethod 

1080 def getExecutionOrder(cls): 

1081 return cls.DEFAULT_CATALOGCALCULATION 

1082 

1083 def calculate(self, 

1084 diaObjects, 

1085 diaSources, 

1086 filterDiaSources, 

1087 filterName, 

1088 **kwargs): 

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

1090 calibrated image. 

1091 

1092 Parameters 

1093 ---------- 

1094 diaObject : `dict` 

1095 Summary object to store values in. 

1096 diaSources : `pandas.DataFrame` 

1097 DataFrame representing all diaSources associated with this 

1098 diaObject. 

1099 filterDiaSources : `pandas.DataFrame` 

1100 DataFrame representing diaSources associated with this 

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

1102 filterName : `str` 

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

1104 **kwargs 

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

1106 """ 

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

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

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

1110 filterDiaSources.totFlux.std()