Hide keyboard shortcuts

Hot-keys 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

1""" 

2photUtils - 

3 

4 

5ljones@astro.washington.edu (and ajc@astro.washington.edu) 

6 

7and now (2014 March 28): scott.f.daniel@gmail.com 

8 

9Collection of utilities to aid usage of Sed and Bandpass with dictionaries. 

10 

11""" 

12 

13from builtins import zip 

14from builtins import range 

15from builtins import object 

16import os 

17import numpy as np 

18from collections import OrderedDict 

19from lsst.utils import getPackageDir 

20from lsst.sims.photUtils import Sed, Bandpass, LSSTdefaults, calcGamma, \ 

21 calcMagError_m5, calcSNR_m5, PhotometricParameters, magErrorFromSNR, \ 

22 BandpassDict 

23from lsst.sims.utils import defaultSpecMap 

24from lsst.sims.catalogs.decorators import compound 

25from lsst.sims.photUtils import SedList 

26from lsst.sims.utils import defaultSpecMap 

27from lsst.utils import getPackageDir 

28 

29__all__ = ["PhotometryBase", "PhotometryGalaxies", "PhotometryStars", "PhotometrySSM"] 

30 

31 

32class PhotometryBase(object): 

33 """ 

34 This class provides an InstanceCatalog with a member variable photParams, 

35 which is an instance of the class PhotometricParameters. 

36 

37 It also provides the method calculateMagnitudeUncertainty, which takes magnitudes, 

38 a BandpassDict, and an ObservationMetaData as inputs and returns the uncertainties 

39 on those magnitudes. 

40 """ 

41 

42 #an object carrying around photometric parameters like readnoise, effective area, plate scale, etc. 

43 #defaults to LSST values 

44 photParams = PhotometricParameters() 

45 

46 

47 def _cacheGamma(self, m5_names, bandpassDict): 

48 """ 

49 Generate or populate the cache of gamma values used by this InstanceCatalog 

50 to calculate photometric uncertainties (gamma is defined in equation 5 of 

51 the LSST overview paper arXiv:0805.2366) 

52 

53 @param [in] m5_names is a list of the names of keys by which m5 values are 

54 referred to in the dict self.obs_metadata.m5 

55 

56 @param [in] bandpassDict is the bandpassDict containing the bandpasses 

57 corresponding to those m5 values. 

58 """ 

59 

60 if not hasattr(self, '_gamma_cache'): 

61 self._gamma_cache = {} 

62 

63 for mm, bp in zip(m5_names, bandpassDict.values()): 

64 if mm not in self._gamma_cache and mm in self.obs_metadata.m5: 

65 self._gamma_cache[mm] = calcGamma(bp, self.obs_metadata.m5[mm], photParams=self.photParams) 

66 

67 

68 def _magnitudeUncertaintyGetter(self, column_name_list, m5_name_list, bandpassDict_name): 

69 """ 

70 Generic getter for magnitude uncertainty columns. 

71 

72 Columns must be named 'sigma_xx' where 'xx' is the column name of 

73 the associated magnitude 

74 

75 @param [in] column_name_list is the list of magnitude column names 

76 associated with the uncertainties calculated by this getter 

77 (the 'xx' in the 'sigma_xx' above) 

78 

79 @param [in] m5_name_list are the keys to the self.obs_metadata.m5 dict 

80 corresponding to the bandpasses in column_names. For example: in 

81 the case of galaxies, the magnitude columns 

82 

83 column_names = ['uBulge', 'gBulge', 'rBulge', 'iBulge', 'zBulge', 'yBulge'] 

84 

85 may correspond to m5 values keyed to 

86 

87 m5_name_list = ['u', 'g', 'r', 'i', 'z', 'y'] 

88 

89 @param [in] bandpassDict_name is a string indicating the name of 

90 the InstanceCatalog member variable containing the BandpassDict 

91 to be used when calculating these magnitude uncertainties. 

92 The BandpassDict itself will be accessed using 

93 getattr(self, bandpassDict_name) 

94 

95 @param [out] returns a 2-D numpy array in which the first index 

96 is the uncertainty column and the second index is the catalog row 

97 (i.e. output suitable for a getter in an InstanceCatalog) 

98 """ 

99 

100 # make sure that the magnitudes associated with any requested 

101 # uncertainties actually are calculated 

102 num_elements = None 

103 mag_dict = {} 

104 for name in column_name_list: 

105 if 'sigma_%s' % name in self._actually_calculated_columns: 

106 mag_dict[name] = self.column_by_name(name) 

107 if num_elements is None: 

108 num_elements = len(mag_dict[name]) 

109 

110 # These lines must come after the preceding lines; 

111 # the bandpassDict will not be loaded until the magnitude 

112 # getters are called 

113 bandpassDict = getattr(self, bandpassDict_name) 

