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

339 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-15 02:47 -0800

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", "SnDiffCalculator", "SnPercentDiffCalculator", "KronFluxDivPsfFlux", 

23 "MagDiff", "ColorDiff", "ColorDiffPull", "ExtinctionCorrectedMagDiff", 

24 "CalcE", "CalcE1", "CalcE2", "CalcEDiff", "CalcShapeSize", "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, DiffOfDividedColumns, 

37 PercentDiffOfDividedColumns) 

38 

39from ._treecorrConfig import BinnedCorr2Config 

40 

41_LOG = logging.getLogger(__name__) 

42 

43 

44class SNCalculator(DivideColumns): 

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

46 

47 def setDefaults(self): 

48 super().setDefaults() 

49 self.colA.column = "i_psfFlux" 

50 self.colB.column = "i_psfFluxErr" 

51 

52 

53class SnDiffCalculator(DiffOfDividedColumns): 

54 """Calculate the signal to noise difference between two measurements. 

55 

56 By default the i band flux is used and the S/N comparison is between PSF 

57 and CModel fluxes. 

58 """ 

59 

60 def setDefaults(self): 

61 super().setDefaults() 

62 self.colA1.column = "i_psfFlux" 

63 self.colB1.column = "i_psfFluxErr" 

64 self.colA2.column = "i_cModelFlux" 

65 self.colB2.column = "i_cModelFluxErr" 

66 

67 

68class SnPercentDiffCalculator(PercentDiffOfDividedColumns): 

69 """Calculate the signal to noise %difference between two measurements. 

70 

71 By default the i band flux is used and the S/N comparison is between PSF 

72 and CModel fluxes. 

73 """ 

74 

75 def setDefaults(self): 

76 super().setDefaults() 

77 self.colA1.column = "i_psfFlux" 

78 self.colB1.column = "i_psfFluxErr" 

79 self.colA2.column = "i_cModelFlux" 

80 self.colB2.column = "i_cModelFluxErr" 

81 

82 

83class KronFluxDivPsfFlux(DivideColumns): 

84 """Divide the Kron instFlux by the PSF instFlux""" 

85 

86 def setDefaults(self): 

87 super().setDefaults() 

88 self.colA.column = "i_kronFlux" 

89 self.colB.column = "i_psfFlux" 

90 

91 

92class MagDiff(MultiColumnAction): 

93 """Calculate the difference between two magnitudes; 

94 each magnitude is derived from a flux column. 

95 

96 Parameters 

97 ---------- 

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

99 The catalog to calculate the magnitude difference from. 

100 

101 Returns 

102 ------- 

103 The magnitude difference in milli mags. 

104 

105 Notes 

106 ----- 

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

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

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

110 information and assumes that the fluxes are already 

111 calibrated. 

112 """ 

113 

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

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

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

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

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

119 

120 @property 

121 def columns(self): 

122 return (self.col1, self.col2) 

123 

124 def __call__(self, df): 

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

126 mag1 = flux1.to(u.ABmag) 

127 

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

129 mag2 = flux2.to(u.ABmag) 

130 

131 magDiff = mag1 - mag2 

132 

133 if self.returnMillimags: 

134 magDiff = magDiff.to(u.mmag) 

135 

136 return magDiff 

137 

138 

139class ExtinctionCorrectedMagDiff(DataFrameAction): 

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

141 

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

143 per the naming convention in the Object Table: 

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

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

146 config parameters. 

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

148 """ 

149 

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

151 default=MagDiff, dtype=DataFrameAction) 

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

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

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

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

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

157 extinctionCoeffs = DictField( 

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

159 "Key must be the band", 

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

161 default=None) 

162 

163 @property 

164 def columns(self): 

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

166 

167 def __call__(self, df): 

168 diff = self.magDiff(df) 

169 if not self.extinctionCoeffs: 

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

171 return diff 

172 

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

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

175 

176 for band in (col1Band, col1Band): 

177 if band not in self.extinctionCoeffs: 

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

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

180 return diff 

181 

182 av1 = self.extinctionCoeffs[col1Band] 

183 av2 = self.extinctionCoeffs[col2Band] 

184 

185 ebv = df[self.ebvCol].values 

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

187 

188 if self.magDiff.returnMillimags: 

189 correction = correction.to(u.mmag) 

190 

191 return diff - correction 

192 

193 

194class CalcE(MultiColumnAction): 

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

196 

197 The complex ellipticity is typically defined as 

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

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

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

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

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

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

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

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

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

207 

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

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

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

211 artifacts. 

212 

213 References 

214 ---------- 

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

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

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

218 

219 Notes 

220 ----- 

221 

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

223 of the sources. 

224 

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

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

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

228 

229 See Also 

230 -------- 

231 CalcE1 

232 CalcE2 

233 """ 

