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

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

207 statements  

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

2 "ExtinctionCorrectedMagDiff"] 

3 

4from lsst.pipe.tasks.configurableActions import ConfigurableActionField 

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

6from lsst.pex.config import Field, DictField 

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 This is a shape measurement used for doing QA on the ellipticity 

138 of the sources. 

139 

140 The complex ellipticity is typically defined as 

141 E = ((ixx - iyy) + 1j*(2*ixy))/(ixx + iyy) = |E|exp(i*2*theta). 

142 

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

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

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

146 """ 

147 

148 colXx = Field(doc="The column name to get the xx shape component from.", 

149 dtype=str, 

150 default="ixx") 

151 

152 colYy = Field(doc="The column name to get the yy shape component from.", 

153 dtype=str, 

154 default="iyy") 

155 

156 colXy = Field(doc="The column name to get the xy shape component from.", 

157 dtype=str, 

158 default="ixy") 

159 

160 halvePhaseAngle = Field(doc=("Divide the phase angle by 2? " 

161 "Suitable for quiver plots."), 

162 dtype=bool, 

163 default=False) 

164 

165 @property 

166 def columns(self): 

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

168 

169 def __call__(self, df): 

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

171 e /= (df[self.colXx] + df[self.colYy]) 

172 if self.halvePhaseAngle: 

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

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

175 # instead of the more expensive trig calls. 

176 e *= np.abs(e) 

177 return np.sqrt(e) 

178 else: 

179 return e 

180 

181 

182class CalcEDiff(DataFrameAction): 

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

184 

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

186 of the sources. 

187 

188 The complex ellipticity difference between E_A and E_B is efined as 

189 dE = |dE|exp(i*2*theta). 

190 

191 For plotting purposes we might want to plot |dE|*exp(i*theta). 

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

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

194 """ 

195 colA = ConfigurableActionField(doc="Ellipticity to subtract from", 

196 dtype=MultiColumnAction, 

197 default=CalcE) 

198 

199 colB = ConfigurableActionField(doc="Ellipticity to subtract", 

200 dtype=MultiColumnAction, 

201 default=CalcE) 

202 

203 halvePhaseAngle = Field(doc=("Divide the phase angle by 2? " 

204 "Suitable for quiver plots."), 

205 dtype=bool, 

206 default=False) 

207 

208 @property 

209 def columns(self): 

210 yield from self.colA.columns 

211 yield from self.colB.columns 

212 

213 def __call__(self, df): 

214 eMeas = self.colA(df) 

215 ePSF = self.colB(df) 

216 eDiff = eMeas - ePSF 

217 if self.halvePhaseAngle: 

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

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

220 # instead of the more expensive trig calls. 

221 eDiff *= np.abs(eDiff) 

222 return np.sqrt(eDiff) 

223 else: 

224 return eDiff 

225 

226 

227class CalcE1(MultiColumnAction): 

228 """Calculate E1: (ixx - iyy)/(ixx + iyy) 

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

230 of the sources.""" 

231 

232 colXx = Field(doc="The column name to get the xx shape component from.", 

233 dtype=str, 

234 default="ixx") 

235 

236 colYy = Field(doc="The column name to get the yy shape component from.", 

237 dtype=str, 

238 default="iyy") 

239 

240 @property 

241 def columns(self): 

242 return (self.colXx, self.colYy) 

243 

244 def __call__(self, df): 

245 e1 = (df[self.colXx] - df[self.colYy])/(df[self.colXx] + df[self.colYy]) 

246 

247 return e1 

248 

249 

250class CalcE2(MultiColumnAction): 

251 """Calculate E2: 2ixy/(ixx+iyy) 

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

253 of the sources.""" 

254 

255 colXx = Field(doc="The column name to get the xx shape component from.", 

256 dtype=str, 

257 default="ixx") 

258 

259 colYy = Field(doc="The column name to get the yy shape component from.", 

260 dtype=str, 

261 default="iyy") 

262 

263 colXy = Field(doc="The column name to get the xy shape component from.", 

264 dtype=str, 

265 default="ixy") 

266 

267 @property 

268 def columns(self): 

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

270 

271 def __call__(self, df): 

272 e2 = 2*df[self.colXy]/(df[self.colXx] + df[self.colYy]) 

273 return e2 

274 

275 

276class CalcShapeSize(MultiColumnAction): 

277 """Calculate a size: (ixx*iyy - ixy**2)**0.25 

278 

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

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

281 geometric mean of the eigenvalues (determinant radius, computed here). 

282 Both of these measures give the `sigma^2` parameter for a 2D Gaussian. 

283 The determinant radius computed here is consistent with the measure 

284 computed in GalSim: 

285 http://github.com/GalSim-developers/GalSim/blob/ece3bd32c1ae6ed771f2b489c5ab1b25729e0ea4/galsim/hsm.py#L42 

286 

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

288 of the sources.""" 