114 self._cacheGamma(m5_name_list, bandpassDict) 

115 

116 

117 output = [] 

118 

119 for name, m5_name, bp in zip(column_name_list, m5_name_list, bandpassDict.values()): 

120 if 'sigma_%s' % name not in self._actually_calculated_columns: 

121 output.append(np.ones(num_elements)*np.NaN) 

122 else: 

123 try: 

124 m5 = self.obs_metadata.m5[m5_name] 

125 gamma = self._gamma_cache[m5_name] 

126 

127 sigma_list, gamma = calcMagError_m5(mag_dict[name], bp, m5, self.photParams, gamma=gamma) 

128 

129 output.append(sigma_list) 

130 

131 except KeyError as kk: 

132 msg = 'You got the KeyError: %s' % kk.args[0] 

133 raise KeyError('%s \n' % msg \ 

134 + 'Is it possible your ObservationMetaData does not have the proper\n' 

135 'm5 values defined?') 

136 

137 

138 return np.array(output) 

139 

140 

141 @compound('sigma_lsst_u','sigma_lsst_g','sigma_lsst_r','sigma_lsst_i', 

142 'sigma_lsst_z','sigma_lsst_y') 

143 def get_lsst_photometric_uncertainties(self): 

144 """ 

145 Getter for photometric uncertainties associated with lsst bandpasses 

146 """ 

147 

148 return self._magnitudeUncertaintyGetter(['lsst_u', 'lsst_g', 'lsst_r', 

149 'lsst_i', 'lsst_z', 'lsst_y'], 

150 ['u', 'g', 'r', 'i', 'z', 'y'], 

151 'lsstBandpassDict') 

152 

153 

154 def calculateVisibility(self, magFilter, sigma=0.1, randomSeed=None, pre_generate_randoms=False): 

155 """ 

156 Determine (probabilistically) whether a source was detected or not. 

157 

158 The 'completeness' of source detection at any magnitude is calculated by 

159 completeness = (1 + e^^(magFilter-m5)/sigma)^(-1) 

160 For each source with a magnitude magFilter, if a uniform random number [0-1) 

161 is less than or equal to 'completeness', then it is counted as "detected". 

162 See equation 24, figure 8 and table 5 from SDSS completeness analysis in 

163 http://iopscience.iop.org/0004-637X/794/2/120/pdf/apj_794_2_120.pdf 

164 "THE SLOAN DIGITAL SKY SURVEY COADD: 275 deg2 OF DEEP SLOAN DIGITAL SKY SURVEY IMAGING ON STRIPE 82" 

165 

166 @ param [in] magFilter is the magnitude of the object in the observed filter. 

167 

168 @ param [in] sigma is the FWHM of the distribution (default = 0.1) 

169 

170 @ param [in] randomSeed is an option to set a random seed (default None) 

171 @ param [in] pre_generate_randoms is an option (default False) to pre-generate a series of 12,000,000 random numbers 

172 for use throughout the visibility calculation [the random numbers used are randoms[objId]]. 

173 

174 @ param [out] visibility (None/1). 

175 """ 

176 if len(magFilter) == 0: 

177 return np.array([]) 

178 # Calculate the completeness at the magnitude of each object. 

179 completeness = 1.0 / (1 + np.exp((magFilter - self.obs_metadata.m5[self.obs_metadata.bandpass])/sigma)) 

180 # Seed numpy if desired and not previously done. 

181 if (randomSeed is not None) and (not hasattr(self, 'ssm_random_seeded')): 

182 np.random.seed(randomSeed) 

183 self.ssm_random_seeded = True 

184 # Pre-generate random numbers, if desired and not previously done. 

185 if pre_generate_randoms and not hasattr(self, 'ssm_randoms'): 

186 self.ssm_randoms = np.random.rand(14000000) 

187 # Calculate probability values to compare to completeness. 

188 if hasattr(self, 'ssm_randoms'): 

189 # Grab the random numbers from self.randoms. 

190 probability = self.ssm_randoms[self.column_by_name('objId')] 

191 else: 

192 probability = np.random.random_sample(len(magFilter)) 

193 # Compare the random number to the completeness. 

194 visibility = np.where(probability <= completeness, 1, None) 

195 return visibility 

196 

197 def _variabilityGetter(self, columnNames): 

198 """ 

199 Find columns named 'delta_*' and return them to be added 

200 to '*' magnitude columns (i.e. look for delta_lsst_u so that 

201 it can be added to lsst_u) 

202 

203 Parameters 

204 ---------- 

205 columnNames is a list of the quiescent columns (lsst_u in the 

206 example above) whose deltas we are looking for 

207 

208 Returns 

209 ------- 

210 A numpy array in which each row is a delta magnitude and each 

211 column is an astrophysical object/database row 

212 """ 

