Coverage for python/lsst/analysis/drp/calcFunctors.py: 35%

308 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-14 03:32 -0700

1# This file is part of analysis_drp. 

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__all__ = ["SNCalculator", "KronFluxDivPsfFlux", "MagDiff", "ColorDiff", "ColorDiffPull", 

23 "ExtinctionCorrectedMagDiff", "CalcE", "CalcE1", "CalcE2", "CalcEDiff", "CalcShapeSize", 

24 "CalcRhoStatistics", ] 

25 

26import logging 

27 

28import numpy as np 

29import treecorr 

30from astropy import units as u 

31 

32from lsst.pex.config import ChoiceField, ConfigField, DictField, Field, FieldValidationError 

33from lsst.pipe.tasks.configurableActions import ConfigurableActionField 

34from lsst.pipe.tasks.dataFrameActions import (CoordColumn, DataFrameAction, DivideColumns, 

35 FractionalDifferenceColumns, MultiColumnAction, 

36 SingleColumnAction,) 

37 

38from ._treecorrConfig import BinnedCorr2Config 

39 

40_LOG = logging.getLogger(__name__) 

41 

42 

43class SNCalculator(DivideColumns): 

44 """Calculate the signal to noise by default the i band PSF flux is used""" 

45 

46 def setDefaults(self): 

47 super().setDefaults() 

48 self.colA.column = "i_psfFlux" 

49 self.colB.column = "i_psfFluxErr" 

50 

51 

52class KronFluxDivPsfFlux(DivideColumns): 

53 """Divide the Kron instFlux by the PSF instFlux""" 

54 

55 def setDefaults(self): 

56 super().setDefaults() 

57 self.colA.column = "i_kronFlux" 

58 self.colB.column = "i_psfFlux" 

59 

60 

61class MagDiff(MultiColumnAction): 

62 """Calculate the difference between two magnitudes; 

63 each magnitude is derived from a flux column. 

64 

65 Parameters 

66 ---------- 

67 df : `pandas.core.frame.DataFrame` 

68 The catalog to calculate the magnitude difference from. 

69 

70 Returns 

71 ------- 

72 The magnitude difference in milli mags. 

73 

74 Notes 

75 ----- 

76 The flux columns need to be in units (specifiable in 

77 the fluxUnits1 and 2 config options) that can be converted 

78 to janskies. This action doesn't have any calibration 

79 information and assumes that the fluxes are already 

80 calibrated. 

81 """ 

82 

83 col1 = Field(doc="Column to subtract from", dtype=str) 

84 fluxUnits1 = Field(doc="Units for col1", dtype=str, default="nanojansky") 

85 col2 = Field(doc="Column to subtract", dtype=str) 

86 fluxUnits2 = Field(doc="Units for col2", dtype=str, default="nanojansky") 

87 returnMillimags = Field(doc="Use millimags or not?", dtype=bool, default=True) 

88 

89 @property 

90 def columns(self): 

91 return (self.col1, self.col2) 

92 

93 def __call__(self, df): 

94 flux1 = df[self.col1].values * u.Unit(self.fluxUnits1) 

95 mag1 = flux1.to(u.ABmag) 

96 

97 flux2 = df[self.col2].values * u.Unit(self.fluxUnits2) 

98 mag2 = flux2.to(u.ABmag) 

99 

100 magDiff = mag1 - mag2 

101 

102 if self.returnMillimags: 

103 magDiff = magDiff.to(u.mmag) 

104 

105 return magDiff 

106 

107 

108class ExtinctionCorrectedMagDiff(DataFrameAction): 

109 """Compute the difference between two magnitudes and correct for extinction 

110 

111 By default bands are derived from the <band>_ prefix on flux columns, 

112 per the naming convention in the Object Table: 

113 e.g. the band of 'g_psfFlux' is 'g'. If column names follow another 

114 convention, bands can alternatively be supplied via the band1 or band2 

115 config parameters. 

116 If band1 and band2 are supplied, the flux column names are ignored. 

117 """ 

118 

119 magDiff = ConfigurableActionField(doc="Action that returns a difference in magnitudes", 

120 default=MagDiff, dtype=DataFrameAction) 

121 ebvCol = Field(doc="E(B-V) Column Name", dtype=str, default="ebv") 

122 band1 = Field(doc="Optional band for magDiff.col1. Supercedes column name prefix", 

123 dtype=str, optional=True, default=None) 

124 band2 = Field(doc="Optional band for magDiff.col2. Supercedes column name prefix", 

125 dtype=str, optional=True, default=None) 

126 extinctionCoeffs = DictField( 

127 doc="Dictionary of extinction coefficients for conversion from E(B-V) to extinction, A_band." 

128 "Key must be the band", 

129 keytype=str, itemtype=float, optional=True, 

130 default=None) 

131 

132 @property 

133 def columns(self): 

134 return self.magDiff.columns + (self.ebvCol,) 

135 

