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# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22 

23""" 

24sed - 

25 

26Class data: 

27wavelen (nm) 

28flambda (ergs/cm^2/s/nm) 

29fnu (Jansky) 

30zp (basically translates to units of fnu = -8.9 (if Janskys) or 48.6 (ergs/cm^2/s/hz)) 

31the name of the sed file 

32 

33It is important to note the units are NANOMETERS, not ANGSTROMS. It is possible to rig this so you can 

34use angstroms instead of nm, but you should know what you're doing and understand the wavelength grid 

35limits applied here and in Bandpass.py. 

36 

37Methods: 

38 Because of how these methods will be applied for catalog generation, (taking one base SED and then 

39 applying various dust extinctions and redshifts), many of the methods will either work on, 

40 and update self, OR they can be given a set of lambda/flambda arrays and then will return 

41 new versions of these arrays. In general, the methods will not explicitly set flambda or fnu to 

42 something you (the user) did not specify - so, for example, when calculating magnitudes (which depend on 

43 a wavelength/fnu gridded to match the given bandpass) the wavelength and fnu used are temporary copies 

44 and the object itself is not changed. 

45 In general, the philosophy of Sed.py is to not define the wavelength grid for the object until necessary 

46 (so, not until needed for the magnitude calculation or resampleSED is called). At that time the min/max/step 

47 wavelengths or the bandpass wavelengths are used to define a new wavelength grid for the sed object. 

48 When considering whether to use the internal wavelen/flambda (self) values, versus input values: 

49 For consistency, anytime self.wavelen/flambda is used, it will be updated if the values are changed 

50 (except in the special case of calculating magnitudes), and if self.wavelen/flambda is updated, 

51 self.fnu will be set to None. This is because many operations are typically chained together 

52 which alter flambda -- so it is more efficient to wait and recalculate fnu at the end, plus it 

53 avoids possible de-synchronization errors (flambda reflecting the addition of dust while fnu does 

54 not, for example). If arrays are passed into a method, they will not be altered and the arrays 

55 which are returned will be allocated new memory. 

56 Another general philosophy for Sed.py is use separate methods for items which only need to be generated once 

57 for several objects (such as the dust A_x, b_x arrays). This allows the user to optimize their code for 

58 faster operation, depending on what their requirements are (see example_SedBandpass_star.py and 

59 exampleSedBandpass_galaxy for examples). 

60 

61Method include: 

62 setSED / setFlatSED / readSED_flambda / readSED_fnu -- to input information into Sed wavelen/flambda. 

63 getSED_flambda / getSED_fnu -- to return wavelen / flambda or fnu to the user. 

64 clearSED -- set everything to 0. 

65 synchronizeSED -- to calculate wavelen/flambda/fnu on the desired grid and calculate fnu. 

66 _checkUseSelf/needResample -- not expected to be useful to the user, rather intended for internal use. 

67 resampleSED -- primarily internal use, but may be useful to user. Resamples SED onto specified grid. 

68 flambdaTofnu / fnuToflambda -- conversion methods, does not affect wavelen gridding. 

69 redshiftSED -- redshifts the SED, optionally adding dimmingx 

70 (setupODonnell_ab or setupCCM_ab) / addDust -- separated into two components, so that a_x/b_x can be reused between SEDS 

71if the wavelength range and grid is the same for each SED (calculate a_x/b_x with either setupODonnell_ab 

72or setupCCM_ab). 

73 multiplySED -- multiply two SEDS together. 

74 calcADU / calcMag / calcFlux -- with a Bandpass, calculate the ADU/magnitude/flux of a SED. 

75 calcFluxNorm / multiplyFluxNorm -- handle fluxnorm parameters (from UW LSST database) properly. 

76 These methods are intended to give a user an easy way to scale an SED to match an expected magnitude. 

77 renormalizeSED -- intended for rescaling SEDS to a common flambda or fnu level. 

78 writeSED -- keep a file record of your SED. 

79 setPhiArray -- given a list of bandpasses, sets up the 2-d phiArray (for manyMagCalc) and dlambda value. 

80 manyMagCalc -- given 2-d phiArray and dlambda, this will return an array of magnitudes (in the same 

81order as the bandpasses) of this SED in each of those bandpasses. 

82 

83""" 

84 

85from __future__ import with_statement 

86from __future__ import print_function 

87from builtins import zip 

88from builtins import str 

89from builtins import range 

90from builtins import object 

91import warnings 

92import numpy 

93import sys 

94import time 

95import scipy.interpolate as interpolate 

96import gzip 

97import pickle 

98import os 

99from .PhysicalParameters import PhysicalParameters 

100import warnings 

101try: 

102 from lsst.utils import getPackageDir 

103except: 

104 pass 

105 

106 

107# since Python now suppresses DeprecationWarnings by default 

108warnings.filterwarnings("default", category=DeprecationWarning, module='lsst.sims.photUtils.Sed') 

109 

110 

111__all__ = ["Sed", "cache_LSST_seds", "read_close_Kurucz"] 

112 

113 

114_global_lsst_sed_cache = None 

115 

116# a cache for ASCII files read-in by the user 

117_global_misc_sed_cache = None 

118 

119 

120class SedCacheError(Exception): 

121 pass 

122 

123 

124class sed_unpickler(pickle.Unpickler): 

125 

126 _allowed_obj = (("numpy", "ndarray"), 

127 ("numpy", "dtype"), 

128 ("numpy.core.multiarray", "_reconstruct")) 

129 

130 def find_class(self, module, name): 

131 allowed = False 

132 for _module, _name in self._allowed_obj: 

133 if module == _module and name == _name: 

134 allowed = True 

135 break 

136 

137 if not allowed: 

138 raise RuntimeError("Cannot call find_class() on %s, %s with sed_unpickler " % (module, name) 

139 + "this is for security reasons\n" 

140 + "https://docs.python.org/3.1/library/pickle.html#pickle-restrict") 

141 

142 if module == "numpy": 

143 if name == "ndarray": 

144 return getattr(numpy, name) 

145 elif name == "dtype": 

146 return getattr(numpy, name) 

147 else: 

148 raise RuntimeError("sed_unpickler not meant to load numpy.%s" % name) 

149 elif module == "numpy.core.multiarray": 

150 return getattr(numpy.core.multiarray, name) 

151 else: 

152 raise RuntimeError("sed_unpickler cannot handle module %s" % module) 

153 

154 

155def _validate_sed_cache(): 

156 """ 

157 Verifies that the pickled SED cache exists, is a dict, and contains 

158 an entry for every SED in starSED/ and galaxySED. Does nothing if so, 

159 raises a RuntimeError if false. 

160 

161 We are doing this here so that sims_sed_library does not have to depend 

162 on any lsst testing software (in which case, users would have to get 

163 a new copy of sims_sed_library every time the upstream software changed). 

164 

165 We are doing this through a method (rather than giving users access to 

166 _global_lsst_sed_cache) so that users do not accidentally ruin 

167 _global_lsst_sed_cache. 

168 """ 

169 global _global_lsst_sed_cache 

170 if _global_lsst_sed_cache is None: 

171 raise SedCacheError("_global_lsst_sed_cache does not exist") 

172 if not isinstance(_global_lsst_sed_cache, dict): 

173 raise SedCacheError("_global_lsst_sed_cache is a %s; not a dict" 

174 % str(type(_global_lsst_sed_cache))) 

175 sed_dir = getPackageDir('sims_sed_library') 

176 sub_dir_list = ['galaxySED', 'starSED'] 

177 file_ct = 0 

178 for sub_dir in sub_dir_list: 

179 tree = os.walk(os.path.join(sed_dir, sub_dir)) 

180 for entry in tree: 

181 local_dir = entry[0] 

182 file_list = entry[2] 

183 for file_name in file_list: 

184 if file_name.endswith('.gz'): 

185 full_name = os.path.join(sed_dir, sub_dir, local_dir, file_name) 

186 if full_name not in _global_lsst_sed_cache: 

187 raise SedCacheError("%s is not in _global_lsst_sed_cache" 

188 % full_name) 

189 file_ct += 1 

190 if file_ct == 0: 

191 raise SedCacheError("There were not files in _global_lsst_sed_cache") 

192 

193 return 

194 

195 

196def _compare_cached_versus_uncached(): 

197 """ 

198 Verify that loading an SED from the pickled cache give identical 

199 results to loading the same SED from ASCII 

200 """ 

201 sed_dir = os.path.join(getPackageDir('sims_sed_library'), 

202 'starSED', 'kurucz') 

203 

204 dtype = numpy.dtype([('wavelen', float), ('flambda', float)]) 

205 

206 sed_name_list = os.listdir(sed_dir) 

207 msg = ('An SED loaded from the pickled cache is not ' 

208 'identical to the same SED loaded from ASCII; ' 

209 'it is possible that the pickled cache was incorrectly ' 

210 'created in sims_sed_library\n\n' 

211 'Try removing the cache file (the name should hav been printed ' 

212 'to stdout above) and re-running sims_photUtils.cache_LSST_seds()') 

213 for ix in range(5): 

214 full_name = os.path.join(sed_dir, sed_name_list[ix]) 

215 from_np = numpy.genfromtxt(full_name, dtype=dtype) 

216 ss_cache = Sed() 

217 ss_cache.readSED_flambda(full_name) 

218 ss_uncache = Sed(wavelen=from_np['wavelen'], 

219 flambda=from_np['flambda'], 

220 name=full_name) 

221 

222 if not ss_cache == ss_uncache: 

223 raise SedCacheError(msg) 

224 

225 

226def _generate_sed_cache(cache_dir, cache_name): 

227 """ 

228 Read all of the SEDs from sims_sed_library into a dict. 

229 Pickle the dict and store it in 

230 sims_photUtils/cacheDir/lsst_sed_cache.p 

231 

232 Parameters 

233 ---------- 

234 cache_dir is the directory where the cache will be created 

235 cache_name is the name of the cache to be created 

236 

237 Returns 

238 ------- 

239 The dict of SEDs (keyed to their full file name) 

240 """ 