234 

235 colXx = Field( 

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

237 dtype=str, 

238 default="ixx", 

239 ) 

240 

241 colYy = Field( 

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

243 dtype=str, 

244 default="iyy", 

245 ) 

246 

247 colXy = Field( 

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

249 dtype=str, 

250 default="ixy", 

251 ) 

252 

253 ellipticityType = ChoiceField( 

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

255 dtype=str, 

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

257 "(Ixx + Iyy)" 

258 ), 

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

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

261 ), 

262 }, 

263 default="chi", 

264 ) 

265 

266 halvePhaseAngle = Field( 

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

268 dtype=bool, 

269 default=False, 

270 ) 

271 

272 @property 

273 def columns(self): 

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

275 

276 def __call__(self, df): 

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

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

279 

280 if self.ellipticityType == "epsilon": 

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

282 

283 e /= denom 

284 

285 if self.halvePhaseAngle: 

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

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

288 # instead of the more expensive trig calls. 

289 e *= np.abs(e) 

290 return np.sqrt(e) 

291 else: 

292 return e 

293 

294 

295class CalcEDiff(DataFrameAction): 

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

297 

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

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

300 

301 See Also 

302 -------- 

303 CalcE 

304 

305 Notes 

306 ----- 

307 

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

309 of the sources. 

310 

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

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

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

314 """ 

315 colA = ConfigurableActionField( 

316 doc="Ellipticity to subtract from", 

317 dtype=MultiColumnAction, 

318 default=CalcE, 

319 ) 

320 

321 colB = ConfigurableActionField( 

322 doc="Ellipticity to subtract", 

323 dtype=MultiColumnAction, 

324 default=CalcE, 

325 ) 

326 

327 halvePhaseAngle = Field( 

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

329 dtype=bool, 

330 default=False, 

331 ) 

332 

333 @property 

334 def columns(self): 

335 yield from self.colA.columns 

336 yield from self.colB.columns 

337 

338 def validate(self): 

339 super().validate() 

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

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

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

343 

344 def __call__(self, df): 

345 eMeas = self.colA(df) 

346 ePSF = self.colB(df) 

347 eDiff = eMeas - ePSF 

348 if self.halvePhaseAngle: 

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

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

351 # instead of the more expensive trig calls. 

352 eDiff *= np.abs(eDiff) 

353 return np.sqrt(eDiff) 

354 else: 

355 return eDiff 

356 

357 

358class CalcE1(MultiColumnAction): 

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

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

361 

362 See Also 

363 -------- 

364 CalcE 

365 CalcE2 

366 

367 Note 

368 ---- 

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

370 of the sources. 

371 """ 

372 

373 colXx = Field( 

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

375 dtype=str, 

376 default="ixx", 

377 ) 

378 

379 colYy = Field( 

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

381 dtype=str, 

382 default="iyy", 

383 ) 

384 

385 colXy = Field( 

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

387 dtype=str, 

388 default="ixy", 

389 optional=True, 

390 ) 

391 

392 ellipticityType = ChoiceField( 

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

394 dtype=str, 

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

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

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

398 ), 

399 }, 

400 default="chi", 

401 ) 

402 

403 @property 

404 def columns(self): 

405 if self.ellipticityType == "chi": 

406 return (self.colXx, self.colYy) 

407 else: 

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

409 

410 def __call__(self, df): 

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

412 if self.ellipticityType == "epsilon": 

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

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

415 

416 return e1 

417 

418 def validate(self): 

419 super().validate() 

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

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

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

423 

424 

425class CalcE2(MultiColumnAction): 

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

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

428 

429 See Also 

430 -------- 

431 CalcE 

432 CalcE1 

433 

434 Note 

435 ---- 

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

437 of the sources. 

438 """ 

439 

440 colXx = Field( 

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

442 dtype=str, 

443 default="ixx", 

444 ) 

445 

446 colYy = Field( 

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

448 dtype=str, 

449 default="iyy", 

450 ) 

451 

452 colXy = Field( 

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

454 dtype=str, 

455 default="ixy", 

456 ) 

457 

458 ellipticityType = ChoiceField( 

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

460 dtype=str, 

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

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

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

464 ), 

465 }, 

466 default="chi", 

467 ) 

468 

469 @property 

470 def columns(self): 

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

472 

473 def __call__(self, df): 

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

475 if self.ellipticityType == "epsilon": 

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

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

478 return e2 

479 

480 

481class CalcShapeSize(MultiColumnAction): 

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

483 

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

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

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

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

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

489 

490 Since lensing preserves surface brightness, the determinant radius relates 

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

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

493 

494 Note 

495 ---- 

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

497 of the sources. 

498 """ 