136 def __call__(self, df): 

137 diff = self.magDiff(df) 

138 if not self.extinctionCoeffs: 

139 _LOG.warning("No extinction Coefficients. Not applying extinction correction") 

140 return diff 

141 

142 col1Band = self.band1 if self.band1 else self.magDiff.col1.split('_')[0] 

143 col2Band = self.band2 if self.band2 else self.magDiff.col2.split('_')[0] 

144 

145 for band in (col1Band, col1Band): 

146 if band not in self.extinctionCoeffs: 

147 _LOG.warning("%s band not found in coefficients dictionary: %s" 

148 " Not applying extinction correction", band, self.extinctionCoeffs) 

149 return diff 

150 

151 av1 = self.extinctionCoeffs[col1Band] 

152 av2 = self.extinctionCoeffs[col2Band] 

153 

154 ebv = df[self.ebvCol].values 

155 correction = (av1 - av2) * ebv * u.mag 

156 

157 if self.magDiff.returnMillimags: 

158 correction = correction.to(u.mmag) 

159 

160 return diff - correction 

161 

162 

163class CalcE(MultiColumnAction): 

164 """Calculate a complex value representation of the ellipticity. 

165 

166 The complex ellipticity is typically defined as 

167 e = |e|exp(j*2*theta) = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy), where j is 

168 the square root of -1 and Ixx, Iyy, Ixy are second-order central moments. 

169 This is sometimes referred to as distortion, and denoted by e = (e1, e2) 

170 in GalSim and referred to as chi-type ellipticity following the notation 

171 in Eq. 4.4 of Bartelmann and Schneider (2001). The other definition differs 

172 in normalization. It is referred to as shear, and denoted by g = (g1, g2) 

173 in GalSim and referred to as epsilon-type ellipticity again following the 

174 notation in Eq. 4.10 of Bartelmann and Schneider (2001). It is defined as 

175 g = ((Ixx - Iyy) + j*(2*Ixy))/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)). 

176 

177 The shear measure is unbiased in weak-lensing shear, but may exclude some 

178 objects in the presence of noisy moment estimates. The distortion measure 

179 is biased in weak-lensing distortion, but does not suffer from selection 

180 artifacts. 

181 

182 References 

183 ---------- 

184 [1] Bartelmann, M. and Schneider, P., “Weak gravitational lensing”, 

185 Physics Reports, vol. 340, no. 4–5, pp. 291–472, 2001. 

186 doi:10.1016/S0370-1573(00)00082-X; https://arxiv.org/abs/astro-ph/9912508 

187 

188 Notes 

189 ----- 

190 

191 1. This is a shape measurement used for doing QA on the ellipticity 

192 of the sources. 

193 

194 2. For plotting purposes we might want to plot |E|*exp(i*theta). 

195 If `halvePhaseAngle` config parameter is set to `True`, then 

196 the returned quantity therefore corresponds to |E|*exp(i*theta). 

197 

198 See Also 

199 -------- 

200 CalcE1 

201 CalcE2 

202 """ 

203 

204 colXx = Field( 

205 doc="The column name to get the xx shape component from.", 

206 dtype=str, 

207 default="ixx", 

208 ) 

209 

210 colYy = Field( 

211 doc="The column name to get the yy shape component from.", 

212 dtype=str, 

213 default="iyy", 

214 ) 

215 

216 colXy = Field( 

217 doc="The column name to get the xy shape component from.", 

218 dtype=str, 

219 default="ixy", 

220 ) 

221 

222 ellipticityType = ChoiceField( 

223 doc="The type of ellipticity to calculate", 

224 dtype=str, 

225 allowed={"chi": ("Distortion, defined as (Ixx - Iyy + 2j*Ixy)/" 

226 "(Ixx + Iyy)" 

227 ), 

228 "epsilon": ("Shear, defined as (Ixx - Iyy + 2j*Ixy)/" 

229 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))" 

230 ), 

231 }, 

232 default="chi", 

233 ) 

234 

235 halvePhaseAngle = Field( 

236 doc="Divide the phase angle by 2? Suitable for quiver plots.", 

237 dtype=bool, 

238 default=False, 

239 ) 

240 

241 @property 

242 def columns(self): 

243 return (self.colXx, self.colYy, self.colXy) 

244 

245 def __call__(self, df): 

246 e = (df[self.colXx] - df[self.colYy]) + 1j*(2*df[self.colXy]) 

247 denom = (df[self.colXx] + df[self.colYy]) 

248 

249 if self.ellipticityType == "epsilon": 

250 denom += 2*np.sqrt(df[self.colXx]*df[self.colYy] - df[self.colXy]**2) 

251 

252 e /= denom 

253 

254 if self.halvePhaseAngle: 

255 # Ellipiticity is |e|*exp(i*2*theta), but we want to return 

256 # |e|*exp(i*theta). So we multiply by |e| and take its square root 

257 # instead of the more expensive trig calls. 