241 sed_root = getPackageDir('sims_sed_library') 

242 dtype = numpy.dtype([('wavelen', float), ('flambda', float)]) 

243 

244 sub_dir_list = ['agnSED', 'flatSED', 'ssmSED', 'starSED', 'galaxySED'] 

245 

246 cache = {} 

247 

248 total_files = 0 

249 for sub_dir in sub_dir_list: 

250 dir_tree = os.walk(os.path.join(sed_root, sub_dir)) 

251 for sub_tree in dir_tree: 

252 total_files += len([name for name in sub_tree[2] if name.endswith('.gz')]) 

253 

254 t_start = time.time() 

255 print("This could take about 15 minutes.") 

256 print("Note: not all SED files are the same size. ") 

257 print("Do not expect the loading rate to be uniform.\n") 

258 

259 for sub_dir in sub_dir_list: 

260 dir_tree = os.walk(os.path.join(sed_root, sub_dir)) 

261 for sub_tree in dir_tree: 

262 dir_name = sub_tree[0] 

263 file_list = sub_tree[2] 

264 

265 for file_name in file_list: 

266 if file_name.endswith('.gz'): 

267 try: 

268 full_name = os.path.join(dir_name, file_name) 

269 data = numpy.genfromtxt(full_name, dtype=dtype) 

270 cache[full_name] = (data['wavelen'], data['flambda']) 

271 if len(cache) % (total_files//20) == 0: 

272 if len(cache) > total_files//20: 

273 sys.stdout.write('\r') 

274 sys.stdout.write('loaded %d of %d files in about %.2f seconds' 

275 % (len(cache), total_files, time.time()-t_start)) 

276 sys.stdout.flush() 

277 except: 

278 pass 

279 

280 print('\n') 

281 

282 with open(os.path.join(cache_dir, cache_name), "wb") as file_handle: 

283 pickle.dump(cache, file_handle) 

284 

285 print('LSST SED cache saved to:\n') 

286 print('%s' % os.path.join(cache_dir, cache_name)) 

287 

288 # record the specific sims_sed_library directory being cached so that 

289 # a new cache will be generated if sims_sed_library gets updated 

290 with open(os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major), "w") as file_handle: 

291 file_handle.write("%s %s" % (sed_root, cache_name)) 

292 

293 return cache 

294 

295 

296def cache_LSST_seds(wavelen_min=None, wavelen_max=None, cache_dir=None): 

297 """ 

298 Read all of the SEDs in sims_sed_library into a dict. Pickle the dict 

299 and store it in sims_photUtils/cacheDir/lsst_sed_cache.p for future use. 

300 

301 After the file has initially been created, the next time you run this script, 

302 it will just use pickle to load the dict. 

303 

304 Once the dict is loaded, Sed.readSED_flambda() will be able to read any 

305 LSST-shipped SED directly from memory, rather than using I/O to read it 

306 from an ASCII file stored on disk. 

307 

308 Note: the dict of cached SEDs will take up about 5GB on disk. Once loaded, 

309 the cache will take up about 1.5GB of memory. The cache takes about 14 minutes 

310 to generate and about 51 seconds to load on a 2014 Mac Book Pro. 

311 

312 Parameters (optional) 

313 --------------------- 

314 wavelen_min a float 

315 

316 wavelen_max a float 

317 

318 if either of these are not None, then every SED in the cache will be 

319 truncated to only include the wavelength range (in nm) between 

320 wavelen_min and wavelen_max 

321 

322 cache_dir is a string indicating the directory in which to search for/write 

323 the cache. If set to None, the cache will be in 

324 $SIMS_SED_LIBRARY_DIR/lsst_sed_cache_dir/, which may be write-protected on 

325 shared installations of the LSST stack. Defaults to None. 

326 """ 

327 

328 global _global_lsst_sed_cache 

329 

330 try: 

331 sed_cache_name = os.path.join('lsst_sed_cache_%d.p' % sys.version_info.major) 

332 sed_dir = getPackageDir('sims_sed_library') 

333 if cache_dir is None: 

334 cache_dir = os.path.join(getPackageDir('sims_sed_library'), 'lsst_sed_cache_dir') 

335 

336 except: 

337 print('An exception was raised related to sims_sed_library. If you did not ' 

338 'install sims_photUtils with a full LSST simulations stack, you cannot ' 

339 'load and generate the cache of LSST SEDs. If you did install the full sims ' 

340 'stack but are getting this message, please check that sims_sed_library is ' 

341 'actually setup and active in your environment.') 

342 return 

343 

344 if not os.path.exists(cache_dir): 

345 os.mkdir(cache_dir) 

346 

347 must_generate = False 

348 if not os.path.exists(os.path.join(cache_dir, sed_cache_name)): 

349 must_generate = True 

350 if not os.path.exists(os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major)): 

351 must_generate = True 

352 else: 

353 with open(os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major), "r") as input_file: 

354 lines = input_file.readlines() 

355 if len(lines) != 1: 

356 must_generate = True 

357 else: 

358 info = lines[0].split() 

359 if len(info) != 2: 

360 must_generate = True 

361 elif info[0] != sed_dir: 

362 must_generate = True 

363 elif info[1] != sed_cache_name: 

364 must_generate = True 

365 

366 if must_generate: 

367 print("\nCreating cache of LSST SEDs in:\n%s" % os.path.join(cache_dir, sed_cache_name)) 

368 cache = _generate_sed_cache(cache_dir, sed_cache_name) 

369 _global_lsst_sed_cache = cache 

370 else: 

371 print("\nOpening cache of LSST SEDs in:\n%s" % os.path.join(cache_dir, sed_cache_name)) 

372 with open(os.path.join(cache_dir, sed_cache_name), 'rb') as input_file: 

373 _global_lsst_sed_cache = sed_unpickler(input_file).load() 

374 

375 # Now that we have generated/loaded the cache, we must run tests 

376 # to make sure that the cache is correctly constructed. If these 

377 # fail, _global_lsst_sed_cache will be set to 'None' and the code will 

378 # continue running. 

379 try: 

380 _validate_sed_cache() 

381 _compare_cached_versus_uncached() 

382 except SedCacheError as ee: 

383 print(ee.message) 

384 print("Cannot use cache of LSST SEDs") 

385 _global_lsst_sed_cache = None 

386 pass 

387 

388 if wavelen_min is not None or wavelen_max is not None: 

389 if wavelen_min is None: 

390 wavelen_min = 0.0 

391 if wavelen_max is None: 

392 wavelen_max = numpy.inf 

393 

394 new_cache = {} 

395 list_of_sed_names = list(_global_lsst_sed_cache.keys()) 

396 for file_name in list_of_sed_names: 

397 wav, fl = _global_lsst_sed_cache.pop(file_name) 

398 valid_dexes = numpy.where(numpy.logical_and(wav >= wavelen_min, 

399 wav <= wavelen_max)) 

400 new_cache[file_name] = (wav[valid_dexes], fl[valid_dexes]) 

401 

402 _global_lsst_sed_cache = new_cache 

403 

404 return 

405 

406 

407class Sed(object): 

408 """Class for holding and utilizing spectral energy distributions (SEDs)""" 

409 def __init__(self, wavelen=None, flambda=None, fnu=None, badval=numpy.NaN, name=None): 

410 """ 

411 Initialize sed object by giving filename or lambda/flambda array. 

412 

413 Note that this does *not* regrid flambda and leaves fnu undefined. 

414 """ 

415 self.fnu = None 

416 self.wavelen = None 

417 self.flambda = None 

418 # self.zp = -8.9 # default units, Jansky. 

419 self.zp = -2.5*numpy.log10(3631) 

420 self.name = name 

421 self.badval = badval 

422 

423 self._physParams = PhysicalParameters() 

424 

425 # If init was given data to initialize class, use it. 

426 if (wavelen is not None) and ((flambda is not None) or (fnu is not None)): 

427 if name is None: 

428 name = 'FromArray' 

429 self.setSED(wavelen, flambda=flambda, fnu=fnu, name=name) 

430 return 

431 

432 def __eq__(self, other): 

433 if self.name != other.name: 

434 return False 

435 if self.zp != other.zp: 

436 return False 

437 if not numpy.isnan(self.badval): 

438 if self.badval != other.badval: 

439 return False 

440 else: 

441 if not numpy.isnan(other.badval): 

442 return False 

443 if self.fnu is not None and other.fnu is None: 

444 return False 

445 if self.fnu is None and other.fnu is not None: 

446 return False 

447 if self.fnu is not None: 

448 try: 

449 numpy.testing.assert_array_equal(self.fnu, other.fnu) 

450 except: 

451 return False 

452 

453 if self.flambda is None and other.flambda is not None: 

454 return False 

455 if other.flambda is not None and self.flambda is None: 

456 return False 

457 if self.flambda is not None: 

458 try: 

459 numpy.testing.assert_array_equal(self.flambda, other.flambda) 

460 except: 

461 return False 

462 

463 if self.wavelen is None and other.wavelen is not None: 

464 return False 

465 if self.wavelen is not None and other.wavelen is None: 

466 return False 

467 if self.wavelen is not None: 

468 try: 

469 numpy.testing.assert_array_equal(self.wavelen, other.wavelen) 

470 except: 

471 return False 

472 

473 return True 

474 

475 def __ne__(self, other): 

476 return not self.__eq__(other) 

477 

478 # Methods for getters and setters. 

479 

480 def setSED(self, wavelen, flambda=None, fnu=None, name='FromArray'): 

481 """ 

482 Populate wavelen/flambda fields in sed by giving lambda/flambda or lambda/fnu array. 

483 

484 If flambda present, this overrides fnu. Method sets fnu=None unless only fnu is given. 

485 Sets wavelen/flambda or wavelen/flambda/fnu over wavelength array given. 

486 """ 

487 # Check wavelen array for type matches. 

488 if isinstance(wavelen, numpy.ndarray) is False: 

489 raise ValueError("Wavelength must be a numpy array") 

490 # Wavelen type ok - make new copy of data for self. 

