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

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

235 statements  

1__all__ = ["SNCalculator", "KronFluxDivPsfFlux", "MagDiff", "ColorDiff", "ColorDiffPull", 

2 "ExtinctionCorrectedMagDiff", "CalcE", "CalcEDiff", "CalcE1", "CalcE2", "CalcShapeSize"] 

3 

4from lsst.pipe.tasks.configurableActions import ConfigurableActionField 

5from lsst.pipe.tasks.dataFrameActions import DataFrameAction, DivideColumns, MultiColumnAction 

6from lsst.pex.config import ChoiceField, DictField, Field 

7from astropy import units as u 

8import numpy as np 

9import logging 

10 

11_LOG = logging.getLogger(__name__) 

12 

13 

14class SNCalculator(DivideColumns): 

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

16 

17 def setDefaults(self): 

18 super().setDefaults() 

19 self.colA.column = "i_psfFlux" 

20 self.colB.column = "i_psfFluxErr" 

21 

22 

23class KronFluxDivPsfFlux(DivideColumns): 

24 """Divide the Kron instFlux by the PSF instFlux""" 

25 

26 def setDefaults(self): 

27 super().setDefaults() 

28 self.colA.column = "i_kronFlux" 

29 self.colB.column = "i_psfFlux" 

30 

31 

32class MagDiff(MultiColumnAction): 

33 """Calculate the difference between two magnitudes; 

34 each magnitude is derived from a flux column. 

35 

36 Parameters 

37 ---------- 

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

39 The catalog to calculate the magnitude difference from. 

40 

41 Returns 

42 ------- 

43 The magnitude difference in milli mags. 

44 

45 Notes 

46 ----- 

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

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

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

50 information and assumes that the fluxes are already 

51 calibrated. 

52 """ 

53 

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

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

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

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

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

59 

60 @property 

61 def columns(self): 

62 return (self.col1, self.col2) 

63 

64 def __call__(self, df): 

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

66 mag1 = flux1.to(u.ABmag) 

67 

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

69 mag2 = flux2.to(u.ABmag) 

70 

71 magDiff = mag1 - mag2 

72 

73 if self.returnMillimags: 

74 magDiff = magDiff.to(u.mmag) 

75 

76 return magDiff 

77 

78 

79class ExtinctionCorrectedMagDiff(DataFrameAction): 

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

81 

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

83 per the naming convention in the Object Table: 

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

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

86 config parameters. 

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

88 """ 

89 

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

91 default=MagDiff, dtype=DataFrameAction) 

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

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

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

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

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

97 extinctionCoeffs = DictField( 

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

99 "Key must be the band", 

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

101 default=None) 

102 

103 @property 

104 def columns(self): 

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

106 

107 def __call__(self, df): 

108 diff = self.magDiff(df) 

109 if not self.extinctionCoeffs: 

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

111 return diff 

112 

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

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

115 

116 for band in (col1Band, col1Band): 

117 if band not in self.extinctionCoeffs: 

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

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

120 return diff 

121 

122 av1 = self.extinctionCoeffs[col1Band] 

123 av2 = self.extinctionCoeffs[col2Band] 

124 

125 ebv = df[self.ebvCol].values 

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

127 

128 if self.magDiff.returnMillimags: 

129 correction = correction.to(u.mmag) 

130 

131 return diff - correction 

132 

133 

134class CalcE(MultiColumnAction): 

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

136 

137 The complex ellipticity is typically defined as 

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

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

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

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

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

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

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

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

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

147 

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

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

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

151 artifacts. 

152 

153 Reference 

154 --------- 

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

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

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

158 

159 Notes 

160 ----- 

161 

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

163 of the sources. 

164 

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

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

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

168 

169 See Also 

170 -------- 

171 CalcE1 

172 CalcE2 

173 """ 

174 

175 colXx = Field( 

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

177 dtype=str, 

178 default="ixx", 

179 ) 

180 

181 colYy = Field( 

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

183 dtype=str, 

184 default="iyy", 

185 ) 

186 

187 colXy = Field( 

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

189 dtype=str, 

190 default="ixy", 

191 ) 

192 

193 ellipticityType = ChoiceField( 

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

195 dtype=str, 

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

197 "(Ixx + Iyy)" 

198 ), 

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

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

201 ), 

202 }, 

203 default="chi", 

204 ) 

205 

206 halvePhaseAngle = Field( 

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

208 dtype=bool, 

209 default=False, 

210 ) 