213 

214 num_obj = len(self.column_by_name(self.db_obj.idColKey)) 

215 delta = [] 

216 

217 # figure out which of these columns we are actually calculating 

218 indices = [ii for ii, name in enumerate(columnNames) 

219 if name in self._actually_calculated_columns] 

220 

221 for ix, columnName in enumerate(columnNames): 

222 if indices is None or ix in indices: 

223 delta_name = 'delta_' + columnName 

224 if delta_name in self._all_available_columns: 

225 delta.append(self.column_by_name(delta_name)) 

226 else: 

227 delta.append(np.zeros(num_obj)) 

228 else: 

229 delta.append(np.zeros(num_obj)) 

230 

231 return np.array(delta) 

232 

233class PhotometryGalaxies(PhotometryBase): 

234 """ 

235 This mixin provides the code necessary for calculating the component magnitudes associated with 

236 galaxies. It assumes that we want LSST filters. 

237 """ 

238 

239 def _hasCosmoDistMod(self): 

240 """ 

241 Determine whether or not this InstanceCatalog has a column 

242 specifically devoted to the cosmological distance modulus. 

243 """ 

244 if 'cosmologicalDistanceModulus' in self._all_available_columns: 

245 return True 

246 return False 

247 

248 

249 def _loadBulgeSedList(self, wavelen_match): 

250 """ 

251 Load a SedList of galaxy bulge Seds. 

252 The list will be stored in the variable self._bulgeSedList. 

253 

254 @param [in] wavelen_match is the wavelength grid (in nm) 

255 on which the Seds are to be sampled. 

256 """ 

257 

258 sedNameList = self.column_by_name('sedFilenameBulge') 

259 magNormList = self.column_by_name('magNormBulge') 

260 redshiftList = self.column_by_name('redshift') 

261 internalAvList = self.column_by_name('internalAvBulge') 

262 cosmologicalDimming = not self._hasCosmoDistMod() 

263 

264 if len(sedNameList)==0: 

265 return np.ones((0)) 

266 

267 if not hasattr(self, '_bulgeSedList'): 

268 self._bulgeSedList = SedList(sedNameList, magNormList, 

269 internalAvList=internalAvList, 

270 redshiftList=redshiftList, 

271 cosmologicalDimming=cosmologicalDimming, 

272 wavelenMatch=wavelen_match, 

273 fileDir=getPackageDir('sims_sed_library'), 

274 specMap=defaultSpecMap) 

275 else: 

276 self._bulgeSedList.flush() 

277 self._bulgeSedList.loadSedsFromList(sedNameList, magNormList, 

278 internalAvList=internalAvList, 

279 redshiftList=redshiftList) 

280 

281 

282 def _loadDiskSedList(self, wavelen_match): 

283 """ 

284 Load a SedList of galaxy disk Seds. 

285 The list will be stored in the variable self._bulgeSedList. 

286 

287 @param [in] wavelen_match is the wavelength grid (in nm) 

288 on which the Seds are to be sampled. 

289 """ 

290 

291 sedNameList = self.column_by_name('sedFilenameDisk') 

292 magNormList = self.column_by_name('magNormDisk') 

293 redshiftList = self.column_by_name('redshift') 

294 internalAvList = self.column_by_name('internalAvDisk') 

295 cosmologicalDimming = not self._hasCosmoDistMod() 

296 

297 if len(sedNameList)==0: 

298 return np.ones((0)) 

299 

300 if not hasattr(self, '_diskSedList'): 

301 self._diskSedList = SedList(sedNameList, magNormList, 

302 internalAvList=internalAvList, 

303 redshiftList=redshiftList, 

304 cosmologicalDimming=cosmologicalDimming, 

305 wavelenMatch=wavelen_match, 

306 fileDir=getPackageDir('sims_sed_library'), 

307 specMap=defaultSpecMap) 

308 else: 

309 self._diskSedList.flush() 

310 self._diskSedList.loadSedsFromList(sedNameList, magNormList, 

311 internalAvList=internalAvList, 

312 redshiftList=redshiftList) 

313 

314 

315 def _loadAgnSedList(self, wavelen_match): 

316 """ 

317 Load a SedList of galaxy AGN Seds. 

318 The list will be stored in the variable self._bulgeSedList. 

319 

320 @param [in] wavelen_match is the wavelength grid (in nm) 

321 on which the Seds are to be sampled. 

322 """ 

323 

324 sedNameList = self.column_by_name('sedFilenameAgn') 

325 magNormList = self.column_by_name('magNormAgn') 

326 redshiftList = self.column_by_name('redshift') 

327 cosmologicalDimming = not self._hasCosmoDistMod() 

328 

329 if len(sedNameList)==0: 

330 return np.ones((0)) 

331 

332 if not hasattr(self, '_agnSedList'): 