491 self.wavelen = numpy.copy(wavelen) 

492 self.flambda = None 

493 self.fnu = None 

494 # Check if given flambda or fnu. 

495 if flambda is not None: 

496 # Check flambda data type and length. 

497 if (isinstance(flambda, numpy.ndarray) is False) or (len(flambda) != len(self.wavelen)): 

498 raise ValueError("Flambda must be a numpy array of same length as Wavelen.") 

499 # Flambda ok, make a new copy of data for self. 

500 self.flambda = numpy.copy(flambda) 

501 else: 

502 # Were passed fnu instead : check fnu data type and length. 

503 if fnu is None: 

504 raise ValueError("Both fnu and flambda are 'None', cannot set the SED.") 

505 elif (isinstance(fnu, numpy.ndarray) is False) or (len(fnu) != len(self.wavelen)): 

506 raise ValueError("(No Flambda) - Fnu must be numpy array of same length as Wavelen.") 

507 # Convert fnu to flambda. 

508 self.wavelen, self.flambda = self.fnuToflambda(wavelen, fnu) 

509 self.name = name 

510 return 

511 

512 def setFlatSED(self, wavelen_min=None, 

513 wavelen_max=None, 

514 wavelen_step=None, name='Flat'): 

515 """ 

516 Populate the wavelength/flambda/fnu fields in sed according to a flat fnu source. 

517 """ 

518 if wavelen_min is None: 

519 wavelen_min = self._physParams.minwavelen 

520 

521 if wavelen_max is None: 

522 wavelen_max = self._physParams.maxwavelen 

523 

524 if wavelen_step is None: 

525 wavelen_step = self._physParams.wavelenstep 

526 

527 self.wavelen = numpy.arange(wavelen_min, wavelen_max+wavelen_step, wavelen_step, dtype='float') 

528 self.fnu = numpy.ones(len(self.wavelen), dtype='float') * 3631 # jansky 

529 self.fnuToflambda() 

530 self.name = name 

531 return 

532 

533 def readSED_flambda(self, filename, name=None, cache_sed=True): 

534 """ 

535 Read a file containing [lambda Flambda] (lambda in nm) (Flambda erg/cm^2/s/nm). 

536 

537 Does not resample wavelen/flambda onto grid; leave fnu=None. 

538 """ 

539 global _global_lsst_sed_cache 

540 global _global_misc_sed_cache 

541 

542 # Try to open data file. 

543 # ASSUME that if filename ends with '.gz' that the file is gzipped. Otherwise, regular file. 

544 if filename.endswith('.gz'): 

545 gzipped_filename = filename 

546 unzipped_filename = filename[:-3] 

547 else: 

548 gzipped_filename = filename + '.gz' 

549 unzipped_filename = filename 

550 

551 cached_source = None 

552 if _global_lsst_sed_cache is not None: 

553 if gzipped_filename in _global_lsst_sed_cache: 

554 cached_source = _global_lsst_sed_cache[gzipped_filename] 

555 elif unzipped_filename in _global_lsst_sed_cache: 

556 cached_source = _global_lsst_sed_cache[unzipped_filename] 

557 

558 if cached_source is None and _global_misc_sed_cache is not None: 

559 if gzipped_filename in _global_misc_sed_cache: 

560 cached_source = _global_misc_sed_cache[gzipped_filename] 

561 if unzipped_filename in _global_misc_sed_cache: 

562 cached_source = _global_misc_sed_cache[unzipped_filename] 

563 

564 if cached_source is not None: 

565 sourcewavelen = numpy.copy(cached_source[0]) 

566 sourceflambda = numpy.copy(cached_source[1]) 

567 

568 if cached_source is None: 

569 # Read source SED from file - lambda, flambda should be first two columns in the file. 

570 # lambda should be in nm and flambda should be in ergs/cm2/s/nm 

571 dtype = numpy.dtype([('wavelen', float), ('flambda', float)]) 

572 try: 

573 data = numpy.genfromtxt(gzipped_filename, dtype=dtype) 

574 except IOError: 

575 try: 

576 data = numpy.genfromtxt(unzipped_filename, dtype=dtype) 

577 except Exception as err: 

578 # see 

579 # http://stackoverflow.com/questions/ 

580 # 9157210/how-do-i-raise-the-same-exception-with-a-custom-message-in-python 

581 new_args = [err.args[0] + \ 

582 "\n\nError reading sed file %s; " % filename \ 

583 + "it may not exist."] 

584 for aa in err.args[1:]: 

585 new_args.append(aa) 

586 err.args = tuple(new_args) 

587 raise 

588 

589 sourcewavelen = data['wavelen'] 

590 sourceflambda = data['flambda'] 

591 

592 if cache_sed: 

593 if _global_misc_sed_cache is None: 

594 _global_misc_sed_cache = {} 

595 _global_misc_sed_cache[filename] = (numpy.copy(sourcewavelen), 

596 numpy.copy(sourceflambda)) 

597 

598 self.wavelen = sourcewavelen 

599 self.flambda = sourceflambda 

600 self.fnu = None 

601 if name is None: 

602 self.name = filename 

603 else: 

604 self.name = name 

605 return 

606 

607 def readSED_fnu(self, filename, name=None): 

608 """ 

609 Read a file containing [lambda Fnu] (lambda in nm) (Fnu in Jansky). 

610 

611 Does not resample wavelen/fnu/flambda onto a grid; leaves fnu set. 

612 """ 

613 # Try to open the data file. 

614 try: 

615 if filename.endswith('.gz'): 

616 f = gzip.open(filename, 'rt') 

617 else: 

618 f = open(filename, 'r') 

619 # if the above fails, look for the file with and without the gz 

620 except IOError: 

621 try: 

622 if filename.endswith(".gz"): 

623 f = open(filename[:-3], 'r') 

624 else: 

625 f = gzip.open(filename+".gz", 'rt') 

626 except IOError: 

627 raise IOError("The throughput file %s does not exist" % (filename)) 

628 # Read source SED from file - lambda, fnu should be first two columns in the file. 

629 # lambda should be in nm and fnu should be in Jansky. 

630 sourcewavelen = [] 

631 sourcefnu = [] 

632 for line in f: 

633 if line.startswith("#"): 

634 continue 

635 values = line.split() 

636 sourcewavelen.append(float(values[0])) 

637 sourcefnu.append(float(values[1])) 

638 f.close() 

639 # Convert to numpy arrays. 

640 sourcewavelen = numpy.array(sourcewavelen) 

641 sourcefnu = numpy.array(sourcefnu) 

642 # Convert fnu to flambda 

643 self.fnuToflambda(sourcewavelen, sourcefnu) 

644 if name is None: 

645 self.name = filename 

646 else: 

647 self.name = name 

648 return 

649 

650 def getSED_flambda(self): 

651 """ 

652 Return copy of wavelen/flambda. 

653 """ 

654 # Get new memory copies of the arrays. 

655 wavelen = numpy.copy(self.wavelen) 

656 flambda = numpy.copy(self.flambda) 

657 return wavelen, flambda 

658 

659 def getSED_fnu(self): 

660 """ 

661 Return copy of wavelen/fnu, without altering self. 

662 """ 

663 wavelen = numpy.copy(self.wavelen) 

664 # Check if fnu currently set. 

665 if self.fnu is not None: 

666 # Get new memory copy of fnu. 

667 fnu = numpy.copy(self.fnu) 

668 else: 

669 # Fnu was not set .. grab copy fnu without changing self. 

670 wavelen, fnu = self.flambdaTofnu(self.wavelen, self.flambda) 

671 # Now wavelen/fnu (new mem) are gridded evenly, but self.wavelen/flambda/fnu remain unchanged. 

672 return wavelen, fnu 

673 

674 # Methods that update or change self. 

675 

676 def clearSED(self): 

677 """ 

678 Reset all data in sed to None. 

679 """ 

680 self.wavelen = None 

681 self.fnu = None 

682 self.flambda = None 

683 self.zp = -8.9 

684 self.name = None 

685 return 

686 

687 def synchronizeSED(self, wavelen_min=None, wavelen_max=None, wavelen_step=None): 

688 """ 

689 Set all wavelen/flambda/fnu values, potentially on min/max/step grid. 

690 

691 Uses flambda to recalculate fnu. If wavelen min/max/step are given, resamples 

692 wavelength/flambda/fnu onto an even grid with these values. 

693 """ 

694 # Grid wavelength/flambda/fnu if desired. 

695 if ((wavelen_min is not None) and (wavelen_max is not None) and (wavelen_step is not None)): 

696 self.resampleSED(wavelen_min=wavelen_min, wavelen_max=wavelen_max, 

697 wavelen_step=wavelen_step) 

698 # Reset or set fnu. 

699 self.flambdaTofnu() 

700 return 

701 

702 # Utilities common to several later methods. 

703 

704 def _checkUseSelf(self, wavelen, flux): 

705 """ 

706 Simple utility to check if should be using self's data or passed arrays. 

707 

708 Also does data integrity check on wavelen/flux if not self. 

709 """ 

710 update_self = False 

711 if (wavelen is None) or (flux is None): 

712 # Then one of the arrays was not passed - check if this is true for both arrays. 

713 if (wavelen is not None) or (flux is not None): 

714 # Then one of the arrays was passed - raise exception. 

715 raise ValueError("Must either pass *both* wavelen/flux pair, or use defaults.") 

716 update_self = True 

717 else: 

718 # Both of the arrays were passed in - check their validity. 

719 if (isinstance(wavelen, numpy.ndarray) is False) or (isinstance(flux, numpy.ndarray) is False): 

720 raise ValueError("Must pass wavelen/flux as numpy arrays.") 

721 if len(wavelen) != len(flux): 

722 raise ValueError("Must pass equal length wavelen/flux arrays.") 

723 return update_self 

724 

725 def _needResample(self, wavelen_match=None, wavelen=None, 

726 wavelen_min=None, wavelen_max=None, wavelen_step=None): 

727 """ 

728 Check if wavelen or self.wavelen matches wavelen or wavelen_min/max/step grid. 

729 """ 