211 

212 @property 

213 def columns(self): 

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

215 

216 def __call__(self, df): 

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

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

219 

220 if self.ellipticityType == "epsilon": 

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

222 

223 e /= denom 

224 

225 if self.halvePhaseAngle: 

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

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

228 # instead of the more expensive trig calls. 

229 e *= np.abs(e) 

230 return np.sqrt(e) 

231 else: 

232 return e 

233 

234 

235class CalcEDiff(DataFrameAction): 

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

237 

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

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

240 

241 See Also 

242 -------- 

243 CalcE 

244 

245 Notes 

246 ----- 

247 

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

249 of the sources. 

250 

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

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

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

254 """ 

255 colA = ConfigurableActionField( 

256 doc="Ellipticity to subtract from", 

257 dtype=MultiColumnAction, 

258 default=CalcE, 

259 ) 

260 

261 colB = ConfigurableActionField( 

262 doc="Ellipticity to subtract", 

263 dtype=MultiColumnAction, 

264 default=CalcE, 

265 ) 

266 

267 halvePhaseAngle = Field( 

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

269 dtype=bool, 

270 default=False, 

271 ) 

272 

273 @property 

274 def columns(self): 

275 yield from self.colA.columns 

276 yield from self.colB.columns 

277 

278 def __call__(self, df): 

279 eMeas = self.colA(df) 

280 ePSF = self.colB(df) 

281 eDiff = eMeas - ePSF 

282 if self.halvePhaseAngle: 

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

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

285 # instead of the more expensive trig calls. 

286 eDiff *= np.abs(eDiff) 

287 return np.sqrt(eDiff) 

288 else: 

289 return eDiff 

290 

291 

292class CalcE1(MultiColumnAction): 

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

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

295 

296 See Also 

297 -------- 

298 CalcE 

299 CalcE2 

300 

301 Note 

302 ---- 

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

304 of the sources. 

305 """ 

306 

307 colXx = Field( 

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

309 dtype=str, 

310 default="ixx", 

311 ) 

312 

313 colYy = Field( 

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

315 dtype=str, 

316 default="iyy", 

317 ) 

318 

319 colXy = Field( 

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

321 dtype=str, 

322 default="ixy", 

323 optional=True, 

324 ) 

325 

326 ellipticityType = ChoiceField( 

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

328 dtype=str, 

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

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

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

332 ), 

333 }, 

334 default="chi", 

335 ) 

336 

337 @property 

338 def columns(self): 

339 if self.ellipticityType == "chi": 

340 return (self.colXx, self.colYy) 

341 else: 

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

343 

344 def __call__(self, df): 

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

346 if self.ellipticityType == "epsilon": 

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

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

349 

350 return e1 

351 

352 def validate(self): 

353 super().validate() 

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

355 raise ValueError("colXy is required for epsilon-type shear ellipticity") 

356 

357 

358class CalcE2(MultiColumnAction): 

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

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

361 

362 See Also 

363 -------- 

364 CalcE 

365 CalcE1 

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 ) 

390 

391 ellipticityType = ChoiceField( 

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

393 dtype=str, 

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

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

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

397 ), 

398 }, 

399 default="chi", 

400 ) 

401 

402 @property 

403 def columns(self): 

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

405 

406 def __call__(self, df): 

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

408 if self.ellipticityType == "epsilon": 

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

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

411 return e2 

412 

413 

414class CalcShapeSize(MultiColumnAction): 

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

416 

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

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

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

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

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

422 

423 Since lensing preserves surface brightness, the determinant radius relates 

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

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

426 

427 Note 

428 ---- 

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

430 of the sources. 

431 """ 

432 

433 colXx = Field( 

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

435 dtype=str, 

436 default="ixx", 

437 ) 

438 

439 colYy = Field( 

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

441 dtype=str, 

442 default="iyy", 

443 ) 

444 

445 colXy = Field( 

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

447 dtype=str, 

448 default="ixy", 

449 optional=True, 

450 ) 

451 

452 sizeType = ChoiceField( 

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

454 dtype=str, 

455 default="determinant", 

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

457 "determinant": "determinant radius", 

458 }, 

459 ) 

460 

461 @property 

462 def columns(self): 

463 if self.sizeType == "trace": 

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

465 else: 

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

467 

468 def __call__(self, df): 

469 if self.sizeType == "trace": 

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

471 else: 

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

473 

474 return size 

475 

476 def validate(self): 

477 super().validate() 

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

479 raise ValueError("colXy is required for determinant-type size") 

480 

481 

482class ColorDiff(MultiColumnAction): 

483 """Calculate the difference between two colors; 