333 self._agnSedList = SedList(sedNameList, magNormList, 

334 redshiftList=redshiftList, 

335 cosmologicalDimming=cosmologicalDimming, 

336 wavelenMatch=wavelen_match, 

337 fileDir=getPackageDir('sims_sed_library'), 

338 specMap=defaultSpecMap) 

339 else: 

340 self._agnSedList.flush() 

341 self._agnSedList.loadSedsFromList(sedNameList, magNormList, 

342 redshiftList=redshiftList) 

343 

344 

345 def sum_magnitudes(self, disk = None, bulge = None, agn = None): 

346 """ 

347 Sum the component magnitudes of a galaxy and return the answer 

348 

349 @param [in] disk is the disk magnitude must be a numpy array or a float 

350 

351 @param [in] bulge is the bulge magnitude must be a numpy array or a float 

352 

353 @param [in] agn is the agn magnitude must be a numpy array or a float 

354 

355 @param [out] outMag is the total magnitude of the galaxy 

356 """ 

357 with np.errstate(divide='ignore', invalid='ignore'): 

358 baselineType = type(None) 

359 if not isinstance(disk, type(None)): 

360 baselineType = type(disk) 

361 if baselineType == np.ndarray: 

362 elements=len(disk) 

363 

364 if not isinstance(bulge, type(None)): 

365 if baselineType == type(None): 

366 baselineType = type(bulge) 

367 if baselineType == np.ndarray: 

368 elements = len(bulge) 

369 elif not isinstance(bulge, baselineType): 

370 raise RuntimeError("All non-None arguments of sum_magnitudes need to be " + 

371 "of the same type (float or numpy array)") 

372 

373 elif not isinstance(agn, type(None)): 

374 if baseLineType == type(None): 

375 baselineType = type(agn) 

376 if baselineType == np.ndarray: 

377 elements = len(agn) 

378 elif not isinstance(agn, baselineType): 

379 raise RuntimeError("All non-None arguments of sum_magnitudes need to be " + 

380 "of the same type (float or numpy array)") 

381 

382 if baselineType is not float and \ 

383 baselineType is not np.ndarray and \ 

384 baselineType is not np.float and \ 

385 baselineType is not np.float64: 

386 

387 raise RuntimeError("Arguments of sum_magnitudes need to be " + 

388 "either floats or numpy arrays; you appear to have passed %s " % baselineType) 

389 

390 mm_0 = 22. 

391 tol = 1.0e-30 

392 

393 if baselineType == np.ndarray: 

394 nn = np.zeros(elements) 

395 else: 

396 nn = 0.0 

397 

398 if disk is not None: 

399 nn += np.where(np.isnan(disk), 0.0, np.power(10, -0.4*(disk - mm_0))) 

400 

401 if bulge is not None: 

402 nn += np.where(np.isnan(bulge), 0.0, np.power(10, -0.4*(bulge - mm_0))) 

403 

404 if agn is not None: 

405 nn += np.where(np.isnan(agn), 0.0, np.power(10, -0.4*(agn - mm_0))) 

406 

407 if baselineType == np.ndarray: 

408 # according to this link 

409 # http://stackoverflow.com/questions/25087769/runtimewarning-divide-by-zero-error-how-to-avoid-python-numpy 

410 # we will still get a divide by zero error from log10, but np.where will be 

411 # circumventing the offending value, so it is probably okay 

412 return np.where(nn>tol, -2.5*np.log10(nn) + mm_0, np.NaN) 

413 else: 

414 if nn>tol: 

415 return -2.5*np.log10(nn) + mm_0 

416 else: 

417 return np.NaN 

418 

419 

420 def _quiescentMagnitudeGetter(self, componentName, bandpassDict, columnNameList): 

421 """ 

422 A generic getter for quiescent magnitudes of galaxy components. 

423 

424 @param [in] componentName is either 'bulge', 'disk', or 'agn' 

425 

426 @param [in] bandpassDict is a BandpassDict of the bandpasses 

427 in which to calculate the magnitudes 

428 

429 @param [in] columnNameList is a list of the columns corresponding to 

430 these magnitudes (for purposes of applying variability). 

431 

432 @param [out] magnitudes is a 2-D numpy array of magnitudes in which 

433 rows correspond to bandpasses and columns correspond to astronomical 

434 objects. 

435 """ 

436 

437 # figure out which of these columns we are actually calculating 

438 indices = [ii for ii, name in enumerate(columnNameList) 

439 if name in self._actually_calculated_columns] 

440 

441 if len(indices) == len(columnNameList): 

442 indices = None 

443 

444 if componentName == 'bulge': 

445 self._loadBulgeSedList(bandpassDict.wavelenMatch) 

446 if not hasattr(self, '_bulgeSedList'): 

447 sedList = None 

448 else: 