730 # Check if should use self or passed wavelen. 

731 if wavelen is None: 

732 wavelen = self.wavelen 

733 # Check if wavelength arrays are equal, if wavelen_match passed. 

734 if wavelen_match is not None: 

735 if numpy.shape(wavelen_match) != numpy.shape(wavelen): 

736 need_regrid = True 

737 else: 

738 # check the elements to see if any vary 

739 need_regrid = numpy.any(abs(wavelen_match-wavelen) > 1e-10) 

740 else: 

741 need_regrid = True 

742 # Check if wavelen_min/max/step are set - if ==None, then return (no regridding). 

743 # It's possible (writeSED) to call this routine, even with no final grid in mind. 

744 if ((wavelen_min is None) and (wavelen_max is None) and (wavelen_step is None)): 

745 need_regrid = False 

746 else: 

747 # Okay, now look at comparison of wavelen to the grid. 

748 wavelen_max_in = wavelen[len(wavelen)-1] 

749 wavelen_min_in = wavelen[0] 

750 # First check match to minimum/maximum : 

751 if ((wavelen_min_in == wavelen_min) and (wavelen_max_in == wavelen_max)): 

752 # Then check on step size in wavelength array. 

753 stepsize = numpy.unique(numpy.diff(wavelen)) 

754 if (len(stepsize) == 1) and (stepsize[0] == wavelen_step): 

755 need_regrid = False 

756 # At this point, need_grid=True unless it's proven to be False, so return value. 

757 return need_regrid 

758 

759 def resampleSED(self, wavelen=None, flux=None, wavelen_match=None, 

760 wavelen_min=None, wavelen_max=None, wavelen_step=None, force=False): 

761 """ 

762 Resample flux onto grid defined by min/max/step OR another wavelength array. 

763 

764 Give method wavelen/flux OR default to self.wavelen/self.flambda. 

765 Method either returns wavelen/flambda (if given those arrays) or updates wavelen/flambda in self. 

766 If updating self, resets fnu to None. 

767 Method will first check if resampling needs to be done or not, unless 'force' is True. 

768 """ 

769 # Check if need resampling: 

770 if force or (self._needResample(wavelen_match=wavelen_match, wavelen=wavelen, wavelen_min=wavelen_min, 

771 wavelen_max=wavelen_max, wavelen_step=wavelen_step)): 

772 # Is method acting on self.wavelen/flambda or passed in wavelen/flux arrays? 

773 update_self = self._checkUseSelf(wavelen, flux) 

774 if update_self: 

775 wavelen = self.wavelen 

776 flux = self.flambda 

777 self.fnu = None 

778 # Now, on with the resampling. 

779 # Set up gridded wavelength or copy of wavelen array to match. 

780 if wavelen_match is None: 

781 if ((wavelen_min is None) and (wavelen_max is None) and (wavelen_step is None)): 

782 raise ValueError('Must set either wavelen_match or wavelen_min/max/step.') 

783 wavelen_grid = numpy.arange(wavelen_min, wavelen_max+wavelen_step, 

784 wavelen_step, dtype='float') 

785 else: 

786 wavelen_grid = numpy.copy(wavelen_match) 

787 # Check if the wavelength range desired and the wavelength range of the object overlap. 

788 # If there is any non-overlap, raise warning. 

789 if (wavelen.max() < wavelen_grid.max()) or (wavelen.min() > wavelen_grid.min()): 

790 warnings.warn('There is an area of non-overlap between desired wavelength range ' 

791 + ' (%.2f to %.2f)' % (wavelen_grid.min(), wavelen_grid.max()) 

792 + 'and sed %s (%.2f to %.2f)' % (self.name, wavelen.min(), wavelen.max())) 

793 # Do the interpolation of wavelen/flux onto grid. (type/len failures will die here). 

794 if wavelen[0] > wavelen_grid[0] or wavelen[-1] < wavelen_grid[-1]: 

795 f = interpolate.interp1d(wavelen, flux, bounds_error=False, fill_value=numpy.NaN) 

796 flux_grid = f(wavelen_grid) 

797 else: 

798 flux_grid = numpy.interp(wavelen_grid, wavelen, flux) 

799 

800 # Update self values if necessary. 

801 if update_self: 

802 self.wavelen = wavelen_grid 

803 self.flambda = flux_grid 

804 return 

805 return wavelen_grid, flux_grid 

806 else: # wavelength grids already match. 

807 update_self = self._checkUseSelf(wavelen, flux) 

808 if update_self: 

809 return 

810 return wavelen, flux 

811 

812 def flambdaTofnu(self, wavelen=None, flambda=None): 

813 """ 

814 Convert flambda into fnu. 

815 

816 This routine assumes that flambda is in ergs/cm^s/s/nm and produces fnu in Jansky. 

817 Can act on self or user can provide wavelen/flambda and get back wavelen/fnu. 

818 """ 

819 # Change Flamda to Fnu by multiplying Flambda * lambda^2 = Fv 

820 # Fv dv = Fl dl .. Fv = Fl dl / dv = Fl dl / (dl*c/l/l) = Fl*l*l/c 

821 # Check - Is the method acting on self.wavelen/flambda/fnu or passed wavelen/flambda arrays? 

822 update_self = self._checkUseSelf(wavelen, flambda) 

823 if update_self: 

824 wavelen = self.wavelen 

825 flambda = self.flambda 

826 self.fnu = None 

827 # Now on with the calculation. 

828 # Calculate fnu. 

829 fnu = flambda * wavelen * wavelen * self._physParams.nm2m / self._physParams.lightspeed 

830 fnu = fnu * self._physParams.ergsetc2jansky 

831 # If are using/updating self, then *all* wavelen/flambda/fnu will be gridded. 

832 # This is so wavelen/fnu AND wavelen/flambda can be kept in sync. 

833 if update_self: 

834 self.wavelen = wavelen 

835 self.flambda = flambda 

836 self.fnu = fnu 

837 return 

838 # Return wavelen, fnu, unless updating self (then does not return). 

839 return wavelen, fnu 

840 

841 def fnuToflambda(self, wavelen=None, fnu=None): 

842 """ 

843 Convert fnu into flambda. 

844 

845 Assumes fnu in units of Jansky and flambda in ergs/cm^s/s/nm. 

846 Can act on self or user can give wavelen/fnu and get wavelen/flambda returned. 

847 """ 

848 # Fv dv = Fl dl .. Fv = Fl dl / dv = Fl dl / (dl*c/l/l) = Fl*l*l/c 

849 # Is method acting on self or passed arrays? 

850 update_self = self._checkUseSelf(wavelen, fnu) 

851 if update_self: 

852 wavelen = self.wavelen 

853 fnu = self.fnu 

854 # On with the calculation. 

855 # Calculate flambda. 

856 flambda = fnu / wavelen / wavelen * self._physParams.lightspeed / self._physParams.nm2m 

857 flambda = flambda / self._physParams.ergsetc2jansky 

858 # If updating self, then *all of wavelen/fnu/flambda will be updated. 

859 # This is so wavelen/fnu AND wavelen/flambda can be kept in sync. 

860 if update_self: 

861 self.wavelen = wavelen 

862 self.flambda = flambda 

863 self.fnu = fnu 

864 return 

865 # Return wavelen/flambda. 

866 return wavelen, flambda 

867 

868 # methods to alter the sed 

869 

870 def redshiftSED(self, redshift, dimming=False, wavelen=None, flambda=None): 

871 """ 

872 Redshift an SED, optionally adding cosmological dimming. 

873 

874 Pass wavelen/flambda or redshift/update self.wavelen/flambda (unsets fnu). 

875 """ 

876 # Updating self or passed arrays? 

877 update_self = self._checkUseSelf(wavelen, flambda) 

878 if update_self: 

879 wavelen = self.wavelen 

880 flambda = self.flambda 

881 self.fnu = None 

882 else: 

883 # Make a copy of input data, because will change its values. 

884 wavelen = numpy.copy(wavelen) 

885 flambda = numpy.copy(flambda) 

886 # Okay, move onto redshifting the wavelen/flambda pair. 

887 # Or blueshift, as the case may be. 

888 if redshift < 0: 

889 wavelen = wavelen / (1.0-redshift) 

890 else: 

891 wavelen = wavelen * (1.0+redshift) 

892 # Flambda now just has different wavelength for each value. 

893 # Add cosmological dimming if required. 

894 if dimming: 

895 if redshift < 0: 

896 flambda = flambda * (1.0-redshift) 

897 else: 

898 flambda = flambda / (1.0+redshift) 

899 # Update self, if required - but just flambda (still no grid required). 

900 if update_self: 

901 self.wavelen = wavelen 

902 self.flambda = flambda 

903 return 

904 return wavelen, flambda 

905 

906 def setupCCMab(self, wavelen=None): 

907 """ 

908 Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen). 

909 

910 If wavelen not specified, calculates a and b on the own object's wavelength grid. 

911 Returns a(x) and b(x) can be common to many seds, wavelen is the same. 

912 

913 This method sets up extinction due to the model of 

914 Cardelli, Clayton and Mathis 1989 (ApJ 345, 245) 

915 """ 

916 warnings.warn("Sed.setupCCMab is now deprecated in favor of Sed.setupCCM_ab", 

917 DeprecationWarning) 

918 

919 return self.setupCCM_ab(wavelen=wavelen) 

920 

921 def setupCCM_ab(self, wavelen=None): 

922 """ 

923 Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen). 

924 

925 If wavelen not specified, calculates a and b on the own object's wavelength grid. 

926 Returns a(x) and b(x) can be common to many seds, wavelen is the same. 

927 

928 This method sets up extinction due to the model of 

929 Cardelli, Clayton and Mathis 1989 (ApJ 345, 245) 

930 """ 

931 # This extinction law taken from Cardelli, Clayton and Mathis ApJ 1989. 

932 # The general form is A_l / A(V) = a(x) + b(x)/R_V (where x=1/lambda in microns), 

933 # then different values for a(x) and b(x) depending on wavelength regime. 

