Coverage for python/lsst/sims/photUtils/Sed.py : 6%

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#
23"""
24sed -
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
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.
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).
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.
83"""
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
107# since Python now suppresses DeprecationWarnings by default
108warnings.filterwarnings("default", category=DeprecationWarning, module='lsst.sims.photUtils.Sed')
111__all__ = ["Sed", "cache_LSST_seds", "read_close_Kurucz"]
114_global_lsst_sed_cache = None
116# a cache for ASCII files read-in by the user
117_global_misc_sed_cache = None
120class SedCacheError(Exception):
121 pass
124class sed_unpickler(pickle.Unpickler):
126 _allowed_obj = (("numpy", "ndarray"),
127 ("numpy", "dtype"),
128 ("numpy.core.multiarray", "_reconstruct"))
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
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")
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)
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.
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).
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")
193 return
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')
204 dtype = numpy.dtype([('wavelen', float), ('flambda', float)])
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)
222 if not ss_cache == ss_uncache:
223 raise SedCacheError(msg)
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
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
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)])
244 sub_dir_list = ['agnSED', 'flatSED', 'ssmSED', 'starSED', 'galaxySED']
246 cache = {}
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')])
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")
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]
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
280 print('\n')
282 with open(os.path.join(cache_dir, cache_name), "wb") as file_handle:
283 pickle.dump(cache, file_handle)
285 print('LSST SED cache saved to:\n')
286 print('%s' % os.path.join(cache_dir, cache_name))
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))
293 return cache
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.
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.
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.
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.
312 Parameters (optional)
313 ---------------------
314 wavelen_min a float
316 wavelen_max a float
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
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 """
328 global _global_lsst_sed_cache
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')
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
344 if not os.path.exists(cache_dir):
345 os.mkdir(cache_dir)
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
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()
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
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
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])
402 _global_lsst_sed_cache = new_cache
404 return
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.
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
423 self._physParams = PhysicalParameters()
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
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
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
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
473 return True
475 def __ne__(self, other):
476 return not self.__eq__(other)
478 # Methods for getters and setters.
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.
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
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
521 if wavelen_max is None:
522 wavelen_max = self._physParams.maxwavelen
524 if wavelen_step is None:
525 wavelen_step = self._physParams.wavelenstep
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
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).
537 Does not resample wavelen/flambda onto grid; leave fnu=None.
538 """
539 global _global_lsst_sed_cache
540 global _global_misc_sed_cache
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
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]
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]
564 if cached_source is not None:
565 sourcewavelen = numpy.copy(cached_source[0])
566 sourceflambda = numpy.copy(cached_source[1])
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
589 sourcewavelen = data['wavelen']
590 sourceflambda = data['flambda']
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))
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
607 def readSED_fnu(self, filename, name=None):
608 """
609 Read a file containing [lambda Fnu] (lambda in nm) (Fnu in Jansky).
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
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
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
674 # Methods that update or change self.
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
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.
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
702 # Utilities common to several later methods.
704 def _checkUseSelf(self, wavelen, flux):
705 """
706 Simple utility to check if should be using self's data or passed arrays.
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
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
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.
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)
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
812 def flambdaTofnu(self, wavelen=None, flambda=None):
813 """
814 Convert flambda into fnu.
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
841 def fnuToflambda(self, wavelen=None, fnu=None):
842 """
843 Convert fnu into flambda.
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
868 # methods to alter the sed
870 def redshiftSED(self, redshift, dimming=False, wavelen=None, flambda=None):
871 """
872 Redshift an SED, optionally adding cosmological dimming.
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
906 def setupCCMab(self, wavelen=None):
907 """
908 Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen).
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.
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)
919 return self.setupCCM_ab(wavelen=wavelen)
921 def setupCCM_ab(self, wavelen=None):
922 """
923 Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen).
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.
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
981 def setupODonnell_ab(self, wavelen=None):
982 """
983 Calculate a(x) and b(x) for O'Donnell dust model. (x=1/wavelen).
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.
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
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.
1044 Get a_x and b_x either from setupCCMab or setupODonnell_ab
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)
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.
1057 Get a_x and b_x either from setupCCMab or setupODonnell_ab
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)
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.
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
1107 def multiplySED(self, other_sed, wavelen_step=None):
1108 """
1109 Multiply two SEDs together - flambda * flambda - and return a new sed object.
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 """
1115 if wavelen_step is None:
1116 wavelen_step = self._physParams.wavelenstep
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
1143 # routines related to magnitudes and fluxes
1145 def calcADU(self, bandpass, photParams, wavelen=None, fnu=None):
1146 """
1147 Calculate the number of adu from camera, using sb and fnu.
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).
1153 @param [in] bandpass is an instantiation of the Bandpass class
1155 @param [in] photParams is an instantiation of the
1156 PhotometricParameters class that carries details about the
1157 photometric response of the telescope.
1159 @param [in] wavelen (optional) is the wavelength grid in nm
1161 @param [in] fnu (optional) is the flux in Janskys
1163 If wavelen and fnu are not specified, this will just use self.wavelen and
1164 self.fnu
1166 """
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
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 """
1194 return numpy.power(10.0, -0.4*(mag + self.zp))
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 """
1202 return -2.5*numpy.log10(flux) - self.zp
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
1212 \int self.flambda(lambda) * bandpass.sb(lambda) * dlambda
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.
1222 Parameters
1223 ----------
1224 bandpass is an instantiation of the Bandpass class
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)
1234 dlambda = wavelen[1]-wavelen[0]
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
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.
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.
1257 Passed wavelen/fnu arrays will be unchanged, but if uses self will check if fnu is set.
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
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).
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
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.
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
1327 def multiplyFluxNorm(self, fluxNorm, wavelen=None, fnu=None):
1328 """
1329 Multiply wavelen/fnu (or self.wavelen/fnu) by fluxnorm.
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
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.
1365 Can normalized in flambda or fnu values. wavelen_step specifies the wavelength spacing
1366 when using 'gap'.
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.
1375 if wavelen_step is None:
1376 wavelen_step = self._physParams.wavelenstep
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
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.
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
1500# Bonus, functions for many-magnitude calculation for many SEDs with a single bandpass
1502 def setupPhiArray(self, bandpasslist):
1503 """
1504 Sets up a 2-d numpy phi array from bandpasslist suitable for input to Sed's manyMagCalc.
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
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.
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
1540 wavelen_step: `float`, mandatory
1541 the uniform grid size of the SED
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
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
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.
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
1563 http://www.lsst.org/scientists/scibook
1564 """
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
1572 def manyMagCalc(self, phiarray, wavelen_step, observedBandpassInd=None):
1573 """
1574 Calculate many magnitudes for many bandpasses using a single sed.
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
1589 wavelen_step: `float`, mandatory
1590 the uniform grid size of the SED
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
1596 """
1597 fluxes = self.manyFluxCalc(phiarray, wavelen_step, observedBandpassInd)
1598 mags = -2.5*numpy.log10(fluxes) - self.zp
1599 return mags
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.
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.
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
1623 """
1624 global _global_lsst_sed_cache
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'])
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]
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]
1671 # Read in the matching file
1672 sed = Sed()
1673 sed.readSED_flambda(fileMatch)
1674 return sed, {'teff': teff, 'feH': feH, 'logg': logg}