449 sedList = self._bulgeSedList 

450 elif componentName == 'disk': 

451 self._loadDiskSedList(bandpassDict.wavelenMatch) 

452 if not hasattr(self, '_diskSedList'): 

453 sedList = None 

454 else: 

455 sedList = self._diskSedList 

456 elif componentName == 'agn': 

457 self._loadAgnSedList(bandpassDict.wavelenMatch) 

458 if not hasattr(self, '_agnSedList'): 

459 sedList = None 

460 else: 

461 sedList = self._agnSedList 

462 else: 

463 raise RuntimeError('_quiescentMagnitudeGetter does not understand component %s ' \ 

464 % componentName) 

465 

466 if sedList is None: 

467 magnitudes = np.ones((len(columnNameList), 0)) 

468 else: 

469 magnitudes = bandpassDict.magListForSedList(sedList, indices=indices).transpose() 

470 

471 if self._hasCosmoDistMod(): 

472 cosmoDistMod = self.column_by_name('cosmologicalDistanceModulus') 

473 if len(cosmoDistMod)>0: 

474 for ix in range(magnitudes.shape[0]): 

475 magnitudes[ix] += cosmoDistMod 

476 

477 return magnitudes 

478 

479 

480 @compound('sigma_uBulge', 'sigma_gBulge', 'sigma_rBulge', 

481 'sigma_iBulge', 'sigma_zBulge', 'sigma_yBulge') 

482 def get_photometric_uncertainties_bulge(self): 

483 """ 

484 Getter for photometric uncertainties associated with galaxy bulges 

485 """ 

486 

487 return self._magnitudeUncertaintyGetter(['uBulge', 'gBulge', 'rBulge', 

488 'iBulge', 'zBulge', 'yBulge'], 

489 ['u', 'g', 'r', 'i', 'z', 'y'], 

490 'lsstBandpassDict') 

491 

492 

493 @compound('sigma_uDisk', 'sigma_gDisk', 'sigma_rDisk', 

494 'sigma_iDisk', 'sigma_zDisk', 'sigma_yDisk') 

495 def get_photometric_uncertainties_disk(self): 

496 """ 

497 Getter for photometeric uncertainties associated with galaxy disks 

498 """ 

499 

500 return self._magnitudeUncertaintyGetter(['uDisk', 'gDisk', 'rDisk', 

501 'iDisk', 'zDisk', 'yDisk'], 

502 ['u', 'g', 'r', 'i', 'z', 'y'], 

503 'lsstBandpassDict') 

504 

505 

506 @compound('sigma_uAgn', 'sigma_gAgn', 'sigma_rAgn', 

507 'sigma_iAgn', 'sigma_zAgn', 'sigma_yAgn') 

508 def get_photometric_uncertainties_agn(self): 

509 """ 

510 Getter for photometric uncertainties associated with Agn 

511 """ 

512 

513 return self._magnitudeUncertaintyGetter(['uAgn', 'gAgn', 'rAgn', 

514 'iAgn', 'zAgn', 'yAgn'], 

515 ['u', 'g', 'r', 'i', 'z', 'y'], 

516 'lsstBandpassDict') 

517 

518 

519 @compound('uBulge', 'gBulge', 'rBulge', 'iBulge', 'zBulge', 'yBulge') 

520 def get_lsst_bulge_mags(self): 

521 """ 

522 Getter for bulge magnitudes in LSST bandpasses 

523 """ 

524 

525 # load a BandpassDict of LSST bandpasses, if not done already 

526 if not hasattr(self, 'lsstBandpassDict'): 

527 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles() 

528 

529 # actually calculate the magnitudes 

530 mag = self._quiescentMagnitudeGetter('bulge', self.lsstBandpassDict, 

531 self.get_lsst_bulge_mags._colnames) 

532 

533 mag += self._variabilityGetter(self.get_lsst_bulge_mags._colnames) 

534 return mag 

535 

536 

537 @compound('uDisk', 'gDisk', 'rDisk', 'iDisk', 'zDisk', 'yDisk') 

538 def get_lsst_disk_mags(self): 

539 """ 

540 Getter for galaxy disk magnitudes in the LSST bandpasses 

541 """ 

542 

543 # load a BandpassDict of LSST bandpasses, if not done already 

544 if not hasattr(self, 'lsstBandpassDict'): 

545 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles() 

546 

547 # actually calculate the magnitudes 

548 mag = self._quiescentMagnitudeGetter('disk', self.lsstBandpassDict, 

549 self.get_lsst_disk_mags._colnames) 

550 

551 mag += self._variabilityGetter(self.get_lsst_disk_mags._colnames) 

552 return mag 

553 

554 

555 @compound('uAgn', 'gAgn', 'rAgn', 'iAgn', 'zAgn', 'yAgn') 