934 # Also, the extinction is parametrized as R_v = A_v / E(B-V). 

935 # Magnitudes of extinction (A_l) translates to flux by a_l = -2.5log(f_red / f_nonred). 

936 if wavelen is None: 

937 wavelen = numpy.copy(self.wavelen) 

938 a_x = numpy.zeros(len(wavelen), dtype='float') 

939 b_x = numpy.zeros(len(wavelen), dtype='float') 

940 # Convert wavelength to x (in inverse microns). 

941 x = numpy.empty(len(wavelen), dtype=float) 

942 nm_to_micron = 1/1000.0 

943 x = 1.0 / (wavelen * nm_to_micron) 

944 # Dust in infrared 0.3 /mu < x < 1.1 /mu (inverse microns). 

945 condition = (x >= 0.3) & (x <= 1.1) 

946 if len(a_x[condition]) > 0: 

947 y = x[condition] 

948 a_x[condition] = 0.574 * y**1.61 

949 b_x[condition] = -0.527 * y**1.61 

950 # Dust in optical/NIR 1.1 /mu < x < 3.3 /mu region. 

951 condition = (x >= 1.1) & (x <= 3.3) 

952 if len(a_x[condition]) > 0: 

953 y = x[condition] - 1.82 

954 a_x[condition] = 1 + 0.17699*y - 0.50447*y**2 - 0.02427*y**3 + 0.72085*y**4 

955 a_x[condition] = a_x[condition] + 0.01979*y**5 - 0.77530*y**6 + 0.32999*y**7 

956 b_x[condition] = 1.41338*y + 2.28305*y**2 + 1.07233*y**3 - 5.38434*y**4 

957 b_x[condition] = b_x[condition] - 0.62251*y**5 + 5.30260*y**6 - 2.09002*y**7 

958 # Dust in ultraviolet and UV (if needed for high-z) 3.3 /mu< x< 8 /mu. 

959 condition = (x >= 3.3) & (x < 5.9) 

960 if len(a_x[condition]) > 0: 

961 y = x[condition] 

962 a_x[condition] = 1.752 - 0.316*y - 0.104/((y-4.67)**2 + 0.341) 

963 b_x[condition] = -3.090 + 1.825*y + 1.206/((y-4.62)**2 + 0.263) 

964 condition = (x > 5.9) & (x < 8) 

965 if len(a_x[condition]) > 0: 

966 y = x[condition] 

967 Fa_x = numpy.empty(len(a_x[condition]), dtype=float) 

968 Fb_x = numpy.empty(len(a_x[condition]), dtype=float) 

969 Fa_x = -0.04473*(y-5.9)**2 - 0.009779*(y-5.9)**3 

970 Fb_x = 0.2130*(y-5.9)**2 + 0.1207*(y-5.9)**3 

971 a_x[condition] = 1.752 - 0.316*y - 0.104/((y-4.67)**2 + 0.341) + Fa_x 

972 b_x[condition] = -3.090 + 1.825*y + 1.206/((y-4.62)**2 + 0.263) + Fb_x 

973 # Dust in far UV (if needed for high-z) 8 /mu < x < 10 /mu region. 

974 condition = (x >= 8) & (x <= 11.) 

975 if len(a_x[condition]) > 0: 

976 y = x[condition]-8.0 

977 a_x[condition] = -1.073 - 0.628*(y) + 0.137*(y)**2 - 0.070*(y)**3 

978 b_x[condition] = 13.670 + 4.257*(y) - 0.420*(y)**2 + 0.374*(y)**3 

979 return a_x, b_x 

980 

981 def setupODonnell_ab(self, wavelen=None): 

982 """ 

983 Calculate a(x) and b(x) for O'Donnell dust model. (x=1/wavelen). 

984 

985 If wavelen not specified, calculates a and b on the own object's wavelength grid. 

986 Returns a(x) and b(x) can be common to many seds, wavelen is the same. 

987 

988 This method sets up the extinction parameters from the model of O'Donnel 1994 

989 (ApJ 422, 158) 

990 """ 

991 # The general form is A_l / A(V) = a(x) + b(x)/R_V (where x=1/lambda in microns), 

992 # then different values for a(x) and b(x) depending on wavelength regime. 

993 # Also, the extinction is parametrized as R_v = A_v / E(B-V). 

994 # Magnitudes of extinction (A_l) translates to flux by a_l = -2.5log(f_red / f_nonred). 

995 if wavelen is None: 

996 wavelen = numpy.copy(self.wavelen) 

997 a_x = numpy.zeros(len(wavelen), dtype='float') 

998 b_x = numpy.zeros(len(wavelen), dtype='float') 

999 # Convert wavelength to x (in inverse microns). 

1000 x = numpy.empty(len(wavelen), dtype=float) 

1001 nm_to_micron = 1/1000.0 

1002 x = 1.0 / (wavelen * nm_to_micron) 

1003 # Dust in infrared 0.3 /mu < x < 1.1 /mu (inverse microns). 

1004 condition = (x >= 0.3) & (x <= 1.1) 

1005 if len(a_x[condition]) > 0: 

1006 y = x[condition] 

1007 a_x[condition] = 0.574 * y**1.61 

1008 b_x[condition] = -0.527 * y**1.61 

1009 # Dust in optical/NIR 1.1 /mu < x < 3.3 /mu region. 

1010 condition = (x >= 1.1) & (x <= 3.3) 

1011 if len(a_x[condition]) > 0: 

1012 y = x[condition] - 1.82 

1013 a_x[condition] = 1 + 0.104*y - 0.609*y**2 + 0.701*y**3 + 1.137*y**4 

1014 a_x[condition] = a_x[condition] - 1.718*y**5 - 0.827*y**6 + 1.647*y**7 - 0.505*y**8 

1015 b_x[condition] = 1.952*y + 2.908*y**2 - 3.989*y**3 - 7.985*y**4 

1016 b_x[condition] = b_x[condition] + 11.102*y**5 + 5.491*y**6 - 10.805*y**7 + 3.347*y**8 

1017 # Dust in ultraviolet and UV (if needed for high-z) 3.3 /mu< x< 8 /mu. 

1018 condition = (x >= 3.3) & (x < 5.9) 

1019 if len(a_x[condition]) > 0: 

1020 y = x[condition] 

1021 a_x[condition] = 1.752 - 0.316*y - 0.104/((y-4.67)**2 + 0.341) 

1022 b_x[condition] = -3.090 + 1.825*y + 1.206/((y-4.62)**2 + 0.263) 

1023 condition = (x > 5.9) & (x < 8) 

1024 if len(a_x[condition]) > 0: 

1025 y = x[condition] 

1026 Fa_x = numpy.empty(len(a_x[condition]), dtype=float) 

1027 Fb_x = numpy.empty(len(a_x[condition]), dtype=float) 

1028 Fa_x = -0.04473*(y-5.9)**2 - 0.009779*(y-5.9)**3 

1029 Fb_x = 0.2130*(y-5.9)**2 + 0.1207*(y-5.9)**3 

1030 a_x[condition] = 1.752 - 0.316*y - 0.104/((y-4.67)**2 + 0.341) + Fa_x 

1031 b_x[condition] = -3.090 + 1.825*y + 1.206/((y-4.62)**2 + 0.263) + Fb_x 

1032 # Dust in far UV (if needed for high-z) 8 /mu < x < 10 /mu region. 

1033 condition = (x >= 8) & (x <= 11.) 

1034 if len(a_x[condition]) > 0: 

1035 y = x[condition]-8.0 

1036 a_x[condition] = -1.073 - 0.628*(y) + 0.137*(y)**2 - 0.070*(y)**3 

1037 b_x[condition] = 13.670 + 4.257*(y) - 0.420*(y)**2 + 0.374*(y)**3 

1038 return a_x, b_x 

1039 

1040 def addCCMDust(self, a_x, b_x, A_v=None, ebv=None, R_v=3.1, wavelen=None, flambda=None): 

1041 """ 

1042 Add dust model extinction to the SED, modifying flambda and fnu. 

1043 

1044 Get a_x and b_x either from setupCCMab or setupODonnell_ab 

1045 

1046 Specify any two of A_V, E(B-V) or R_V (=3.1 default). 

1047 """ 

1048 warnings.warn("Sed.addCCMDust is now deprecated in favor of Sed.addDust", 

1049 DeprecationWarning) 

1050 return self.addDust(a_x, b_x, A_v=A_v, ebv=ebv, 

1051 R_v=R_v, wavelen=wavelen, flambda=flambda) 

1052 

1053 def addDust(self, a_x, b_x, A_v=None, ebv=None, R_v=3.1, wavelen=None, flambda=None): 

1054 """ 

1055 Add dust model extinction to the SED, modifying flambda and fnu. 

1056 

1057 Get a_x and b_x either from setupCCMab or setupODonnell_ab 

1058 

1059 Specify any two of A_V, E(B-V) or R_V (=3.1 default). 

1060 """ 

1061 if not hasattr(self, '_ln10_04'): 

1062 self._ln10_04 = 0.4*numpy.log(10.0) 

1063 

1064 # The extinction law taken from Cardelli, Clayton and Mathis ApJ 1989. 

1065 # The general form is A_l / A(V) = a(x) + b(x)/R_V (where x=1/lambda in microns). 

1066 # Then, different values for a(x) and b(x) depending on wavelength regime. 

1067 # Also, the extinction is parametrized as R_v = A_v / E(B-V). 

1068 # The magnitudes of extinction (A_l) translates to flux by a_l = -2.5log(f_red / f_nonred). 

1069 # 

1070 # Figure out if updating self or passed arrays. 

1071 update_self = self._checkUseSelf(wavelen, flambda) 

1072 if update_self: 

1073 wavelen = self.wavelen 

1074 flambda = self.flambda 

1075 self.fnu = None 

1076 else: 

1077 wavelen = numpy.copy(wavelen) 

1078 flambda = numpy.copy(flambda) 

1079 # Input parameters for reddening can include any of 3 parameters; only 2 are independent. 

1080 # Figure out what parameters were given, and see if self-consistent. 