258 e *= np.abs(e) 

259 return np.sqrt(e) 

260 else: 

261 return e 

262 

263 

264class CalcEDiff(DataFrameAction): 

265 """Calculate the difference of two ellipticities as a complex quantity. 

266 

267 The complex ellipticity difference between e_A and e_B is defined as 

268 e_A - e_B = de = |de|exp(j*2*theta). 

269 

270 See Also 

271 -------- 

272 CalcE 

273 

274 Notes 

275 ----- 

276 

277 1. This is a shape measurement used for doing QA on the ellipticity 

278 of the sources. 

279 

280 2. For plotting purposes we might want to plot |de|*exp(j*theta). 

281 If `halvePhaseAngle` config parameter is set to `True`, then 

282 the returned quantity therefore corresponds to |e|*exp(j*theta). 

283 """ 

284 colA = ConfigurableActionField( 

285 doc="Ellipticity to subtract from", 

286 dtype=MultiColumnAction, 

287 default=CalcE, 

288 ) 

289 

290 colB = ConfigurableActionField( 

291 doc="Ellipticity to subtract", 

292 dtype=MultiColumnAction, 

293 default=CalcE, 

294 ) 

295 

296 halvePhaseAngle = Field( 

297 doc="Divide the phase angle by 2? Suitable for quiver plots.", 

298 dtype=bool, 

299 default=False, 

300 ) 

301 

302 @property 

303 def columns(self): 

304 yield from self.colA.columns 

305 yield from self.colB.columns 

306 

307 def validate(self): 

308 super().validate() 

309 if self.colA.ellipticityType != self.colB.ellipticityType: 

310 msg = "Both the ellipticities in CalcEDiff must have the same type." 

311 raise FieldValidationError(self.colB.__class__.ellipticityType, self, msg) 

312 

313 def __call__(self, df): 

314 eMeas = self.colA(df) 

315 ePSF = self.colB(df) 

316 eDiff = eMeas - ePSF 

317 if self.halvePhaseAngle: 

318 # Ellipiticity is |e|*exp(i*2*theta), but we want to return 

319 # |e|*exp(j*theta). So we multiply by |e| and take its square root 

320 # instead of the more expensive trig calls. 

321 eDiff *= np.abs(eDiff) 

322 return np.sqrt(eDiff) 

323 else: 

324 return eDiff 

325 

326 

327class CalcE1(MultiColumnAction): 

328 """Calculate chi-type e1 = (Ixx - Iyy)/(Ixx + Iyy) or 

329 epsilon-type g1 = (Ixx - Iyy)/(Ixx + Iyy + 2sqrt(Ixx*Iyy - Ixy**2)). 

330 

331 See Also 

332 -------- 

333 CalcE 

334 CalcE2 

335 

336 Note 

337 ---- 

338 This is a shape measurement used for doing QA on the ellipticity 

339 of the sources. 

340 """ 

341 

342 colXx = Field( 

343 doc="The column name to get the xx shape component from.", 

344 dtype=str, 

345 default="ixx", 

346 ) 

347 

348 colYy = Field( 

349 doc="The column name to get the yy shape component from.", 

350 dtype=str, 

351 default="iyy", 

352 ) 

353 

354 colXy = Field( 

355 doc="The column name to get the xy shape component from.", 

356 dtype=str, 

357 default="ixy", 

358 optional=True, 

359 ) 

360 

361 ellipticityType = ChoiceField( 

362 doc="The type of ellipticity to calculate", 

363 dtype=str, 

364 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)", 

365 "epsilon": ("Shear, defined as (Ixx - Iyy)/" 

366 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))" 

367 ), 

368 }, 

369 default="chi", 

370 ) 

371 

372 @property 

373 def columns(self): 

374 if self.ellipticityType == "chi": 

375 return (self.colXx, self.colYy) 

376 else: 

377 return (self.colXx, self.colYy, self.colXy) 

378 

379 def __call__(self, df): 

380 denom = df[self.colXx] + df[self.colYy] 

381 if self.ellipticityType == "epsilon": 

382 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2) 

383 e1 = (df[self.colXx] - df[self.colYy])/denom 

384 

385 return e1 

386 

387 def validate(self): 

388 super().validate() 

389 if self.ellipticityType == "epsilon" and self.colXy is None: 

390 msg = "colXy is required for epsilon-type shear ellipticity" 

391 raise FieldValidationError(self.__class__.colXy, self, msg) 

392 

393 

394class CalcE2(MultiColumnAction): 

395 """Calculate chi-type e2 = 2Ixy/(Ixx+Iyy) or 

396 epsilon-type g2 = 2Ixy/(Ixx+Iyy+2sqrt(Ixx*Iyy - Ixy**2)). 

397 

398 See Also 

399 -------- 

400 CalcE 

401 CalcE1 

402 

403 Note 

404 ---- 

405 This is a shape measurement used for doing QA on the ellipticity 

406 of the sources. 

407 """ 