499 

500 colXx = Field( 

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

502 dtype=str, 

503 default="ixx", 

504 ) 

505 

506 colYy = Field( 

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

508 dtype=str, 

509 default="iyy", 

510 ) 

511 

512 colXy = Field( 

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

514 dtype=str, 

515 default="ixy", 

516 optional=True, 

517 ) 

518 

519 sizeType = ChoiceField( 

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

521 dtype=str, 

522 default="determinant", 

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

524 "determinant": "determinant radius", 

525 }, 

526 ) 

527 

528 @property 

529 def columns(self): 

530 if self.sizeType == "trace": 

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

532 else: 

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

534 

535 def __call__(self, df): 

536 if self.sizeType == "trace": 

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

538 else: 

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

540 

541 return size 

542 

543 def validate(self): 

544 super().validate() 

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

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

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

548 

549 

550class CalcRhoStatistics(DataFrameAction): 

551 r"""Calculate Rho statistics. 

552 

553 Rho statistics refer to a collection of correlation functions involving 

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

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

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

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

558 

559 Notes 

560 ----- 

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

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

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

564 

565 .. math:: 

566 

567 \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 

568 

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

570 

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

572 

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

574 

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

576 

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

578 

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

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

581 

582 References 

583 ---------- 

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

585 The DES Science Verification weak lensing shear catalogues. 

586 MNRAS, 460, 2245–2281. 

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

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

589 """ 

590 

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

592 

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

594 

595 colXx = Field( 

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

597 dtype=str, 

598 default="ixx" 

599 ) 

600 

601 colYy = Field( 

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

603 dtype=str, 

604 default="iyy" 

605 ) 

606 

607 colXy = Field( 

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

609 dtype=str, 

610 default="ixy" 

611 ) 

612 

613 colPsfXx = Field( 

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

615 dtype=str, 

616 default="ixxPSF" 

617 ) 

618 

619 colPsfYy = Field( 

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

621 dtype=str, 

622 default="iyyPSF" 

623 ) 

624 

625 colPsfXy = Field( 

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

627 dtype=str, 

628 default="ixyPSF" 

629 ) 

630 

631 ellipticityType = ChoiceField( 

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

633 dtype=str, 

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

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

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

637 ), 

638 }, 

639 default="chi", 

640 ) 

641 

642 sizeType = ChoiceField( 

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

644 dtype=str, 

645 default="trace", 

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

647 "determinant": "determinant radius", 

648 }, 

649 ) 

650 

651 treecorr = ConfigField( 

652 doc="TreeCorr configuration", 

653 dtype=BinnedCorr2Config, 

654 ) 

655 

656 def setDefaults(self): 

657 super().setDefaults() 

658 self.treecorr = BinnedCorr2Config() 

659 self.treecorr.sep_units = "arcmin" 

660 self.treecorr.metric = "Arc" 

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

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

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

664 

665 @property 

666 def columns(self): 

667 return ( 

668 self.colXx, 

669 self.colYy, 

670 self.colXy, 

671 self.colPsfXx, 

672 self.colPsfYy, 

673 self.colPsfXy, 

674 self.colRa.column, 

675 self.colDec.column, 

676 ) 

677 

678 def __call__(self, df): 

679 # Create instances of various actions. 

680 calcEMeas = CalcE( 

681 colXx=self.colXx, 

682 colYy=self.colYy, 

683 colXy=self.colXy, 

684 ellipticityType=self.ellipticityType, 

685 ) 

686 calcEpsf = CalcE( 

687 colXx=self.colPsfXx, 

688 colYy=self.colPsfYy, 

689 colXy=self.colPsfXy, 

690 ellipticityType=self.ellipticityType, 

691 ) 

692 

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

694 

695 calcSizeResiduals = FractionalDifferenceColumns( 

696 colA=CalcShapeSize( 

697 colXx=self.colXx, 

698 colYy=self.colYy, 

699 colXy=self.colXy, 

700 sizeType=self.sizeType, 

701 ), 

702 colB=CalcShapeSize( 

703 colXx=self.colPsfXx, 

704 colYy=self.colPsfYy, 

705 colXy=self.colPsfXy, 

706 sizeType=self.sizeType, 

707 ), 

708 ) 

709 

710 # Call the actions on the dataframe. 

711 eMEAS = calcEMeas(df) 

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