1081 if R_v == 3.1: 

1082 if A_v is None: 

1083 A_v = R_v * ebv 

1084 elif (A_v is not None) and (ebv is not None): 

1085 # Specified A_v and ebv, so R_v should be nondefault. 

1086 R_v = A_v / ebv 

1087 if (R_v != 3.1): 

1088 if (A_v is not None) and (ebv is not None): 

1089 calcRv = A_v / ebv 

1090 if calcRv != R_v: 

1091 raise ValueError("CCM parametrization expects R_v = A_v / E(B-V);", 

1092 "Please check input values, because values are inconsistent.") 

1093 elif A_v is None: 

1094 A_v = R_v * ebv 

1095 # R_v and A_v values are specified or calculated. 

1096 

1097 A_lambda = (a_x + b_x / R_v) * A_v 

1098 # dmag_red(dust) = -2.5 log10 (f_red / f_nored) : (f_red / f_nored) = 10**-0.4*dmag_red 

1099 dust = numpy.exp(-A_lambda*self._ln10_04) 

1100 flambda *= dust 

1101 # Update self if required. 

1102 if update_self: 

1103 self.flambda = flambda 

1104 return 

1105 return wavelen, flambda 

1106 

1107 def multiplySED(self, other_sed, wavelen_step=None): 

1108 """ 

1109 Multiply two SEDs together - flambda * flambda - and return a new sed object. 

1110 

1111 Unless the two wavelength arrays are equal, returns a SED gridded with stepsize wavelen_step 

1112 over intersecting wavelength region. Does not alter self or other_sed. 

1113 """ 

1114 

1115 if wavelen_step is None: 

1116 wavelen_step = self._physParams.wavelenstep 

1117 

1118 # Check if the wavelength arrays are equal (in which case do not resample) 

1119 if (numpy.all(self.wavelen == other_sed.wavelen)): 

1120 flambda = self.flambda * other_sed.flambda 

1121 new_sed = Sed(self.wavelen, flambda=flambda) 

1122 else: 

1123 # Find overlapping wavelength region. 

1124 wavelen_max = min(self.wavelen.max(), other_sed.wavelen.max()) 

1125 wavelen_min = max(self.wavelen.min(), other_sed.wavelen.min()) 

1126 if wavelen_max < wavelen_min: 

1127 raise Exception('The two SEDS do not overlap in wavelength space.') 

1128 # Set up wavelen/flambda of first object, on grid. 

1129 wavelen_1, flambda_1 = self.resampleSED(self.wavelen, self.flambda, 

1130 wavelen_min=wavelen_min, 

1131 wavelen_max=wavelen_max, 

1132 wavelen_step=wavelen_step) 

1133 # Set up wavelen/flambda of second object, on grid. 

1134 wavelen_2, flambda_2 = self.resampleSED(wavelen=other_sed.wavelen, flux=other_sed.flambda, 

1135 wavelen_min=wavelen_min, wavelen_max=wavelen_max, 

1136 wavelen_step = wavelen_step) 

1137 # Multiply the two flambda together. 

1138 flambda = flambda_1 * flambda_2 

1139 # Instantiate new sed object. wavelen_1 == wavelen_2 as both are on grid. 

1140 new_sed = Sed(wavelen_1, flambda) 

1141 return new_sed 

1142 

1143 # routines related to magnitudes and fluxes 

1144 

1145 def calcADU(self, bandpass, photParams, wavelen=None, fnu=None): 

1146 """ 

1147 Calculate the number of adu from camera, using sb and fnu. 

1148 

1149 Given wavelen/fnu arrays or use self. Self or passed wavelen/fnu arrays will be unchanged. 

1150 Calculating the AB mag requires the wavelen/fnu pair to be on the same grid as bandpass; 

1151 (temporary values of these are used). 

1152 

1153 @param [in] bandpass is an instantiation of the Bandpass class 

1154 

1155 @param [in] photParams is an instantiation of the 

1156 PhotometricParameters class that carries details about the 

1157 photometric response of the telescope. 

1158 

1159 @param [in] wavelen (optional) is the wavelength grid in nm 

1160 

1161 @param [in] fnu (optional) is the flux in Janskys 

1162 

1163 If wavelen and fnu are not specified, this will just use self.wavelen and 

1164 self.fnu 

1165 

1166 """ 

1167 

1168 use_self = self._checkUseSelf(wavelen, fnu) 

1169 # Use self values if desired, otherwise use values passed to function. 

1170 if use_self: 

1171 # Calculate fnu if required. 

1172 if self.fnu is None: 

1173 # If fnu not present, calculate. (does not regrid). 

1174 self.flambdaTofnu() 

1175 wavelen = self.wavelen 

1176 fnu = self.fnu 

1177 # Make sure wavelen/fnu are on the same wavelength grid as bandpass. 

1178 wavelen, fnu = self.resampleSED(wavelen, fnu, wavelen_match=bandpass.wavelen) 

1179 # Calculate the number of photons. 

1180 dlambda = wavelen[1] - wavelen[0] 

1181 # Nphoton in units of 10^-23 ergs/cm^s/nm. 

1182 nphoton = (fnu / wavelen * bandpass.sb).sum() 

1183 adu = nphoton * (photParams.exptime * photParams.nexp * photParams.effarea/photParams.gain) * \ 

1184 (1/self._physParams.ergsetc2jansky) * \ 

1185 (1/self._physParams.planck) * dlambda 

1186 return adu 

1187 

1188 def fluxFromMag(self, mag): 

1189 """ 

1190 Convert a magnitude back into a flux (implies knowledge of the zeropoint, which is 

1191 stored in this class) 

1192 """ 

1193 

1194 return numpy.power(10.0, -0.4*(mag + self.zp)) 

1195 

1196 def magFromFlux(self, flux): 

1197 """ 

1198 Convert a flux into a magnitude (implies knowledge of the zeropoint, which is stored 

1199 in this class) 

1200 """ 

1201 

1202 return -2.5*numpy.log10(flux) - self.zp 

1203 

1204 def calcErgs(self, bandpass): 

1205 """ 

1206 Integrate the SED over a bandpass directly. If self.flambda 

1207 is in ergs/s/cm^2/nm and bandpass.sb is the unitless probability 

1208 that a photon of a given wavelength will pass through the system, 

1209 this method will return the ergs/s/cm^2 of the source observed 

1210 through that bandpass (i.e. it will return the integral 

1211 

1212 \int self.flambda(lambda) * bandpass.sb(lambda) * dlambda 

1213 

1214 This is to be contrasted with self.calcFlux(), which returns 

1215 the integral of the source's specific flux density over the 

1216 normalized response function of bandpass, giving a flux in 

1217 Janskys (10^-23 erg/cm^2/s/Hz), which should be though of as 

1218 a weighted average of the specific flux density of the source 

1219 over the normalized response function, as detailed in Section 

1220 4.1 of the LSST design document LSE-180. 

1221 

1222 Parameters 

1223 ---------- 

1224 bandpass is an instantiation of the Bandpass class 

1225 

1226 Returns 

1227 ------- 

1228 The flux of the current SED through the bandpass in ergs/s/cm^2 

1229 """ 

1230 wavelen, flambda = self.resampleSED(wavelen=self.wavelen, 

1231 flux=self.flambda, 

1232 wavelen_match=bandpass.wavelen) 

1233 

1234 dlambda = wavelen[1]-wavelen[0] 

1235 

1236 # use the trapezoid rule 

1237 energy = (0.5*(flambda[1:]*bandpass.sb[1:] + 

1238 flambda[:-1]*bandpass.sb[:-1])*dlambda).sum() 

1239 return energy 

1240 

1241 def calcFlux(self, bandpass, wavelen=None, fnu=None): 

1242 """ 

1243 Integrate the specific flux density of the object over the normalized response 

1244 curve of a bandpass, giving a flux in Janskys (10^-23 ergs/s/cm^2/Hz) through 

1245 the normalized response curve, as detailed in Section 4.1 of the LSST design 

1246 document LSE-180 and Section 2.6 of the LSST Science Book 

1247 (http://ww.lsst.org/scientists/scibook). This flux in Janskys (which is usually 

1248 though of as a unit of specific flux density), should be considered a weighted 

1249 average of the specific flux density over the normalized response curve of the 

1250 bandpass. Because we are using the normalized response curve (phi in LSE-180), 

1251 this quantity will depend only on the shape of the response curve, not its 

1252 absolute normalization. 

1253 

1254 Note: the way that the normalized response curve has been defined (see equation 

1255 5 of LSE-180) is appropriate for photon-counting detectors, not calorimeters. 

1256 

1257 Passed wavelen/fnu arrays will be unchanged, but if uses self will check if fnu is set. 

1258 

1259 Calculating the AB mag requires the wavelen/fnu pair to be on the same grid as bandpass; 

1260 (temporary values of these are used). 

1261 """ 

1262 # Note - the behavior in this first section might be considered a little odd. 

1263 # However, I felt calculating a magnitude should not (unexpectedly) regrid your 

1264 # wavelen/flambda information if you were using self., as this is not obvious from the "outside". 

1265 # To preserve 'user logic', the wavelen/flambda of self are left untouched. Unfortunately 

1266 # this means, this method can be used inefficiently if calculating many magnitudes with 

1267 # the same sed and same bandpass region - in that case, use self.synchronizeSED() with 

1268 # the wavelen min/max/step set to the bandpass min/max/step first .. 

1269 # then you can calculate multiple magnitudes much more efficiently! 

1270 use_self = self._checkUseSelf(wavelen, fnu) 

1271 # Use self values if desired, otherwise use values passed to function. 

1272 if use_self: 

1273 # Calculate fnu if required. 

1274 if self.fnu is None: 

1275 self.flambdaTofnu() 

1276 wavelen = self.wavelen 

1277 fnu = self.fnu 

1278 # Go on with magnitude calculation. 

1279 wavelen, fnu = self.resampleSED(wavelen, fnu, wavelen_match=bandpass.wavelen) 

1280 # Calculate bandpass phi value if required. 