556 def get_lsst_agn_mags(self): 

557 """ 

558 Getter for AGN magnitudes in the LSST bandpasses 

559 """ 

560 

561 # load a BandpassDict of LSST bandpasses, if not done already 

562 if not hasattr(self, 'lsstBandpassDict'): 

563 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles() 

564 

565 # actually calculate the magnitudes 

566 mag = self._quiescentMagnitudeGetter('agn', self.lsstBandpassDict, 

567 self.get_lsst_agn_mags._colnames) 

568 

569 mag += self._variabilityGetter(self.get_lsst_agn_mags._colnames) 

570 return mag 

571 

572 @compound('lsst_u', 'lsst_g', 'lsst_r', 'lsst_i', 'lsst_z', 'lsst_y') 

573 def get_lsst_total_mags(self): 

574 """ 

575 Getter for total galaxy magnitudes in the LSST bandpasses 

576 """ 

577 

578 idList = self.column_by_name('uniqueId') 

579 numObj = len(idList) 

580 output = [] 

581 

582 # Loop over the columns calculated by this getter. For each 

583 # column, calculate the bluge, disk, and agn magnitude in the 

584 # corresponding bandpass, then sum them using the 

585 # sum_magnitudes method. 

586 for columnName in self.get_lsst_total_mags._colnames: 

587 if columnName not in self._actually_calculated_columns: 

588 sub_list = [np.NaN]*numObj 

589 else: 

590 bandpass = columnName[-1] 

591 bulge = self.column_by_name('%sBulge' % bandpass) 

592 disk = self.column_by_name('%sDisk' % bandpass) 

593 agn = self.column_by_name('%sAgn' % bandpass) 

594 sub_list = self.sum_magnitudes(bulge=bulge, disk=disk, agn=agn) 

595 

596 output.append(sub_list) 

597 return np.array(output) 

598 

599 

600 

601 

602class PhotometryStars(PhotometryBase): 

603 """ 

604 This mixin provides the infrastructure for doing photometry on stars 

605 

606 It assumes that we want LSST filters. 

607 """ 

608 

609 def _loadSedList(self, wavelen_match): 

610 """ 

611 Method to load the member variable self._sedList, which is a SedList. 

612 If self._sedList does not already exist, this method sets it up. 

613 If it does already exist, this method flushes its contents and loads a new 

614 chunk of Seds. 

615 """ 

616 

617 sedNameList = self.column_by_name('sedFilename') 

618 magNormList = self.column_by_name('magNorm') 

619 galacticAvList = self.column_by_name('galacticAv') 

620 

621 if len(sedNameList)==0: 

622 return np.ones((0)) 

623 

624 if not hasattr(self, '_sedList'): 

625 self._sedList = SedList(sedNameList, magNormList, 

626 galacticAvList=galacticAvList, 

627 wavelenMatch=wavelen_match, 

628 fileDir=getPackageDir('sims_sed_library'), 

629 specMap=defaultSpecMap) 

630 else: 

631 self._sedList.flush() 

632 self._sedList.loadSedsFromList(sedNameList, magNormList, 

633 galacticAvList=galacticAvList) 

634 

635 

636 def _quiescentMagnitudeGetter(self, bandpassDict, columnNameList): 

637 """ 

638 This method gets the magnitudes for an InstanceCatalog, returning them 

639 in a 2-D numpy array in which rows correspond to bandpasses and columns 

640 correspond to astronomical objects. 

641 

642 @param [in] bandpassDict is a BandpassDict containing the bandpasses 

643 whose magnitudes are to be calculated 

644 

645 @param [in] columnNameList is a list of the names of the magnitude columns 

646 being calculated 

647 

648 @param [out] magnitudes is a 2-D numpy array of magnitudes in which 

649 rows correspond to bandpasses in bandpassDict and columns correspond 

650 to astronomical objects. 

651 """ 

652 

653 # figure out which of these columns we are actually calculating 

654 indices = [ii for ii, name in enumerate(columnNameList) 

655 if name in self._actually_calculated_columns] 

656 

657 if len(indices) == len(columnNameList): 

658 indices = None 

659 

660 self._loadSedList(bandpassDict.wavelenMatch) 

661 

662 if not hasattr(self, '_sedList'): 

663 magnitudes = np.ones((len(columnNameList),0)) 

664 else: 

665 magnitudes = bandpassDict.magListForSedList(self._sedList, indices=indices).transpose() 

666 

667 return magnitudes 

668 

669 @compound('quiescent_lsst_u', 'quiescent_lsst_g', 'quiescent_lsst_r', 

670 'quiescent_lsst_i', 'quiescent_lsst_z', 'quiescent_lsst_y') 

671 def get_quiescent_lsst_magnitudes(self): 

672 

673 if not hasattr(self, 'lsstBandpassDict'): 