408 

409 colXx = Field( 

410 doc="The column name to get the xx shape component from.", 

411 dtype=str, 

412 default="ixx", 

413 ) 

414 

415 colYy = Field( 

416 doc="The column name to get the yy shape component from.", 

417 dtype=str, 

418 default="iyy", 

419 ) 

420 

421 colXy = Field( 

422 doc="The column name to get the xy shape component from.", 

423 dtype=str, 

424 default="ixy", 

425 ) 

426 

427 ellipticityType = ChoiceField( 

428 doc="The type of ellipticity to calculate", 

429 dtype=str, 

430 allowed={"chi": "Distortion, defined as 2*Ixy/(Ixx + Iyy)", 

431 "epsilon": ("Shear, defined as 2*Ixy/" 

432 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))" 

433 ), 

434 }, 

435 default="chi", 

436 ) 

437 

438 @property 

439 def columns(self): 

440 return (self.colXx, self.colYy, self.colXy) 

441 

442 def __call__(self, df): 

443 denom = df[self.colXx] + df[self.colYy] 

444 if self.ellipticityType == "epsilon": 

445 denom += 2*np.sqrt(df[self.colXx] * df[self.colYy] - df[self.colXy]**2) 

446 e2 = 2*df[self.colXy]/denom 

447 return e2 

448 

449 

450class CalcShapeSize(MultiColumnAction): 

451 """Calculate a size: (Ixx*Iyy - Ixy**2)**0.25 OR (0.5*(Ixx + Iyy))**0.5 

452 

453 The square of size measure is typically expressed either as the arithmetic 

454 mean of the eigenvalues of the moment matrix (trace radius) or as the 

455 geometric mean of the eigenvalues (determinant radius), which can be 

456 specified using the ``sizeType`` parameter. Both of these measures give the 

457 `sigma^2` parameter for a 2D Gaussian. 

458 

459 Since lensing preserves surface brightness, the determinant radius relates 

460 the magnification cleanly as it is derived from the area of isophotes, but 

461 have a slightly higher chance of being NaNs for noisy moment estimates. 

462 

463 Note 

464 ---- 

465 This is a size measurement used for doing QA on the ellipticity 

466 of the sources. 

467 """ 

468 

469 colXx = Field( 

470 doc="The column name to get the xx shape component from.", 

471 dtype=str, 

472 default="ixx", 

473 ) 

474 

475 colYy = Field( 

476 doc="The column name to get the yy shape component from.", 

477 dtype=str, 

478 default="iyy", 

479 ) 

480 

481 colXy = Field( 

482 doc="The column name to get the xy shape component from.", 

483 dtype=str, 

484 default="ixy", 

485 optional=True, 

486 ) 

487 

488 sizeType = ChoiceField( 

489 doc="The type of size to calculate", 

490 dtype=str, 

491 default="determinant", 

492 allowed={"trace": "trace radius", 

493 "determinant": "determinant radius", 

494 }, 

495 ) 

496 

497 @property 

498 def columns(self): 

499 if self.sizeType == "trace": 

500 return (self.colXx, self.colYy,) 

501 else: 

502 return (self.colXx, self.colYy, self.colXy) 

503 

504 def __call__(self, df): 

505 if self.sizeType == "trace": 

506 size = np.power(0.5*(df[self.colXx] + df[self.colYy]), 0.5) 

507 else: 

508 size = np.power(df[self.colXx]*df[self.colYy] - df[self.colXy]**2, 0.25) 

509 

510 return size 

511 

512 def validate(self): 

513 super().validate() 

514 if self.sizeType == "determinant" and self.colXy is None: 

515 msg = "colXy is required for determinant-type size" 

516 raise FieldValidationError(self.__class__.colXy, self, msg) 

517 

518 

519class CalcRhoStatistics(DataFrameAction): 

