Source code for beast.physicsmodel.dust.extinction

"""
Extinction Curves
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import numpy as np
from scipy import interpolate, interp

from astropy import units

import dust_extinction.parameter_averages as dustext_par
import dust_extinction.averages as dustext_avg
from dust_extinction.helpers import _test_valid_x_range

from ...observationmodel import phot
from ...config import __ROOT__

__all__ = ['ExtinctionLaw', 'Cardelli89', 'Fitzpatrick99',
           'Gordon03_SMCBar', 'Gordon16_RvFALaw',
           'Generalized_RvFALaw', 'Generalized_DustExt']

libdir = __ROOT__


[docs]class ExtinctionLaw(object): """ Extinction Law Template Class Parameters ---------- Attributes ---------- name : string Name identifying the extinction law """ def __init__(self): self.name = 'None'
[docs] def function(self, lamb, *arg, **kwargs): """ function to provide extinction curve given wavelength(s) """ raise NotImplementedError
[docs] def inFilter(self, names, filterLib=None, *args, **kwargs): """ Calculates the extinction for a given filter band or filter color colors (e.g. U, U-B ...) Parameters ---------- names: str or list(str) or list(filters) filter names or filter instances to evaluate. a name can be a color such as 'U-B' filterLib: filepath path to the filter library hd5 file (default is the internal library) Returns ------- r: float or ndarray(dtype=float) attenuation value or array of values """ if (type(names) == str): if '-' in names: _filters = phot.load_filters(names.replace(' ', '').split('-'), interp=True, filterLib=filterLib) return float(self.function(_filters[0].cl, *args, **kwargs) - self.function(_filters[1].cl, *args, **kwargs)) else: _filters = phot.load_filters([names], interp=True, filterLib=filterLib) return float(self.function(_filters[0].cl, *args, **kwargs)) elif hasattr(names, '__iter__'): if (type(names[0]) == str): # assumes all entires are str lst = np.unique(' '.join(names).replace('-', ' ').split()) _filters = phot.load_filters(lst, interp=True, filterLib=filterLib) # in case of python 2.6xx, explicit loop d = {} for lk, fk in zip(lst, _filters): d[lk] = self.function(fk.cl, *args, **kwargs) return np.asarray([float(eval(lk, d)) for lk in names]) else: # assumes list of Filter instances return np.asarray([float(self.function(fk.cl, *args, **kwargs)) for fk in names])
[docs] def __call__(self, *args, **kwargs): return self.function(*args, **kwargs)
[docs] def isvalid(self, *args, **kwargs): """ Check if the current arguments are in the validity domain of the law Must be redefined if any restriction applies to the law """ return True
[docs]class Cardelli89(ExtinctionLaw): """ Cardelli89 Milky Way R(V) dependent Extinction Law From Cardelli, Clayton, and Mathis (1989, ApJ, 345, 245). Fitzpatrick99 should be used instead except for historical purposes as this newer law is based on 10x more observations and a better treatment of the optical/NIR photometry based portion of the curves. """ def __init__(self): self.name = 'Cardelli89' self.x_range = [0.3, 10.0] # inverse microns
[docs] def function(self, lamb, Av=1., Rv=3.1, Alambda=True, **kwargs): """ Cardelli89 extinction Law Parameters ---------- lamb: float or ndarray(dtype=float) wavelength [in Angstroms] at which to evaluate the law. Av: float desired A(V) (default: 1.0) Rv: float desired R(V) (default: 3.1) Alambda: bool if set returns +2.5*1./log(10.)*tau, tau otherwise Returns ------- r: float or ndarray(dtype=float) attenuation as a function of wavelength depending on Alambda option +2.5*1./log(10.)*tau, or tau """ # ensure the units are in angstrom _lamb = units.Quantity(lamb, units.angstrom).value if isinstance(_lamb, float) or isinstance(_lamb, np.float_): _lamb = np.asarray([lamb]) else: _lamb = lamb[:] # convert to wavenumbers x = 1.e4 / _lamb # check that the wavenumbers are within the defined range _test_valid_x_range(x, self.x_range, self.name) # init variables a = np.zeros(np.size(x)) b = np.zeros(np.size(x)) # Infrared (Eq 2a,2b) ind = np.where((x >= 0.3) & (x < 1.1)) a[ind] = 0.574 * x[ind] ** 1.61 b[ind] = -0.527 * x[ind] ** 1.61 # Optical & Near IR # Eq 3a, 3b ind = np.where((x >= 1.1) & (x < 3.3)) y = x[ind] - 1.82 a[ind] = (1. + 0.17699 * y - 0.50447 * y ** 2 - 0.02427 * y ** 3 + 0.72085 * y ** 4 + 0.01979 * y ** 5 - 0.77530 * y ** 6 + 0.32999 * y ** 7) b[ind] = (1.41338 * y + 2.28305 * y ** 2 + 1.07233 * y ** 3 - 5.38434 * y ** 4 - 0.62251 * y ** 5 + 5.30260 * y ** 6 - 2.09002 * y ** 7) # UV # Eq 4a, 4b ind = np.where((x >= 3.3) & (x <= 8.0)) a[ind] = 1.752 - 0.316 * x[ind] - 0.104 / ((x[ind] - 4.67)**2 + 0.341) b[ind] = -3.090 + 1.825 * x[ind] + 1.206 / ((x[ind] - 4.62)**2 + 0.263) ind = np.where((x >= 5.9) & (x <= 8.0)) y = x[ind] - 5.9 Fa = -0.04473 * y ** 2 - 0.009779 * y ** 3 Fb = 0.21300 * y ** 2 + 0.120700 * y ** 3 a[ind] = a[ind] + Fa b[ind] = b[ind] + Fb # Far UV # Eq 5a, 5b ind = np.where((x > 8.0) & (x <= 10.0)) # Fa = Fb = 0 y = x[ind] - 8. a[ind] = -1.073 - 0.628 * y + 0.137 * y ** 2 - 0.070 * y ** 3 b[ind] = 13.670 + 4.257 * y - 0.420 * y ** 2 + 0.374 * y ** 3 # Case of -values x out of range [0.289,10.0] ind = np.where((x > 10.0) | (x < 0.3)) a[ind] = 0.0 b[ind] = 0.0 # Return Extinction vector # Eq 1 if (Alambda): return((a + b / Rv)*Av) else: # return( 1./(2.5 * 1. / np.log(10.)) * ( a + b / Rv ) * Av) return(0.4 * np.log(10.) * (a + b / Rv) * Av)
[docs]class Fitzpatrick99(ExtinctionLaw): """ Fitzpatrick99 Milky Way R(V) dependent Extinction Law From Fitzpatrick (1999, PASP, 111, 63). R(V) dependent extinction curve that explicitly deals with optical/NIR extinction being measured from broad/medium band photometry. Code based on fm_unred.pro from the IDL astronomy library Extended to below 912 A using Draine et al. dust grain models. """ def __init__(self): self.name = 'Fitzpatrick99' self.x_range = [0.3, 10.0]
[docs] def function(self, lamb, Av=1, Rv=3.1, Alambda=True, draine_extend=False, **kwargs): """ Fitzpatrick99 extinction Law Parameters ---------- lamb: float or ndarray(dtype=float) wavelength [in Angstroms] at which to evaluate the law. Av: float desired A(V) (default 1.0) Rv: float desired R(V) (default 3.1) Alambda: bool if set returns +2.5*1./log(10.)*tau, tau otherwise draine_extend: bool if set extends the extinction curve to below 912 A Returns ------- r: float or ndarray(dtype=float) attenuation as a function of wavelength depending on Alambda option +2.5*1./log(10.)*tau, or tau """ # ensure the units are in angstrom _lamb = units.Quantity(lamb, units.angstrom).value if isinstance(_lamb, float) or isinstance(_lamb, np.float_): _lamb = np.asarray([lamb]) else: _lamb = lamb[:] # convert to wavenumbers x = 1.e4 / _lamb # check that the wavenumbers are within the defined range _test_valid_x_range(x, self.x_range, self.name) # initialize values c2 = -0.824 + 4.717 / Rv c1 = 2.030 - 3.007 * c2 c3 = 3.23 c4 = 0.41 x0 = 4.596 gamma = 0.99 k = np.zeros(np.size(x)) # compute the UV portion of A(lambda)/E(B-V) xcutuv = 10000.0 / 2700. xspluv = 10000.0 / np.array([2700., 2600.]) ind = np.where(x >= xcutuv) if np.size(ind) > 0: k[ind] = c1 + (c2 * x[ind]) + \ c3 * ((x[ind]) ** 2) / (((x[ind]) ** 2 - (x0 ** 2)) ** 2 + (gamma ** 2) * ((x[ind]) ** 2)) yspluv = (c1 + (c2 * xspluv) + c3 * ((xspluv) ** 2) / (((xspluv) ** 2 - (x0 ** 2)) ** 2 + (gamma ** 2) * ((xspluv) ** 2))) # FUV portion if not draine_extend: fuvind = np.where(x >= 5.9) k[fuvind] += c4 * (0.5392 * ((x[fuvind] - 5.9) ** 2) + 0.05644 * ((x[fuvind] - 5.9) ** 3)) k[ind] += Rv yspluv += Rv # Optical/NIR portion ind = np.where(x < xcutuv) if np.size(ind) > 0: xsplopir = np.zeros(7) xsplopir[0] = 0.0 xsplopir[1: 7] = 10000.0 / np.array([26500.0, 12200.0, 6000.0, 5470.0, 4670.0, 4110.0]) ysplopir = np.zeros(7) ysplopir[0: 3] = np.array([0.0, 0.26469, 0.82925]) * Rv / 3.1 ysplopir[3: 7] = np.array([np.poly1d([2.13572e-04, 1.00270, -4.22809e-01])(Rv), np.poly1d([-7.35778e-05, 1.00216, -5.13540e-02])(Rv), np.poly1d([-3.32598e-05, 1.00184, 7.00127e-01])(Rv), np.poly1d([1.19456, 1.01707, -5.46959e-03, 7.97809e-04, -4.45636e-05][::-1])(Rv)]) tck = interpolate.splrep(np.hstack([xsplopir, xspluv]), np.hstack([ysplopir, yspluv]), k=3) k[ind] = interpolate.splev(x[ind], tck) # convert from A(lambda)/E(B-V) to A(lambda)/A(V) k /= Rv # FUV portion from Draine curves if draine_extend: fuvind = np.where(x >= 5.9) tmprvs = np.arange(2., 6.1, 0.1) diffRv = Rv - tmprvs if min(abs(diffRv)) < 1e-8: dfname = libdir+'MW_Rv%s_ext.txt' % ("{0:.1f}".format(Rv)) l_draine, k_draine = np.loadtxt(dfname, usecols=(0, 1), unpack=True) else: add, = np.where(diffRv < 0.) Rv1 = tmprvs[add[0]-1] Rv2 = tmprvs[add[0]] dfname = libdir+'MW_Rv%s_ext.txt' % ("{0:.1f}".format(Rv1)) l_draine, k_draine1 = np.loadtxt(dfname, usecols=(0, 1), unpack=True) dfname = libdir+'MW_Rv%s_ext.txt' % ("{0:.1f}".format(Rv2)) l_draine, k_draine2 = np.loadtxt(dfname, usecols=(0, 1), unpack=True) frac = diffRv[add[0]-1]/(Rv2-Rv1) k_draine = (1. - frac)*k_draine1 + frac*k_draine2 dind = np.where((1./l_draine) >= 5.9) k[fuvind] = interp(x[fuvind], 1./l_draine[dind][::-1], k_draine[dind][::-1]) # setup the output if (Alambda): return(k * Av) else: return(k * Av * (np.log(10.) * 0.4))
[docs]class Gordon03_SMCBar(ExtinctionLaw): """ Gordon03 SMCBar extinction curve Average of 4 SMCBar extinction curves from Gordon et al. 2003 (ApJ, 594, 279). Note that Rv has no impact on this law: according to Gordon et al (2003), the average value of Rv is fixed to 2.74 +/- 0.13 """ def __init__(self): self.name = 'Gordon03_SMCBar' self.Rv = 2.74 self.x_range = [0.3, 10.0]
[docs] def function(self, lamb, Av=1, Rv=2.74, Alambda=True, draine_extend=False, **kwargs): """ Gordon03_SMCBar extinction law Parameters ---------- lamb: float or ndarray(dtype=float) wavelength [in Angstroms] at which to evaluate the law. Av: float desired A(V) (default 1.0) Rv: float desired R(V) (default 2.74) Alambda: bool if set returns +2.5*1./log(10.)*tau, tau otherwise Returns ------- r: float or ndarray(dtype=float) attenuation as a function of wavelength depending on Alambda option +2.5*1./log(10.)*tau, or tau """ # ensure the units are in angstrom _lamb = units.Quantity(lamb, units.angstrom).value if isinstance(_lamb, float) or isinstance(_lamb, np.float_): _lamb = np.asarray([lamb]) else: _lamb = lamb[:] # convert to wavenumbers x = 1.e4 / _lamb # check that the wavenumbers are within the defined range _test_valid_x_range(x, self.x_range, self.name) # set Rv explicitly to the fixed value Rv = self.Rv c1 = -4.959 / Rv c2 = 2.264 / Rv c3 = 0.389 / Rv c4 = 0.461 / Rv x0 = 4.6 gamma = 1.0 k = np.zeros(np.size(x)) # UV part xcutuv = 10000.0 / 2700. xspluv = 10000.0 / np.array([2700., 2600.]) ind = np.where(x >= xcutuv) if np.size(ind) > 0: k[ind] = 1.0 + c1 + (c2 * x[ind]) + c3 * ((x[ind]) ** 2) / \ (((x[ind]) ** 2 - (x0 ** 2)) ** 2 + (gamma ** 2) * ((x[ind]) ** 2)) yspluv = (1.0 + c1 + (c2 * xspluv) + c3 * ((xspluv) ** 2) / (((xspluv) ** 2 - (x0 ** 2)) ** 2 + (gamma ** 2) * ((xspluv) ** 2))) # FUV portion ind = np.where(x >= 5.9) if np.size(ind) > 0: if draine_extend: dfname = libdir+'SMC_Rv2.74_norm.txt' l_draine, k_draine = np.loadtxt(dfname, usecols=(0, 1), unpack=True) dind = np.where((1./l_draine) >= 5.9) k[ind] = interp(x[ind], 1./l_draine[dind][::-1], k_draine[dind][::-1]) else: k[ind] += c4 * (0.5392 * ((x[ind] - 5.9) ** 2) + 0.05644 * ((x[ind] - 5.9) ** 3)) # Opt/NIR part ind = np.where(x < xcutuv) if np.size(ind) > 0: xsplopir = np.zeros(9) xsplopir[0] = 0.0 xsplopir[1: 10] = 1.0 / np.array([2.198, 1.65, 1.25, 0.81, 0.65, 0.55, 0.44, 0.37]) # Values directly from Gordon et al. (2003) # ysplopir = np.array([0.0,0.016,0.169,0.131,0.567,0.801, # 1.00,1.374,1.672]) # K & J values adjusted to provide a smooth, # non-negative cubic spline interpolation ysplopir = np.array([0.0, 0.11, 0.169, 0.25, 0.567, 0.801, 1.00, 1.374, 1.672]) tck = interpolate.splrep(np.hstack([xsplopir, xspluv]), np.hstack([ysplopir, yspluv]), k=3) k[ind] = interpolate.splev(x[ind], tck) if (Alambda): return(k * Av) else: return(k * Av * (np.log(10.) * 0.4))
[docs]class Gordon16_RvFALaw(ExtinctionLaw): """ Gordon16 RvFA extinction law Mixture of Milky Way R(V) dependent extinction law and Gordon et al. (2003) SMCBar average extinction curve. This extinction curve model encompasses the average behavior of measured extinction curves in the Milky Way, LMC, and SMC. Implemented as a mixture between Fitzpatrick99 and Gordon03_SMCBar classes """ def __init__(self): self.ALaw = Fitzpatrick99() self.BLaw = Gordon03_SMCBar() self.name = 'Gordon16_RvFALaw' self.x_range = [np.max([self.ALaw.x_range[0], self.BLaw.x_range[0]]), np.min([self.ALaw.x_range[1], self.BLaw.x_range[1]]) ]
[docs] def function(self, lamb, Av=1, Rv=3.1, Alambda=True, f_A=0.5, **kwargs): """ Gordon16_RvFALaw Parameters ---------- lamb: float or ndarray(dtype=float) wavelength [in Angstroms] at which to evaluate the law. Av: float desired A(V) (default 1.0) Alambda: bool if set returns +2.5*1./log(10.)*tau, tau otherwise f_A: float set the mixture ratio between the two laws (default 0.5) Rv: float R(V) of mixture law (default to 3.1) Returns ------- r: float or ndarray(dtype=float) attenuation as a function of wavelength depending on Alambda option +2.5*1./log(10.)*tau, or tau """ # ensure the units are in angstrom _lamb = units.Quantity(lamb, units.angstrom).value # get the R(V) value for the A component Rv_A = self.get_Rv_A(Rv, f_A) # compute the two components k_A = self.ALaw.function(_lamb, Av=Av, Rv=Rv_A, Alambda=Alambda) k_B = self.BLaw.function(_lamb, Av=Av, Alambda=Alambda) # return the mixture return f_A*k_A + (1. - f_A)*k_B
[docs] def get_Rv_A(self, Rv, f_A=0.5): """ Calculate the R(V) of the A component given the R(V) of the mixture Rv_A is such that: 1 / Rv = f_A / Rv_A + (1 - f_A) / Rv_B Rv_A = 1. / (1. / (Rv * f_A) - (1. - f_A) / (f_A * Rv_B)) where Rv_B = 2.74 by definition (see Gordon03_SMCBar) Parameters ---------- Rv: float R(V) of the mixture law f_A: float Mixture ratio between the two components [default is 0.5] Returns ------- Rv_A : float R(V) of the A componet """ Rv_B = self.BLaw.Rv return 1. / (1. / (Rv * f_A) - (1. - f_A) / (f_A * Rv_B))
[docs] def get_Rv(self, Rv_A, f_A=0.5): """ Calculate the Rv of the mixture law given R(V) of the A component Rv_A is such that: 1 / Rv = f_A / Rv_A + (1 - f_A) / Rv_B where Rv_B = 2.74 by definition (see Gordon03_SMCBar) Parameters ---------- Rv_A: float R(V) of the A component f_A: float Mixture ratio between the two components [default is 0.5] Returns ------- Rv : float R(V) of the mixture """ Rv_B = self.BLaw.Rv return 1. / (f_A / Rv_A + (1 - f_A) / Rv_B)
[docs]class Generalized_RvFALaw(Gordon16_RvFALaw): """ Generalized RvFA extinction law Mixture of R(V) dependent extinction law (`ALaw`) and average extinction curve (`BLaw`). This extinction curve model encompasses the average behavior of measured extinction curves in the Milky Way, LMC, and SMC. Implemented as ALaw=Fitzpatrick99() and BLaw=Gordon03_SMCBar() by default. """ def __init__(self, ALaw=Fitzpatrick99(), BLaw=Gordon03_SMCBar()): self.ALaw = ALaw self.BLaw = BLaw self.name = 'Generalized_RvFALaw:'+ALaw.name+'+'+BLaw.name self.x_range = [np.max([self.ALaw.x_range[0], self.BLaw.x_range[0]]), np.min([self.ALaw.x_range[1], self.BLaw.x_range[1]]) ]
[docs]class Generalized_DustExt(ExtinctionLaw): """ Generalized extinction curve class to import classes from dust_extinction package. Accepts class name as string input (`curve`) for all `average` and Rv-dependent `parameter_averages` extinction curve classes. """ def __init__(self, curve='F04'): self.name = 'dustextpkg_'+curve if curve in dustext_par.__all__: self.extcurve_class = getattr(dustext_par, curve) self.hasRvParam = True elif curve in dustext_avg.__all__: self.extcurve_class = getattr(dustext_avg, curve) self.hasRvParam = False self.Rv = self.extcurve_class.Rv else: raise ValueError(curve + ' class not found. \n' + 'Valid dust_extinction package classes: '+ ' '.join(dustext_par.__all__+dustext_avg.__all__)) self.x_range = self.extcurve_class.x_range
[docs] def function(self, lamb, Av=1, Rv=3.1, Alambda=True, **kwargs): """ Generalized Extinction Law Parameters ---------- lamb: float or ndarray(dtype=float) wavelength [in Angstroms] at which to evaluate the law. Av: float desired A(V) (default 1.0) Rv: float desired R(V) (default 3.1) ignored if self.hasRvParam=False; defaults to self.Rv Alambda: bool if set returns +2.5*1./log(10.)*tau, tau otherwise Returns ------- r: float or ndarray(dtype=float) attenuation as a function of wavelength depending on Alambda option +2.5*1./log(10.)*tau, or tau """ # ensure the units are in angstrom _lamb = units.Quantity(lamb, units.angstrom).value if isinstance(_lamb, float) or isinstance(_lamb, np.float_): _lamb = np.asarray([lamb]) else: _lamb = lamb[:] if self.hasRvParam: extcurve_obj = self.extcurve_class(Rv=Rv) else: extcurve_obj = self.extcurve_class() if (Alambda): return extcurve_obj(_lamb*units.angstrom) * Av else: return extcurve_obj(_lamb*units.angstrom) * Av * (np.log(10.)*0.4)