674 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles() 

675 

676 return self._quiescentMagnitudeGetter(self.lsstBandpassDict, 

677 self.get_quiescent_lsst_magnitudes._colnames) 

678 

679 @compound('lsst_u','lsst_g','lsst_r','lsst_i','lsst_z','lsst_y') 

680 def get_lsst_magnitudes(self): 

681 """ 

682 getter for LSST stellar magnitudes 

683 """ 

684 

685 magnitudes = np.array([self.column_by_name('quiescent_lsst_u'), 

686 self.column_by_name('quiescent_lsst_g'), 

687 self.column_by_name('quiescent_lsst_r'), 

688 self.column_by_name('quiescent_lsst_i'), 

689 self.column_by_name('quiescent_lsst_z'), 

690 self.column_by_name('quiescent_lsst_y')]) 

691 

692 delta = self._variabilityGetter(self.get_lsst_magnitudes._colnames) 

693 magnitudes += delta 

694 

695 return magnitudes 

696 

697class PhotometrySSM(PhotometryBase): 

698 """ 

699 A mixin to calculate photometry for solar system objects. 

700 """ 

701 # Because solar system objects will not have dust extinctions, we should be able to read in every 

702 # SED exactly once, calculate the colors and magnitudes, and then get actual magnitudes by adding 

703 # an offset based on magNorm. 

704 

705 def _quiescentMagnitudeGetter(self, bandpassDict, columnNameList, bandpassTag='lsst'): 

706 """ 

707 Method that actually does the work calculating magnitudes for solar system objects. 

708 

709 Because solar system objects have no dust extinction, this method works by loading 

710 each unique Sed once, normalizing it, calculating its magnitudes in the desired 

711 bandpasses, and then storing the normalizing magnitudes and the bandpass magnitudes 

712 in a dict. Magnitudes for subsequent objects with identical Seds will be calculated 

713 by adding an offset to the magnitudes. The offset is determined by comparing normalizing 

714 magnitues. 

715 

716 @param [in] bandpassDict is an instantiation of BandpassDict representing the bandpasses 

717 to be integrated over 

718 

719 @param [in] columnNameList is a list of the names of the columns being calculated 

720 by this getter 

721 

722 @param [in] bandpassTag (optional) is a string indicating the name of the bandpass system 

723 (i.e. 'lsst', 'sdss', etc.). This is in case the user wants to calculate the magnitudes 

724 in multiple systems simultaneously. In that case, the dict will store magnitudes for each 

725 Sed in each magnitude system separately. 

726 

727 @param [out] a numpy array of magnitudes corresponding to bandpassDict. 

728 """ 

729 

730 # figure out which of these columns we are actually calculating 

731 indices = [ii for ii, name in enumerate(columnNameList) 

732 if name in self._actually_calculated_columns] 

733 

734 if len(indices) == len(columnNameList): 

735 indices = None 

736 

737 if not hasattr(self, '_ssmMagDict'): 

738 self._ssmMagDict = {} 

739 self._ssmMagNormDict = {} 

740 self._file_dir = getPackageDir('sims_sed_library') 

741 self._spec_map = defaultSpecMap 

742 self._normalizing_bandpass = Bandpass() 

743 self._normalizing_bandpass.imsimBandpass() 

744 

745 sedNameList = self.column_by_name('sedFilename') 

746 magNormList = self.column_by_name('magNorm') 

747 

748 if len(sedNameList)==0: 

749 # need to return something when InstanceCatalog goes through 

750 # it's "dry run" to determine what columns are required from 

751 # the database 

752 return np.zeros((len(bandpassDict.keys()),0)) 

753 

754 magListOut = [] 

755 

756 for sedName, magNorm in zip(sedNameList, magNormList): 

757 magTag = bandpassTag+'_'+sedName 

758 if sedName not in self._ssmMagNormDict or magTag not in self._ssmMagDict: 

759 dummySed = Sed() 

760 dummySed.readSED_flambda(os.path.join(self._file_dir, self._spec_map[sedName])) 

761 fnorm = dummySed.calcFluxNorm(magNorm, self._normalizing_bandpass) 

762 dummySed.multiplyFluxNorm(fnorm) 

763 magList = bandpassDict.magListForSed(dummySed, indices=indices) 

764 self._ssmMagDict[magTag] = magList 

765 self._ssmMagNormDict[sedName] = magNorm 

766 else: 

767 dmag = magNorm - self._ssmMagNormDict[sedName] 

768 magList = self._ssmMagDict[magTag] + dmag 

769 magListOut.append(magList) 

770 

771 return np.array(magListOut).transpose() 

772 

773 

774 @compound('lsst_u','lsst_g','lsst_r','lsst_i','lsst_z','lsst_y') 

775 def get_lsst_magnitudes(self): 