713 eRes = calcEDiff(df) 

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

715 sizeRes = calcSizeResiduals(df) 

716 

717 # Scale the sizeRes by ellipticities 

718 e1SizeRes = e1*sizeRes 

719 e2SizeRes = e2*sizeRes 

720 

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

722 # Rho statistics. 

723 args = { 

724 0: (sizeRes, None), 

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

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

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

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

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

730 } 

731 

732 ra = self.colRa(df) 

733 dec = self.colDec(df) 

734 

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

736 if self.colRa.inRadians: 

737 ra *= 180.0/np.pi 

738 if self.colDec.inRadians: 

739 dec *= 180.0/np.pi 

740 

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

742 treecorrKwargs = self.treecorr.toDict() 

743 

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

745 rhoStats = { 

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

747 for rhoIndex in range(1, 6) 

748 } 

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

750 

751 return rhoStats 

752 

753 @classmethod 

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

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

756 

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

758 (scalar) fields, usually fractional size residuals. 

759 

760 Parameters 

761 ---------- 

762 ra : `numpy.array` 

763 The right ascension values of entries in the catalog. 

764 dec : `numpy.array` 

765 The declination values of entries in the catalog. 

766 k1 : `numpy.array` 

767 The primary scalar field. 

768 k2 : `numpy.array`, optional 

769 The secondary scalar field. 

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

771 raUnits : `str`, optional 

772 Unit of the right ascension values. Valid options are 

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

774 decUnits : `str`, optional 

775 Unit of the declination values. Valid options are 

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

777 **treecorrKwargs 

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

779 

780 Returns 

781 ------- 

782 xy : `treecorr.KKCorrelation` 

783 A `treecorr.KKCorrelation` object containing the correlation 

784 function. 

785 """ 

786 

787 xy = treecorr.KKCorrelation(**treecorrKwargs) 

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

789 dec_units=decUnits) 

790 if k2 is None: 

791 # Calculate the auto-correlation 

792 xy.process(catA) 

793 else: 

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

795 dec_units=decUnits) 

796 # Calculate the cross-correlation 

797 xy.process(catA, catB) 

798 

799 return xy 

800 

801 @classmethod 

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

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

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

805 

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

807 (shear-like) fields. 

808 

809 Parameters 

810 ---------- 

811 ra : `numpy.array` 

812 The right ascension values of entries in the catalog. 

813 dec : `numpy.array` 

814 The declination values of entries in the catalog. 

815 g1a : `numpy.array` 

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

817 g2a : `numpy.array` 

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

819 g1b : `numpy.array`, optional 

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

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

822 g2b : `numpy.array`, optional 

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

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

825 raUnits : `str`, optional 

826 Unit of the right ascension values. Valid options are 

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

828 decUnits : `str`, optional 

829 Unit of the declination values. Valid options are 

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

831 **treecorrKwargs 

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

833 

834 Returns 

835 ------- 

836 xy : `treecorr.GGCorrelation` 

837 A `treecorr.GGCorrelation` object containing the correlation 

838 function. 

839 """ 

840 xy = treecorr.GGCorrelation(**treecorrKwargs) 

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

842 if g1b is None or g2b is None: 

843 # Calculate the auto-correlation 

844 xy.process(catA) 

845 else: 

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

847 # Calculate the cross-correlation 

848 xy.process(catA, catB) 

849 

850 return xy 

851 

852 

853class ColorDiff(MultiColumnAction): 

854 """Calculate the difference between two colors; 

855 each color is derived from two flux columns. 

856 

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

858 

859 color1 = color1_mag1 - color1_mag2 

860 color2 = color2_mag1 - color2_mag2 

861 

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

863 

864 Parameters 

865 ---------- 

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

867 The catalog to calculate the color difference from. 

868 

869 Returns 

870 ------- 

871 The color difference in millimags. 

872 

873 Notes 

874 ----- 

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

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

877 information and assumes that the fluxes are already 

878 calibrated. 

879 """ 

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

881 dtype=str) 

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

883 dtype=str, 

884 default="nanojansky") 

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

886 dtype=str) 

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

888 dtype=str, 

889 default="nanojansky") 

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

891 dtype=str) 

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

893 dtype=str, 

894 default="nanojansky") 

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

896 dtype=str) 

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

898 dtype=str, 

899 default="nanojansky") 

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

901 dtype=bool, 

902 default=True) 

903 

904 @property 

905 def columns(self): 

906 return (self.color1_flux1, 

907 self.color1_flux2, 

908 self.color2_flux1, 

909 self.color2_flux2) 

910 