484 each color is derived from two flux columns. 

485 

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

487 

488 color1 = color1_mag1 - color1_mag2 

489 color2 = color2_mag1 - color2_mag2 

490 

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

492 

493 Parameters 

494 ---------- 

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

496 The catalog to calculate the color difference from. 

497 

498 Returns 

499 ------- 

500 The color difference in millimags. 

501 

502 Notes 

503 ----- 

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

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

506 information and assumes that the fluxes are already 

507 calibrated. 

508 """ 

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

510 dtype=str) 

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

512 dtype=str, 

513 default="nanojansky") 

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

515 dtype=str) 

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

517 dtype=str, 

518 default="nanojansky") 

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

520 dtype=str) 

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

522 dtype=str, 

523 default="nanojansky") 

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

525 dtype=str) 

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

527 dtype=str, 

528 default="nanojansky") 

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

530 dtype=bool, 

531 default=True) 

532 

533 @property 

534 def columns(self): 

535 return (self.color1_flux1, 

536 self.color1_flux2, 

537 self.color2_flux1, 

538 self.color2_flux2) 

539 

540 def __call__(self, df): 

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

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

543 

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

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

546 

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

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

549 

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

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

552 

553 color1 = color1_mag1 - color1_mag2 

554 color2 = color2_mag1 - color2_mag2 

555 

556 color_diff = color1 - color2 

557 

558 if self.return_millimags: 

559 color_diff = color_diff*1000 

560 

561 return color_diff 

562 

563 

564class ColorDiffPull(ColorDiff): 

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

566 Each color is derived from two flux columns. 

567 

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

569 

570 color1 = color1_mag1 - color1_mag2 

571 color2 = color2_mag1 - color2_mag2 

572 

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

574 

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

576 the color as computed from color1_flux1_err, color1_flux2_err, 

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

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

579 

580 Parameters 

581 ---------- 

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

583 The catalog to calculate the color difference from. 

584 

585 Returns 

586 ------- 

587 The color difference scaled by the error. 

588 

589 Notes 

590 ----- 

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

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

593 information and assumes that the fluxes are already 

594 calibrated. 

595 """ 

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

597 dtype=str, 

598 default="") 

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

600 dtype=str, 

601 default="") 

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

603 dtype=str, 

604 default="") 

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

606 dtype=str, 

607 default="") 

608 

609 def validate(self): 

610 super().validate() 

611 

612 color1_errors = False 

613 color2_errors = False 

614 

615 if self.color1_flux1_err and self.color1_flux2_err: 

616 color1_errors = True 

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

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

619 raise ValueError("Must set both color1_flux1_err and color1_flux2_err if either is set.") 

620 if self.color2_flux1_err and self.color2_flux2_err: 

621 color2_errors = True 

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

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

624 raise ValueError("Must set both color2_flux1_err and color2_flux2_err if either is set.") 

625 

626 if not color1_errors and not color2_errors: 

627 raise ValueError("Must configure flux errors for at least color1 or color2.") 

628 

629 @property 

630 def columns(self): 

631 columns = (self.color1_flux1, 

632 self.color1_flux2, 

633 self.color2_flux1, 

634 self.color2_flux2) 

635 

636 if self.color1_flux1_err: 

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

638 columns = columns + (self.color1_flux1_err, 

639 self.color1_flux2_err) 

640 

641 if self.color2_flux1_err: 

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

643 columns = columns + (self.color2_flux1_err, 

644 self.color2_flux2_err) 

645 

646 return columns 

647 

648 def __call__(self, df): 

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

650 

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

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

653 if self.color1_flux1_err: 

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

655 else: 

656 color1_mag1_err = 0.0 

657 

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

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

660 if self.color1_flux2_err: 

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

662 else: 

663 color1_mag2_err = 0.0 

664 

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

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

667 if self.color2_flux1_err: 

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

669 else: 

670 color2_mag1_err = 0.0 

671 

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

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

674 if self.color2_flux2_err: 

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

676 else: 

677 color2_mag2_err = 0.0 

678 

679 color1 = color1_mag1 - color1_mag2 

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

681 color2 = color2_mag1 - color2_mag2 

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

683 

684 color_diff = color1 - color2 

685 

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

687 

688 return pull