289 

290 colXx = Field(doc="The column name to get the xx shape component from.", 

291 dtype=str, 

292 default="ixx") 

293 

294 colYy = Field(doc="The column name to get the yy shape component from.", 

295 dtype=str, 

296 default="iyy") 

297 

298 colXy = Field(doc="The column name to get the xy shape component from.", 

299 dtype=str, 

300 default="ixy") 

301 

302 @property 

303 def columns(self): 

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

305 

306 def __call__(self, df): 

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

308 return size 

309 

310 

311class ColorDiff(MultiColumnAction): 

312 """Calculate the difference between two colors; 

313 each color is derived from two flux columns. 

314 

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

316 

317 color1 = color1_mag1 - color1_mag2 

318 color2 = color2_mag1 - color2_mag2 

319 

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

321 

322 Parameters 

323 ---------- 

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

325 The catalog to calculate the color difference from. 

326 

327 Returns 

328 ------- 

329 The color difference in millimags. 

330 

331 Notes 

332 ----- 

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

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

335 information and assumes that the fluxes are already 

336 calibrated. 

337 """ 

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

339 dtype=str) 

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

341 dtype=str, 

342 default="nanojansky") 

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

344 dtype=str) 

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

346 dtype=str, 

347 default="nanojansky") 

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

349 dtype=str) 

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

351 dtype=str, 

352 default="nanojansky") 

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

354 dtype=str) 

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

356 dtype=str, 

357 default="nanojansky") 

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

359 dtype=bool, 

360 default=True) 

361 

362 @property 

363 def columns(self): 

364 return (self.color1_flux1, 

365 self.color1_flux2, 

366 self.color2_flux1, 

367 self.color2_flux2) 

368 

369 def __call__(self, df): 

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

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

372 

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

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

375 

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

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

378 

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

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

381 

382 color1 = color1_mag1 - color1_mag2 

383 color2 = color2_mag1 - color2_mag2 

384 

385 color_diff = color1 - color2 

386 

387 if self.return_millimags: 

388 color_diff = color_diff*1000 

389 

390 return color_diff 

391 

392 

393class ColorDiffPull(ColorDiff): 

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

395 Each color is derived from two flux columns. 

396 

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

398 

399 color1 = color1_mag1 - color1_mag2 

400 color2 = color2_mag1 - color2_mag2 

401 

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

403 

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

405 the color as computed from color1_flux1_err, color1_flux2_err, 

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

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

408 

409 Parameters 

410 ---------- 

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

412 The catalog to calculate the color difference from. 

413 

414 Returns 

415 ------- 

416 The color difference scaled by the error. 

417 

418 Notes 

419 ----- 

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

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

422 information and assumes that the fluxes are already 

423 calibrated. 

424 """ 

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

426 dtype=str, 

427 default="") 

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

429 dtype=str, 

430 default="") 

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

432 dtype=str, 

433 default="") 

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

435 dtype=str, 

436 default="") 

437 

438 def validate(self): 

439 super().validate() 

440 

441 color1_errors = False 

442 color2_errors = False 

443 

444 if self.color1_flux1_err and self.color1_flux2_err: 

445 color1_errors = True 

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

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

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

449 if self.color2_flux1_err and self.color2_flux2_err: 

450 color2_errors = True 

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

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

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

454 

455 if not color1_errors and not color2_errors: 

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

457 

458 @property 

459 def columns(self): 

460 columns = (self.color1_flux1, 

461 self.color1_flux2, 

462 self.color2_flux1, 

463 self.color2_flux2) 

464 

465 if self.color1_flux1_err: 

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

467 columns = columns + (self.color1_flux1_err, 

468 self.color1_flux2_err) 

469 

470 if self.color2_flux1_err: 

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

472 columns = columns + (self.color2_flux1_err, 

473 self.color2_flux2_err) 

474 

475 return columns 

476 

477 def __call__(self, df): 

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

479 

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

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

482 if self.color1_flux1_err: 

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

484 else: 

485 color1_mag1_err = 0.0 

486 

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

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

489 if self.color1_flux2_err: 

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

491 else: 

492 color1_mag2_err = 0.0 

493 

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

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

496 if self.color2_flux1_err: 

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

498 else: 

499 color2_mag1_err = 0.0 

500 

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

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

503 if self.color2_flux2_err: 

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

505 else: 

506 color2_mag2_err = 0.0 

507 

508 color1 = color1_mag1 - color1_mag2 

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

510 color2 = color2_mag1 - color2_mag2 

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

512 

513 color_diff = color1 - color2 

514 

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

516 

517 return pull