520 r"""Calculate Rho statistics. 

521 

522 Rho statistics refer to a collection of correlation functions involving 

523 PSF ellipticity and size residuals. They quantify the contribution from PSF 

524 leakage due to errors in PSF modeling to the weak lensing shear correlation 

525 functions. The standard rho statistics are indexed from 1 to 5, and 

526 this action calculates a sixth rho statistic, indexed 0. 

527 

528 Notes 

529 ----- 

530 The exact definitions of rho statistics as defined in [1]_ are given below. 

531 In addition to these five, we also compute the auto-correlation function of 

532 the fractional size residuals and call it as the :math:`\rho_0( \theta )`. 

533 

534 .. math:: 

535 

536 \rho_0(\theta) &= \left\langle \frac{\delta T_{PSF}}{T_{PSF}}(x) \frac{\delta T_{PSF}}{T_{PSF}}(x+\theta) \right\rangle # noqa: E501, W505 

537 

538 \rho_1(\theta) &= \langle \delta e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle # noqa: W505 

539 

540 \rho_2(\theta) &= \langle e^*_{PSF}(x) \delta e_{PSF}(x+\theta) \rangle 

541 

542 \rho_3(\theta) &= \left\langle (e^*_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x)) \delta e_{PSF}(x+\theta) \right\rangle # noqa: E501, W505 

543 

544 \rho_4(\theta) &= \left\langle (\delta e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505 

545 

546 \rho_5(\theta) &= \left\langle (e^*_{PSF}(x) (e_{PSF}\frac{\delta T_{PSF}}{T_{PSF}}(x+\theta)) \right\rangle # noqa: E501, W505 

547 

548 The definition of ellipticity used in [1]_ correspond to ``epsilon``-type ellipticity, which is typically 

549 smaller by a factor of 4 than using ``chi``-type ellipticity. 

550 

551 References 

552 ---------- 

553 .. [1] Jarvis, M., Sheldon, E., Zuntz, J., Kacprzak, T., Bridle, S. L., et. al (2016). # noqa: W501 

554 The DES Science Verification weak lensing shear catalogues. 

555 MNRAS, 460, 2245–2281. 

556 https://doi.org/10.1093/mnras/stw990; 

557 https://arxiv.org/abs/1507.05603 

558 """ 

559 

560 colRa = ConfigurableActionField(doc="RA column", dtype=SingleColumnAction, default=CoordColumn) 

561 

562 colDec = ConfigurableActionField(doc="Dec column", dtype=SingleColumnAction, default=CoordColumn) 

563 

564 colXx = Field( 

565 doc="The column name to get the xx shape component from.", 

566 dtype=str, 

567 default="ixx" 

568 ) 

569 

570 colYy = Field( 

571 doc="The column name to get the yy shape component from.", 

572 dtype=str, 

573 default="iyy" 

574 ) 

575 

576 colXy = Field( 

577 doc="The column name to get the xy shape component from.", 

578 dtype=str, 

579 default="ixy" 

580 ) 

581 

582 colPsfXx = Field( 

583 doc="The column name to get the PSF xx shape component from.", 

584 dtype=str, 

585 default="ixxPSF" 

586 ) 

587 

588 colPsfYy = Field( 

589 doc="The column name to get the PSF yy shape component from.", 

590 dtype=str, 

591 default="iyyPSF" 

592 ) 

593 

594 colPsfXy = Field( 

595 doc="The column name to get the PSF xy shape component from.", 

596 dtype=str, 

597 default="ixyPSF" 

598 ) 

599 

600 ellipticityType = ChoiceField( 

601 doc="The type of ellipticity to calculate", 

602 dtype=str, 

603 allowed={"chi": "Distortion, defined as (Ixx - Iyy)/(Ixx + Iyy)", 

604 "epsilon": ("Shear, defined as (Ixx - Iyy)/" 

605 "(Ixx + Iyy + 2*sqrt(Ixx*Iyy - Ixy**2))" 

606 ), 

607 }, 

608 default="chi", 

609 ) 

610 

611 sizeType = ChoiceField( 

612 doc="The type of size to calculate", 

613 dtype=str, 

614 default="trace", 

615 allowed={"trace": "trace radius", 

616 "determinant": "determinant radius", 

617 }, 

618 ) 

619 

620 treecorr = ConfigField( 

621 doc="TreeCorr configuration", 

622 dtype=BinnedCorr2Config, 

623 ) 

624 

625 def setDefaults(self): 

626 super().setDefaults() 

627 self.treecorr = BinnedCorr2Config() 

628 self.treecorr.sep_units = "arcmin" 

629 self.treecorr.metric = "Arc" 

630 # Note: self.treecorr is not configured completely at this point. 

631 # Exactly three of nbins, bin_size, min_sep, max_sep need to be set. 

632 # These are expected to be set in the tasks that use this action. 

633 

634 @property 

635 def columns(self): 

636 return ( 

637 self.colXx, 

638 self.colYy, 

639 self.colXy, 

640 self.colPsfXx, 

641 self.colPsfYy, 

642 self.colPsfXy, 

643 self.colRa.column, 

644 self.colDec.column, 

645 ) 

646 

647 def __call__(self, df): 

648 # Create instances of various actions. 

649 calcEMeas = CalcE( 

650 colXx=self.colXx, 

651 colYy=self.colYy, 

652 colXy=self.colXy, 

653 ellipticityType=self.ellipticityType, 

654 ) 

655 calcEpsf = CalcE( 

656 colXx=self.colPsfXx, 

657 colYy=self.colPsfYy, 

658 colXy=self.colPsfXy, 

659 ellipticityType=self.ellipticityType, 

660 ) 

661 

662 calcEDiff = CalcEDiff(colA=calcEMeas, colB=calcEpsf) 

663 

664 calcSizeResiduals = FractionalDifferenceColumns( 

665 colA=CalcShapeSize( 

666 colXx=self.colXx, 

667 colYy=self.colYy, 

668 colXy=self.colXy, 

669 sizeType=self.sizeType, 

670 ), 

671 colB=CalcShapeSize( 

672 colXx=self.colPsfXx, 

673 colYy=self.colPsfYy, 

674 colXy=self.colPsfXy, 

675 sizeType=self.sizeType, 

676 ), 

677 ) 