1281 if bandpass.phi is None: 

1282 bandpass.sbTophi() 

1283 # Calculate flux in bandpass and return this value. 

1284 dlambda = wavelen[1] - wavelen[0] 

1285 flux = (fnu*bandpass.phi).sum() * dlambda 

1286 return flux 

1287 

1288 def calcMag(self, bandpass, wavelen=None, fnu=None): 

1289 """ 

1290 Calculate the AB magnitude of an object using the normalized system response (phi from Section 

1291 4.1 of the LSST design document LSE-180). 

1292 

1293 Can pass wavelen/fnu arrays or use self. Self or passed wavelen/fnu arrays will be unchanged. 

1294 Calculating the AB mag requires the wavelen/fnu pair to be on the same grid as bandpass; 

1295 (but only temporary values of these are used). 

1296 """ 

1297 flux = self.calcFlux(bandpass, wavelen=wavelen, fnu=fnu) 

1298 if flux < 1e-300: 

1299 raise Exception("This SED has no flux within this bandpass.") 

1300 mag = self.magFromFlux(flux) 

1301 return mag 

1302 

1303 def calcFluxNorm(self, magmatch, bandpass, wavelen=None, fnu=None): 

1304 """ 

1305 Calculate the fluxNorm (SED normalization value for a given mag) for a sed. 

1306 

1307 Equivalent to adjusting a particular f_nu to Jansky's appropriate for the desired mag. 

1308 Can pass wavelen/fnu or apply to self. 

1309 """ 

1310 use_self = self._checkUseSelf(wavelen, fnu) 

1311 if use_self: 

1312 # Check possibility that fnu is not calculated yet. 

1313 if self.fnu is None: 

1314 self.flambdaTofnu() 

1315 wavelen = self.wavelen 

1316 fnu = self.fnu 

1317 # Fluxnorm gets applied to f_nu (fluxnorm * SED(f_nu) * PHI = mag - 8.9 (AB zeropoint). 

1318 # FluxNorm * SED => correct magnitudes for this object. 

1319 # Calculate fluxnorm. 

1320 curmag = self.calcMag(bandpass, wavelen, fnu) 

1321 if curmag == self.badval: 

1322 return self.badval 

1323 dmag = magmatch - curmag 

1324 fluxnorm = numpy.power(10, (-0.4*dmag)) 

1325 return fluxnorm 

1326 

1327 def multiplyFluxNorm(self, fluxNorm, wavelen=None, fnu=None): 

1328 """ 

1329 Multiply wavelen/fnu (or self.wavelen/fnu) by fluxnorm. 

1330 

1331 Returns wavelen/fnu arrays (or updates self). 

1332 Note that multiplyFluxNorm does not regrid self.wavelen/flambda/fnu at all. 

1333 """ 

1334 # Note that fluxNorm is intended to be applied to f_nu, 

1335 # so that fluxnorm*fnu*phi = mag (expected magnitude). 

1336 update_self = self._checkUseSelf(wavelen, fnu) 

1337 if update_self: 

1338 # Make sure fnu is defined. 

1339 if self.fnu is None: 

1340 self.flambdaTofnu() 

1341 wavelen = self.wavelen 

1342 fnu = self.fnu 

1343 else: 

1344 # Require new copy of the data for multiply. 

1345 wavelen = numpy.copy(wavelen) 

1346 fnu = numpy.copy(fnu) 

1347 # Apply fluxnorm. 

1348 fnu = fnu * fluxNorm 

1349 # Update self. 

1350 if update_self: 

1351 self.wavelen = wavelen 

1352 self.fnu = fnu 

1353 # Update flambda as well. 

1354 self.fnuToflambda() 

1355 return 

1356 # Else return new wavelen/fnu pairs. 

1357 return wavelen, fnu 

1358 

1359 def renormalizeSED(self, wavelen=None, flambda=None, fnu=None, 

1360 lambdanorm=500, normvalue=1, gap=0, normflux='flambda', 

1361 wavelen_step=None): 

1362 """ 

1363 Renormalize sed in flambda to have normflux=normvalue @ lambdanorm or averaged over gap. 

1364 

1365 Can normalized in flambda or fnu values. wavelen_step specifies the wavelength spacing 

1366 when using 'gap'. 

1367 

1368 Either returns wavelen/flambda values or updates self. 

1369 """ 

1370 # Normalizes the fnu/flambda SED at one wavelength or average value over small range (gap). 

1371 # This is useful for generating SED catalogs, mostly, to make them match schema. 

1372 # Do not use this for calculating specific magnitudes -- use calcfluxNorm and multiplyFluxNorm. 

1373 # Start normalizing wavelen/flambda. 

1374 

1375 if wavelen_step is None: 

1376 wavelen_step = self._physParams.wavelenstep 

1377 

1378 if normflux == 'flambda': 

1379 update_self = self._checkUseSelf(wavelen, flambda) 

1380 if update_self: 

1381 wavelen = self.wavelen 

1382 flambda = self.flambda 

1383 else: 

1384 # Make a copy of the input data. 

1385 wavelen = numpy.copy(wavelen) 

1386 # Look for either flambda or fnu in input data. 

1387 if flambda is None: 

1388 if fnu is None: 

1389 raise Exception("If passing wavelength, must also pass fnu or flambda.") 

1390 # If not given flambda, must calculate from the given values of fnu. 

1391 wavelen, flambda = self.fnuToflambda(wavelen, fnu) 

1392 # Make a copy of the input data. 

1393 else: 

1394 flambda = numpy.copy(flambda) 

1395 # Calculate renormalization values. 

1396 # Check that flambda is defined at the wavelength want to use for renormalization. 

1397 if (lambdanorm > wavelen.max()) or (lambdanorm < wavelen.min()): 

1398 raise Exception("Desired wavelength for renormalization, %f, " % (lambdanorm) 

1399 + "is outside defined wavelength range.") 

1400 # "standard" schema have flambda = 1 at 500 nm. 

1401 if gap == 0: 

1402 flambda_atpt = numpy.interp(lambdanorm, wavelen, flambda, left=None, right=None) 

1403 gapval = flambda_atpt 

1404 else: 

1405 lambdapt = numpy.arange(lambdanorm-gap, lambdanorm+gap, wavelen_step, dtype=float) 

1406 flambda_atpt = numpy.zeros(len(lambdapt), dtype='float') 

1407 flambda_atpt = numpy.interp(lambdapt, wavelen, flambda, left=None, right=None) 

1408 gapval = flambda_atpt.sum()/len(lambdapt) 

1409 # Now renormalize fnu and flambda in the case of normalizing flambda. 

1410 if gapval == 0: 

1411 raise Exception("Original flambda is 0 at the desired point of normalization. " 

1412 "Cannot renormalize.") 

1413 konst = normvalue/gapval 

1414 flambda = flambda * konst 

1415 wavelen, fnu = self.flambdaTofnu(wavelen, flambda) 

1416 elif normflux == 'fnu': 

1417 update_self = self._checkUseSelf(wavelen, fnu) 

1418 if update_self: 

1419 wavelen = self.wavelen 

1420 if self.fnu is None: 

1421 self.flambdaTofnu() 

1422 fnu = self.fnu 

1423 else: 

1424 # Make a copy of the input data. 

1425 wavelen = numpy.copy(wavelen) 

1426 # Look for either flambda or fnu in input data. 

1427 if fnu is None: 

1428 if flambda is None: 

1429 raise Exception("If passing wavelength, must also pass fnu or flambda.") 

1430 wavelen, fnu = self.flambdaTofnu(wavelen, fnu) 

1431 # Make a copy of the input data. 

1432 else: 

1433 fnu = numpy.copy(fnu) 

1434 # Calculate renormalization values. 

1435 # Check that flambda is defined at the wavelength want to use for renormalization. 

1436 if (lambdanorm > wavelen.max()) or (lambdanorm < wavelen.min()): 

1437 raise Exception("Desired wavelength for renormalization, %f, " % (lambdanorm) 

1438 + "is outside defined wavelength range.") 

1439 if gap == 0: 

1440 fnu_atpt = numpy.interp(lambdanorm, wavelen, flambda, left=None, right=None) 

1441 gapval = fnu_atpt 

1442 else: 

1443 lambdapt = numpy.arange(lambdanorm-gap, lambdanorm+gap, wavelen_step, dtype=float) 

1444 fnu_atpt = numpy.zeros(len(lambdapt), dtype='float') 

1445 fnu_atpt = numpy.interp(lambdapt, wavelen, fnu, left=None, right=None) 

1446 gapval = fnu_atpt.sum()/len(lambdapt) 

1447 # Now renormalize fnu and flambda in the case of normalizing fnu. 

1448 if gapval == 0: 

1449 raise Exception("Original fnu is 0 at the desired point of normalization. " 

1450 "Cannot renormalize.") 

1451 konst = normvalue/gapval 

1452 fnu = fnu * konst 

1453 wavelen, flambda = self.fnutoflambda(wavelen, fnu) 

1454 if update_self: 

1455 self.wavelen = wavelen 

1456 self.flambda = flambda 

1457 self.fnu = fnu 

1458 return 

1459 new_sed = Sed(wavelen=wavelen, flambda=flambda) 

1460 return new_sed 

1461 

1462 def writeSED(self, filename, print_header=None, print_fnu=False, 

1463 wavelen_min=None, wavelen_max=None, wavelen_step=None): 

1464 """ 

1465 Write SED (wavelen, flambda, optional fnu) out to file. 

1466 

1467 Option of adding a header line (such as version info) to output file. 

1468 Does not alter self, regardless of grid or presence/absence of fnu. 

1469 """ 

1470 # This can be useful for debugging or recording an SED. 

1471 f = open(filename, 'w') 

1472 wavelen = self.wavelen 

1473 flambda = self.flambda 

1474 wavelen, flambda = self.resampleSED(wavelen, flambda, wavelen_min=wavelen_min, 

1475 wavelen_max=wavelen_max, 

1476 wavelen_step=wavelen_step) 