776 """ 

777 getter for LSST magnitudes of solar system objects 

778 """ 

779 if not hasattr(self, 'lsstBandpassDict'): 

780 self.lsstBandpassDict = BandpassDict.loadTotalBandpassesFromFiles() 

781 

782 return self._quiescentMagnitudeGetter(self.lsstBandpassDict, self.get_lsst_magnitudes._colnames) 

783 

784 

785 def get_magFilter(self): 

786 """ 

787 Generate the magnitude in the filter of the observation. 

788 """ 

789 magFilter = 'lsst_' + self.obs_metadata.bandpass 

790 return self.column_by_name(magFilter) 

791 

792 def get_magSNR(self): 

793 """ 

794 Calculate the SNR for the observation, given m5 from obs_metadata and the trailing losses. 

795 """ 

796 magFilter = self.column_by_name('magFilter') 

797 bandpass = self.lsstBandpassDict[self.obs_metadata.bandpass] 

798 # Get m5 for the visit 

799 m5 = self.obs_metadata.m5[self.obs_metadata.bandpass] 

800 # Adjust the magnitude of the source for the trailing losses. 

801 dmagSNR = self.column_by_name('dmagTrailing') 

802 magObj = magFilter - dmagSNR 

803 if len(magObj) == 0: 

804 snr = [] 

805 else: 

806 snr, gamma = calcSNR_m5(magObj, bandpass, m5, self.photParams) 

807 return snr 

808 

809 def get_visibility(self): 

810 """ 

811 Generate a None/1 flag indicating whether the object was detected or not. 

812 

813 Sets the random seed for 'calculateVisibility' using the obs_metadata.obsHistId 

814 """ 

815 magFilter = self.column_by_name('magFilter') 

816 dmagDetect = self.column_by_name('dmagDetection') 

817 magObj = magFilter - dmagDetect 

818 # Adjusted m5 value, accounting for the fact these are moving objects. 

819 mjdSeed = np.int(self.obs_metadata.mjd.TAI * 1000000) % 4294967295 

820 visibility = self.calculateVisibility(magObj, randomSeed=mjdSeed, pre_generate_randoms=True) 

821 return visibility 

822 

823 

824 @compound('dmagTrailing', 'dmagDetection') 

825 def get_ssm_dmag(self): 

826 """ 

827 This getter will calculate: 

828 

829 dmagTrailing: the offset in m5 used to represent the loss in signal to noise 

830 resulting from the fact that the object's motion smears out its PSF 

831 

832 dmagDetection: the offset in m5 used to represent the shift in detection 

833 threshold resulting from the fact that the object's motion smears out 

834 its PSF 

835 """ 

836 

837 if self.obs_metadata.seeing is None: 

838 raise RuntimeError("Cannot calculate dmagTraling/dmagDetection. " 

839 "Your catalog's ObservationMetaData does not " 

840 "specify seeing.") 

841 

842 if len(self.obs_metadata.seeing)>1: 

843 valueList = list(self.obs_metadata.seeing.values()) 

844 for ix in range(1, len(valueList)): 

845 if np.abs(valueList[ix]-valueList[0])>0.0001: 

846 

847 raise RuntimeError("dmagTrailing/dmagDetection calculation is confused. " 

848 "Your catalog's ObservationMetaData contains multiple " 

849 "seeing values. Re-create your catalog with only one seeing value.") 

850 

851 if not hasattr(self, 'photParams') or self.photParams is None: 

852 raise RuntimeError("You cannot calculate dmagTrailing/dmagDetection. " 

853 "Your catalog does not have an associated PhotometricParameters " 

854 "member variable. It is impossible to know what the exposure time is.") 

855 

856 dradt = self.column_by_name('velRa') # in radians per day (actual sky velocity; 

857 # i.e., no need to divide by cos(dec)) 

858 

859 ddecdt = self.column_by_name('velDec') # in radians per day 

860 

861 if len(dradt)==0: 

862 return np.zeros((2,0)) 

863 

864 a_trail = 0.76 

865 b_trail = 1.16 

866 a_det = 0.42 

867 b_det = 0.00 

868 seeing = self.obs_metadata.seeing[self.obs_metadata.bandpass] # this will be in arcsec 

869 texp = self.photParams.nexp*self.photParams.exptime # in seconds 

870 velocity = np.sqrt(np.power(np.degrees(dradt),2) + np.power(np.degrees(ddecdt),2)) # in degrees/day 

871 x = velocity*texp/(24.0*seeing) 

872 xsq = np.power(x,2) 

873 dmagTrail = 1.25*np.log10(1.0 + a_trail * xsq/(1.0+b_trail*x)) 

874 dmagDetect = 1.25*np.log10(1.0 + a_det * xsq/(1.0 + b_det*x)) 

875 

876 return np.array([dmagTrail, dmagDetect])