678 

679 # Call the actions on the dataframe. 

680 eMEAS = calcEMeas(df) 

681 e1, e2 = np.real(eMEAS), np.imag(eMEAS) 

682 eRes = calcEDiff(df) 

683 e1Res, e2Res = np.real(eRes), np.imag(eRes) 

684 sizeRes = calcSizeResiduals(df) 

685 

686 # Scale the sizeRes by ellipticities 

687 e1SizeRes = e1*sizeRes 

688 e2SizeRes = e2*sizeRes 

689 

690 # Package the arguments to capture auto-/cross-correlations for the 

691 # Rho statistics. 

692 args = { 

693 0: (sizeRes, None), 

694 1: (e1Res, e2Res, None, None), 

695 2: (e1, e2, e1Res, e2Res), 

696 3: (e1SizeRes, e2SizeRes, None, None), 

697 4: (e1Res, e2Res, e1SizeRes, e2SizeRes), 

698 5: (e1, e2, e1SizeRes, e2SizeRes), 

699 } 

700 

701 ra = self.colRa(df) 

702 dec = self.colDec(df) 

703 

704 # If RA and DEC are not in radians, they are assumed to be in degrees. 

705 if self.colRa.inRadians: 

706 ra *= 180.0/np.pi 

707 if self.colDec.inRadians: 

708 dec *= 180.0/np.pi 

709 

710 # Convert the self.treecorr Config to a kwarg dict. 

711 treecorrKwargs = self.treecorr.toDict() 

712 

713 # Pass the appropriate arguments to the correlator and build a dict 

714 rhoStats = { 

715 rhoIndex: self._corrSpin2(ra, dec, *(args[rhoIndex]), **treecorrKwargs) 

716 for rhoIndex in range(1, 6) 

717 } 

718 rhoStats[0] = self._corrSpin0(ra, dec, *(args[0]), **treecorrKwargs) 

719 

720 return rhoStats 

721 

722 @classmethod 

723 def _corrSpin0(cls, ra, dec, k1, k2=None, raUnits="degrees", decUnits="degrees", **treecorrKwargs): 

724 """Function to compute correlations between at most two scalar fields. 

725 

726 This is used to compute Rho0 statistics, given the appropriate spin-0 

727 (scalar) fields, usually fractional size residuals. 

728 

729 Parameters 

730 ---------- 

731 ra : `numpy.array` 

732 The right ascension values of entries in the catalog. 

733 dec : `numpy.array` 

734 The declination values of entries in the catalog. 

735 k1 : `numpy.array` 

736 The primary scalar field. 

737 k2 : `numpy.array`, optional 

738 The secondary scalar field. 

739 Autocorrelation of the primary field is computed if `None`. 

740 raUnits : `str`, optional 

741 Unit of the right ascension values. Valid options are 

742 "degrees", "arcmin", "arcsec", "hours" or "radians". 

743 decUnits : `str`, optional 

744 Unit of the declination values. Valid options are 

745 "degrees", "arcmin", "arcsec", "hours" or "radians". 

746 **treecorrKwargs 

747 Keyword arguments to be passed to `treecorr.KKCorrelation`. 

748 

749 Returns 

750 ------- 

751 xy : `treecorr.KKCorrelation` 

752 A `treecorr.KKCorrelation` object containing the correlation 

753 function. 

754 """ 

755 

756 xy = treecorr.KKCorrelation(**treecorrKwargs) 

757 catA = treecorr.Catalog(ra=ra, dec=dec, k=k1, ra_units=raUnits, 

758 dec_units=decUnits) 

759 if k2 is None: 

760 # Calculate the auto-correlation 

761 xy.process(catA) 

762 else: 

763 catB = treecorr.Catalog(ra=ra, dec=dec, k=k2, ra_units=raUnits, 

764 dec_units=decUnits) 

765 # Calculate the cross-correlation 

766 xy.process(catA, catB) 

767 

768 return xy 

769 

770 @classmethod 

771 def _corrSpin2(cls, ra, dec, g1a, g2a, g1b=None, g2b=None, 

772 raUnits="degrees", decUnits="degrees", **treecorrKwargs): 