1477 # Then just use this gridded wavelen/flambda to calculate fnu. 

1478 # Print header. 

1479 if print_header is not None: 

1480 if not print_header.startswith('#'): 

1481 print_header = '# ' + print_header 

1482 f.write(print_header) 

1483 # Print standard header info. 

1484 if print_fnu: 

1485 wavelen, fnu = self.flambdaTofnu(wavelen, flambda) 

1486 print("# Wavelength(nm) Flambda(ergs/cm^s/s/nm) Fnu(Jansky)", file=f) 

1487 else: 

1488 print("# Wavelength(nm) Flambda(ergs/cm^s/s/nm)", file=f) 

1489 for i in range(0, len(wavelen), 1): 

1490 if print_fnu: 

1491 fnu = self.flambdaTofnu(wavelen=wavelen, flambda=flambda) 

1492 print(wavelen[i], flambda[i], fnu[i], file=f) 

1493 else: 

1494 print("%.2f %.7g" % (wavelen[i], flambda[i]), file=f) 

1495 # Done writing, close file. 

1496 f.close() 

1497 return 

1498 

1499 

1500# Bonus, functions for many-magnitude calculation for many SEDs with a single bandpass 

1501 

1502 def setupPhiArray(self, bandpasslist): 

1503 """ 

1504 Sets up a 2-d numpy phi array from bandpasslist suitable for input to Sed's manyMagCalc. 

1505 

1506 This is intended to be used once, most likely before using Sed's manyMagCalc many times on many SEDs. 

1507 Returns 2-d phi array and the wavelen_step (dlambda) appropriate for that array. 

1508 """ 

1509 # Calculate dlambda for phi array. 

1510 wavelen_step = bandpasslist[0].wavelen[1] - bandpasslist[0].wavelen[0] 

1511 wavelen_min = bandpasslist[0].wavelen[0] 

1512 wavelen_max = bandpasslist[0].wavelen[len(bandpasslist[0].wavelen)-1] 

1513 # Set up 

1514 phiarray = numpy.empty((len(bandpasslist), len(bandpasslist[0].wavelen)), dtype='float') 

1515 # Check phis calculated and on same wavelength grid. 

1516 i = 0 

1517 for bp in bandpasslist: 

1518 # Be sure bandpasses on same grid and calculate phi. 

1519 bp.resampleBandpass(wavelen_min=wavelen_min, wavelen_max=wavelen_max, wavelen_step=wavelen_step) 

1520 bp.sbTophi() 

1521 phiarray[i] = bp.phi 

1522 i = i + 1 

1523 return phiarray, wavelen_step 

1524 

1525 def manyFluxCalc(self, phiarray, wavelen_step, observedBandpassInd=None): 

1526 """ 

1527 Calculate fluxes of a single sed for which fnu has been evaluated in a 

1528 set of bandpasses for which phiarray has been set up to have the same 

1529 wavelength grid as the SED in units of ergs/cm^2/sec. It is assumed 

1530 that `self.fnu` is set before calling this method, and that phiArray 

1531 has the same wavelength grid as the Sed. 

1532 

1533 

1534 Parameters 

1535 ---------- 

1536 phiarray: `np.ndarray`, mandatory 

1537 phiarray corresponding to the list of bandpasses in which the band 

1538 fluxes need to be calculated, in the same wavelength grid as the SED 

1539 

1540 wavelen_step: `float`, mandatory 

1541 the uniform grid size of the SED 

1542 

1543 observedBandpassInd: list of integers, optional, defaults to None 

1544 list of indices of phiarray corresponding to observed bandpasses, 

1545 if None, the original phiarray is returned 

1546 

1547 

1548 Returns 

1549 ------- 

1550 `np.ndarray` with size equal to number of bandpass filters band flux 

1551 values in units of ergs/cm^2/sec 

1552 

1553 .. note: Sed.manyFluxCalc `assumes` phiArray has the same wavelenghth 

1554 grid as the Sed and that `sed.fnu` has been calculated for the sed, 

1555 perhaps using `sed.flambdaTofnu()`. This requires calling 

1556 `sed.setupPhiArray()` first. These assumptions are to avoid error 

1557 checking within this function (for speed), but could lead to errors if 

1558 method is used incorrectly. 

1559 

1560 Note on units: Fluxes calculated this way will be the flux density integrated over the 

1561 weighted response curve of the bandpass. See equaiton 2.1 of the LSST Science Book 

1562 

1563 http://www.lsst.org/scientists/scibook 

1564 """ 

1565 

1566 if observedBandpassInd is not None: 

1567 phiarray = phiarray[observedBandpassInd] 

1568 flux = numpy.empty(len(phiarray), dtype='float') 

1569 flux = numpy.sum(phiarray*self.fnu, axis=1)*wavelen_step 

1570 return flux 

1571 

1572 def manyMagCalc(self, phiarray, wavelen_step, observedBandpassInd=None): 

1573 """ 

1574 Calculate many magnitudes for many bandpasses using a single sed. 

1575 

1576 This method assumes that there will be flux within a particular bandpass 

1577 (could return '-Inf' for a magnitude if there is none). 

1578 Use setupPhiArray first, and note that Sed.manyMagCalc *assumes* 

1579 phiArray has the same wavelength grid as the Sed, and that fnu has 

1580 already been calculated for Sed. 

1581 These assumptions are to avoid error checking within this function (for 

1582 speed), but could lead to errors if method is used incorrectly. 

1583 Parameters 

1584 ---------- 

1585 phiarray: `np.ndarray`, mandatory 

1586 phiarray corresponding to the list of bandpasses in which the band 

1587 fluxes need to be calculated, in the same wavelength grid as the SED 

1588 

1589 wavelen_step: `float`, mandatory 

1590 the uniform grid size of the SED 

1591 

1592 observedBandpassInd: list of integers, optional, defaults to None 

1593 list of indices of phiarray corresponding to observed bandpasses, 

1594 if None, the original phiarray is returned 

1595 

1596 """ 

1597 fluxes = self.manyFluxCalc(phiarray, wavelen_step, observedBandpassInd) 

1598 mags = -2.5*numpy.log10(fluxes) - self.zp 

1599 return mags 

1600 

1601 

1602def read_close_Kurucz(teff, feH, logg): 

1603 """ 

1604 Check the cached Kurucz models and load the model closest to the input stellar parameters. 

1605 Parameters are matched in order of Teff, feH, and logg. 

1606 

1607 Parameters 

1608 ---------- 

1609 teff : float 

1610 Effective temperature of the stellar template. Reasonable range is 3830-11,100 K. 

1611 feH : float 

1612 Metallicity [Fe/H] of stellar template. Values in range -5 to 1. 

1613 logg : float 

1614 Log of the surface gravity for the stellar template. Values in range 0. to 50. 

1615 

1616 Returns 

1617 ------- 

1618 sed : Sed Object 

1619 The SED of the closest matching stellar template 

1620 paramDict : dict 

1621 Dictionary of the teff, feH, logg that were actually loaded 

1622 

1623 """ 

1624 global _global_lsst_sed_cache 

1625 

1626 # Load the cache if it hasn't been done 

1627 if _global_lsst_sed_cache is None: 

1628 cache_LSST_seds() 

1629 # Build an array with all the files in the cache 

1630 if not hasattr(read_close_Kurucz, 'param_combos'): 

1631 kurucz_files = [filename for filename 

1632 in _global_lsst_sed_cache if ('kurucz' in filename) & 

1633 ('_g' in os.path.basename(filename))] 

1634 kurucz_files = list(set(kurucz_files)) 

1635 read_close_Kurucz.param_combos = numpy.zeros(len(kurucz_files), 

1636 dtype=[('filename', ('|U200')), ('teff', float), 

1637 ('feH', float), ('logg', float)]) 

1638 for i, filename in enumerate(kurucz_files): 

1639 read_close_Kurucz.param_combos['filename'][i] = filename 

1640 filename = os.path.basename(filename) 

1641 if filename[1] == 'm': 

1642 sign = -1 

1643 else: 

1644 sign = 1 

1645 logz = sign*float(filename.split('_')[0][2:])/10. 

1646 read_close_Kurucz.param_combos['feH'][i] = logz 

1647 logg_temp = float(filename.split('g')[1].split('_')[0]) 

1648 read_close_Kurucz.param_combos['logg'][i] = logg_temp 

1649 teff_temp = float(filename.split('_')[-1].split('.')[0]) 

1650 read_close_Kurucz.param_combos['teff'][i] = teff_temp 

1651 read_close_Kurucz.param_combos = numpy.sort(read_close_Kurucz.param_combos, 

1652 order=['teff', 'feH', 'logg']) 

1653 

1654 # Lookup the closest match. Prob a faster way to do this. 

1655 teff_diff = numpy.abs(read_close_Kurucz.param_combos['teff'] - teff) 

1656 g1 = numpy.where(teff_diff == teff_diff.min())[0] 

1657 feH_diff = numpy.abs(read_close_Kurucz.param_combos['feH'][g1] - feH) 

1658 g2 = numpy.where(feH_diff == feH_diff.min())[0] 

1659 logg_diff = numpy.abs(read_close_Kurucz.param_combos['logg'][g1][g2] - logg) 

1660 g3 = numpy.where(logg_diff == logg_diff.min())[0] 

1661 fileMatch = read_close_Kurucz.param_combos['filename'][g1][g2][g3] 

1662 if numpy.size(fileMatch > 1): 

1663 warnings.warn('Multiple close files') 

1664 fileMatch = fileMatch[0] 

1665 

1666 # Record what paramters were actually loaded 

1667 teff = read_close_Kurucz.param_combos['teff'][g1][g2][g3][0] 

1668 feH = read_close_Kurucz.param_combos['feH'][g1][g2][g3][0] 

1669 logg = read_close_Kurucz.param_combos['logg'][g1][g2][g3][0] 

1670 

1671 # Read in the matching file 

1672 sed = Sed() 

1673 sed.readSED_flambda(fileMatch) 

1674 return sed, {'teff': teff, 'feH': feH, 'logg': logg} 

1675 

1676