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

279 of the sources.""" 

280 

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

282 dtype=str, 

283 default="ixx") 

284 

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

286 dtype=str, 

287 default="iyy") 

288 

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

290 dtype=str, 

291 default="ixy") 

292 

293 @property 

294 def columns(self): 

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

296 

297 def __call__(self, df): 

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

299 return size 

300 

301 

302class ColorDiff(MultiColumnAction): 

303 """Calculate the difference between two colors; 

304 each color is derived from two flux columns. 

305 

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

307 

308 color1 = color1_mag1 - color1_mag2 

309 color2 = color2_mag1 - color2_mag2 

310 

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

312 

313 Parameters 

314 ---------- 

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

316 The catalog to calculate the color difference from. 

317 

318 Returns 

319 ------- 

320 The color difference in millimags. 

321 

322 Notes 

323 ----- 

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

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

326 information and assumes that the fluxes are already 

327 calibrated. 

328 """ 

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

330 dtype=str) 

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

332 dtype=str, 

333 default="nanojansky") 

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

335 dtype=str) 

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

337 dtype=str, 

338 default="nanojansky") 

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

340 dtype=str) 

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

342 dtype=str, 

343 default="nanojansky") 

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

345 dtype=str) 

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

347 dtype=str, 

348 default="nanojansky") 

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

350 dtype=bool, 

351 default=True) 

352 

353 @property 

354 def columns(self): 

355 return (self.color1_flux1, 

356 self.color1_flux2, 

357 self.color2_flux1, 

358 self.color2_flux2) 

359 

360 def __call__(self, df): 

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

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

363 

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

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

366 

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

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

369 

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

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

372 

373 color1 = color1_mag1 - color1_mag2 

374 color2 = color2_mag1 - color2_mag2 

375 

376 color_diff = color1 - color2 

377 

378 if self.return_millimags: 

379 color_diff = color_diff*1000 

380 

381 return color_diff 

382 

383 

384class ColorDiffPull(ColorDiff): 

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

386 Each color is derived from two flux columns. 

387 

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

389 

390 color1 = color1_mag1 - color1_mag2 

391 color2 = color2_mag1 - color2_mag2 

392 

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

394 

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

396 the color as computed from color1_flux1_err, color1_flux2_err, 

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

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

399 

400 Parameters 

401 ---------- 

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

403 The catalog to calculate the color difference from. 

404 

405 Returns 

406 ------- 

407 The color difference scaled by the error. 

408 

409 Notes 

410 ----- 

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

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

413 information and assumes that the fluxes are already 

414 calibrated. 

415 """ 

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

417 dtype=str, 

418 default="") 

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

420 dtype=str, 

421 default="") 

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

423 dtype=str, 

424 default="") 

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

426 dtype=str, 

427 default="") 

428 

429 def validate(self): 

430 super().validate() 

431 

432 color1_errors = False 

433 color2_errors = False 

434 

435 if self.color1_flux1_err and self.color1_flux2_err: 

436 color1_errors = True 

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

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

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

440 if self.color2_flux1_err and self.color2_flux2_err: 

441 color2_errors = True 

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

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

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

445 

446 if not color1_errors and not color2_errors: 

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

448 

449 @property 

450 def columns(self): 

451 columns = (self.color1_flux1, 

452 self.color1_flux2, 

453 self.color2_flux1, 

454 self.color2_flux2) 

455 

456 if self.color1_flux1_err: 

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

458 columns = columns + (self.color1_flux1_err, 

459 self.color1_flux2_err) 

460 

461 if self.color2_flux1_err: 

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

463 columns = columns + (self.color2_flux1_err, 

464 self.color2_flux2_err) 

465 

466 return columns 

467 

468 def __call__(self, df): 

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

470 

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

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

473 if self.color1_flux1_err: 

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

475 else: 

476 color1_mag1_err = 0.0 

477 

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

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

480 if self.color1_flux2_err: 

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

482 else: 

483 color1_mag2_err = 0.0 

484 

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

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

487 if self.color2_flux1_err: 

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

489 else: 

490 color2_mag1_err = 0.0 

491 

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

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

494 if self.color2_flux2_err: 

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

496 else: 

497 color2_mag2_err = 0.0 

498 

499 color1 = color1_mag1 - color1_mag2 

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

501 color2 = color2_mag1 - color2_mag2 

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

503 

504 color_diff = color1 - color2 

505 

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

507 

508 return pull