773 """Function to compute correlations between shear-like fields. 

774 

775 This is used to compute Rho statistics, given the appropriate spin-2 

776 (shear-like) fields. 

777 

778 Parameters 

779 ---------- 

780 ra : `numpy.array` 

781 The right ascension values of entries in the catalog. 

782 dec : `numpy.array` 

783 The declination values of entries in the catalog. 

784 g1a : `numpy.array` 

785 The first component of the primary shear-like field. 

786 g2a : `numpy.array` 

787 The second component of the primary shear-like field. 

788 g1b : `numpy.array`, optional 

789 The first component of the secondary shear-like field. 

790 Autocorrelation of the primary field is computed if `None`. 

791 g2b : `numpy.array`, optional 

792 The second component of the secondary shear-like field. 

793 Autocorrelation of the primary field is computed if `None`. 

794 raUnits : `str`, optional 

795 Unit of the right ascension values. Valid options are 

796 "degrees", "arcmin", "arcsec", "hours" or "radians". 

797 decUnits : `str`, optional 

798 Unit of the declination values. Valid options are 

799 "degrees", "arcmin", "arcsec", "hours" or "radians". 

800 **treecorrKwargs 

801 Keyword arguments to be passed to `treecorr.GGCorrelation`. 

802 

803 Returns 

804 ------- 

805 xy : `treecorr.GGCorrelation` 

806 A `treecorr.GGCorrelation` object containing the correlation 

807 function. 

808 """ 

809 xy = treecorr.GGCorrelation(**treecorrKwargs) 

810 catA = treecorr.Catalog(ra=ra, dec=dec, g1=g1a, g2=g2a, ra_units=raUnits, dec_units=decUnits) 

811 if g1b is None or g2b is None: 

812 # Calculate the auto-correlation 

813 xy.process(catA) 

814 else: 

815 catB = treecorr.Catalog(ra=ra, dec=dec, g1=g1b, g2=g2b, ra_units=raUnits, dec_units=decUnits) 

816 # Calculate the cross-correlation 

817 xy.process(catA, catB) 

818 

819 return xy 

820 

821 

822class ColorDiff(MultiColumnAction): 

823 """Calculate the difference between two colors; 

824 each color is derived from two flux columns. 

825 

826 The color difference is computed as (color1 - color2) with: 

827 

828 color1 = color1_mag1 - color1_mag2 

829 color2 = color2_mag1 - color2_mag2 

830 

831 where color1_mag1 is the magnitude associated with color1_flux1, etc. 

832 

833 Parameters 

834 ---------- 

835 df : `pandas.core.frame.DataFrame` 

836 The catalog to calculate the color difference from. 

837 

838 Returns 

839 ------- 

840 The color difference in millimags. 

841 

842 Notes 

843 ----- 

844 The flux columns need to be in units that can be converted 

845 to janskies. This action doesn't have any calibration 

846 information and assumes that the fluxes are already 

847 calibrated. 

848 """ 

849 color1_flux1 = Field(doc="Column for flux1 to determine color1", 

850 dtype=str) 

851 color1_flux1_units = Field(doc="Units for color1_flux1", 

852 dtype=str, 

853 default="nanojansky") 

854 color1_flux2 = Field(doc="Column for flux2 to determine color1", 

855 dtype=str) 

856 color1_flux2_units = Field(doc="Units for color1_flux2", 

857 dtype=str, 

858 default="nanojansky") 

859 color2_flux1 = Field(doc="Column for flux1 to determine color2", 

860 dtype=str) 

861 color2_flux1_units = Field(doc="Units for color2_flux1", 

862 dtype=str, 

863 default="nanojansky") 

864 color2_flux2 = Field(doc="Column for flux2 to determine color2", 

865 dtype=str) 

866 color2_flux2_units = Field(doc="Units for color2_flux2", 

867 dtype=str, 

868 default="nanojansky") 

869 return_millimags = Field(doc="Use millimags or not?", 

870 dtype=bool, 

871 default=True) 

872 

873 @property 

874 def columns(self): 

875 return (self.color1_flux1, 

876 self.color1_flux2, 

877 self.color2_flux1, 

878 self.color2_flux2) 

879 

880 def __call__(self, df): 

881 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units) 

882 color1_mag1 = color1_flux1.to(u.ABmag).value 

883 

884 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units) 

885 color1_mag2 = color1_flux2.to(u.ABmag).value 

886 

887 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units) 

888 color2_mag1 = color2_flux1.to(u.ABmag).value 

889 

890 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units) 

891 color2_mag2 = color2_flux2.to(u.ABmag).value 

892 

893 color1 = color1_mag1 - color1_mag2 

894 color2 = color2_mag1 - color2_mag2 

895 

896 color_diff = color1 - color2 

897 

898 if self.return_millimags: 

899 color_diff = color_diff*1000 

900 

901 return color_diff 

902 

903 

904class ColorDiffPull(ColorDiff): 