911 def __call__(self, df): 

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

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

914 

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

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

917 

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

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

920 

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

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

923 

924 color1 = color1_mag1 - color1_mag2 

925 color2 = color2_mag1 - color2_mag2 

926 

927 color_diff = color1 - color2 

928 

929 if self.return_millimags: 

930 color_diff = color_diff*1000 

931 

932 return color_diff 

933 

934 

935class ColorDiffPull(ColorDiff): 

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

937 Each color is derived from two flux columns. 

938 

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

940 

941 color1 = color1_mag1 - color1_mag2 

942 color2 = color2_mag1 - color2_mag2 

943 

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

945 

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

947 the color as computed from color1_flux1_err, color1_flux2_err, 

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

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

950 

951 Parameters 

952 ---------- 

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

954 The catalog to calculate the color difference from. 

955 

956 Returns 

957 ------- 

958 The color difference scaled by the error. 

959 

960 Notes 

961 ----- 

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

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

964 information and assumes that the fluxes are already 

965 calibrated. 

966 """ 

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

968 dtype=str, 

969 default="") 

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

971 dtype=str, 

972 default="") 

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

974 dtype=str, 

975 default="") 

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

977 dtype=str, 

978 default="") 

979 

980 def validate(self): 

981 super().validate() 

982 

983 color1_errors = False 

984 color2_errors = False 

985 

986 if self.color1_flux1_err and self.color1_flux2_err: 

987 color1_errors = True 

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

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

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

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

992 if self.color2_flux1_err and self.color2_flux2_err: 

993 color2_errors = True 

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

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

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

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

998 

999 if not color1_errors and not color2_errors: 

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

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

1002 

1003 @property 

1004 def columns(self): 

1005 columns = (self.color1_flux1, 

1006 self.color1_flux2, 

1007 self.color2_flux1, 

1008 self.color2_flux2) 

1009 

1010 if self.color1_flux1_err: 

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

1012 columns = columns + (self.color1_flux1_err, 

1013 self.color1_flux2_err) 

1014 

1015 if self.color2_flux1_err: 

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

1017 columns = columns + (self.color2_flux1_err, 

1018 self.color2_flux2_err) 

1019 

1020 return columns 

1021 

1022 def __call__(self, df): 

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

1024 

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

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

1027 if self.color1_flux1_err: 

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

1029 else: 

1030 color1_mag1_err = 0.0 

1031 

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

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

1034 if self.color1_flux2_err: 

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

1036 else: 

1037 color1_mag2_err = 0.0 

1038 

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

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

1041 if self.color2_flux1_err: 

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

1043 else: 

1044 color2_mag1_err = 0.0 

1045 

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

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

1048 if self.color2_flux2_err: 

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

1050 else: 

1051 color2_mag2_err = 0.0 

1052 

1053 color1 = color1_mag1 - color1_mag2 

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

1055 color2 = color2_mag1 - color2_mag2 

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

1057 

1058 color_diff = color1 - color2 

1059 

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

1061 

1062 return pull 

1063 

1064 

1065class AstromDiff(MultiColumnAction): 

1066 """Calculate the difference between two columns, assuming their units 

1067 are degrees, and convert the difference to arcseconds. 

1068 

1069 Parameters 

1070 ---------- 

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

1072 The catalog to calculate the position difference from. 

1073 

1074 Returns 

1075 ------- 

1076 The difference. 

1077 

1078 Notes 

1079 ----- 

1080 The columns need to be in units (specifiable in 

1081 the radecUnits1 and 2 config options) that can be converted 

1082 to arcseconds. This action doesn't have any calibration 

1083 information and assumes that the positions are already 

1084 calibrated. 

1085 """ 

1086 

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

1088 radecUnits1 = Field(doc="Units for col1", dtype=str, default="degree") 

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

1090 radecUnits2 = Field(doc="Units for col2", dtype=str, default="degree") 

1091 returnMilliArcsecs = Field(doc="Use marcseconds or not?", dtype=bool, default=True) 

1092 

1093 @property 

1094 def columns(self): 

1095 return (self.col1, self.col2) 

1096 

1097 def __call__(self, df): 

1098 angle1 = df[self.col1].values * u.Unit(self.radecUnits1) 

1099 

1100 angle2 = df[self.col2].values * u.Unit(self.radecUnits2) 

1101 

1102 angleDiff = angle1 - angle2 

1103 

1104 if self.returnMilliArcsecs: 

1105 angleDiffValue = angleDiff.to(u.arcsec) * 1000 

1106 else: 

1107 angleDiffValue = angleDiff.value 

1108 return angleDiffValue