905 """Calculate the difference between two colors, scaled by the color error; 

906 Each color is derived from two flux columns. 

907 

908 The color difference is computed as (color1 - color2) with: 

909 

910 color1 = color1_mag1 - color1_mag2 

911 color2 = color2_mag1 - color2_mag2 

912 

913 where color1_mag1 is the magnitude associated with color1_flux1, etc. 

914 

915 The color difference (color1 - color2) is then scaled by the error on 

916 the color as computed from color1_flux1_err, color1_flux2_err, 

917 color2_flux1_err, color2_flux2_err. The errors on color2 may be omitted 

918 if the comparison is between an "observed" catalog and a "truth" catalog. 

919 

920 Parameters 

921 ---------- 

922 df : `pandas.core.frame.DataFrame` 

923 The catalog to calculate the color difference from. 

924 

925 Returns 

926 ------- 

927 The color difference scaled by the error. 

928 

929 Notes 

930 ----- 

931 The flux columns need to be in units that can be converted 

932 to janskies. This action doesn't have any calibration 

933 information and assumes that the fluxes are already 

934 calibrated. 

935 """ 

936 color1_flux1_err = Field(doc="Error column for flux1 for color1", 

937 dtype=str, 

938 default="") 

939 color1_flux2_err = Field(doc="Error column for flux2 for color1", 

940 dtype=str, 

941 default="") 

942 color2_flux1_err = Field(doc="Error column for flux1 for color2", 

943 dtype=str, 

944 default="") 

945 color2_flux2_err = Field(doc="Error column for flux2 for color2", 

946 dtype=str, 

947 default="") 

948 

949 def validate(self): 

950 super().validate() 

951 

952 color1_errors = False 

953 color2_errors = False 

954 

955 if self.color1_flux1_err and self.color1_flux2_err: 

956 color1_errors = True 

957 elif ((self.color1_flux1_err and not self.color1_flux2_err) 

958 or (not self.color1_flux1_err and self.color1_flux2_err)): 

959 msg = "Must set both color1_flux1_err and color1_flux2_err if either is set." 

960 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg) 

961 if self.color2_flux1_err and self.color2_flux2_err: 

962 color2_errors = True 

963 elif ((self.color2_flux1_err and not self.color2_flux2_err) 

964 or (not self.color2_flux1_err and self.color2_flux2_err)): 

965 msg = "Must set both color2_flux1_err and color2_flux2_err if either is set." 

966 raise FieldValidationError(self.__class__.color2_flux1_err, self, msg) 

967 

968 if not color1_errors and not color2_errors: 

969 msg = "Must configure flux errors for at least color1 or color2." 

970 raise FieldValidationError(self.__class__.color1_flux1_err, self, msg) 

971 

972 @property 

973 def columns(self): 

974 columns = (self.color1_flux1, 

975 self.color1_flux2, 

976 self.color2_flux1, 

977 self.color2_flux2) 

978 

979 if self.color1_flux1_err: 

980 # Config validation ensures if one is set, both are set. 

981 columns = columns + (self.color1_flux1_err, 

982 self.color1_flux2_err) 

983 

984 if self.color2_flux1_err: 

985 # Config validation ensures if one is set, both are set. 

986 columns = columns + (self.color2_flux1_err, 

987 self.color2_flux2_err) 

988 

989 return columns 

990 

991 def __call__(self, df): 

992 k = 2.5/np.log(10.) 

993 

994 color1_flux1 = df[self.color1_flux1].values*u.Unit(self.color1_flux1_units) 

995 color1_mag1 = color1_flux1.to(u.ABmag).value 

996 if self.color1_flux1_err: 

997 color1_mag1_err = k*df[self.color1_flux1_err].values/df[self.color1_flux1].values 

998 else: 

999 color1_mag1_err = 0.0 

1000 

1001 color1_flux2 = df[self.color1_flux2].values*u.Unit(self.color1_flux2_units) 

1002 color1_mag2 = color1_flux2.to(u.ABmag).value 

1003 if self.color1_flux2_err: 

1004 color1_mag2_err = k*df[self.color1_flux2_err].values/df[self.color1_flux2].values 

1005 else: 

1006 color1_mag2_err = 0.0 

1007 

1008 color2_flux1 = df[self.color2_flux1].values*u.Unit(self.color2_flux1_units) 

1009 color2_mag1 = color2_flux1.to(u.ABmag).value 

1010 if self.color2_flux1_err: 

1011 color2_mag1_err = k*df[self.color2_flux1_err].values/df[self.color2_flux1].values 

1012 else: 

1013 color2_mag1_err = 0.0 

1014 

1015 color2_flux2 = df[self.color2_flux2].values*u.Unit(self.color2_flux2_units) 

1016 color2_mag2 = color2_flux2.to(u.ABmag).value 

1017 if self.color2_flux2_err: 

1018 color2_mag2_err = k*df[self.color2_flux2_err].values/df[self.color2_flux2].values 

1019 else: 

1020 color2_mag2_err = 0.0 

1021 

1022 color1 = color1_mag1 - color1_mag2 

1023 err1_sq = color1_mag1_err**2. + color1_mag2_err**2. 

1024 color2 = color2_mag1 - color2_mag2 

1025 err2_sq = color2_mag1_err**2. + color2_mag2_err**2. 

1026 

1027 color_diff = color1 - color2 

1028 

1029 pull = color_diff/np.sqrt(err1_sq + err2_sq) 

1030 

1031 return pull