Source code for pyExtinction.AtmosphericExtinction

#!/usr/bin/env python
# -*- coding: utf-8 -*-
###############################################################################
# Filename:          AtmosphericExtinction.py
# Copyright          2012, Clément Buton, Yannick Copin
# Author:            Clément Buton <buton@physik.uni-bonn.de>
# Author:            $Author: ycopin $
# Version:           $Revision: 1.11 $
# Modified at:       $Date: 2017/01/05 10:28:59 $
# $Id: AtmosphericExtinction.py,v 1.11 2017/01/05 10:28:59 ycopin Exp $
###############################################################################

"""
.. _module:

AtmosphericExtinction (module)
==============================
"""

import os
import numpy as N
import astropy.io.fits as F

__author__ = "Yannick Copin <y.copin@ipnl.in2p3.fr>, " \
             "Clément Buton <c.buton@ipnl.in2p3.fr>"
__version__ = '$Id: AtmosphericExtinction.py,v 1.11 2017/01/05 10:28:59 ycopin Exp $'

# Data ==============================

# Default ozone template
O3Template = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                          'data/ozoneTemplate.fits')

EXT2OPT = .92103403719761834  # LOG10/2.5 = Extinction to opt. thickness

# Classes ======================================================================


[docs]class ExtinctionModel(object): def __init__(self, lbda=None, ozoneTemplate=None, lrefAero=1e4): """ Extinction model, from: :param lbda: wavelength vector [AA] (default to extended optical range) :param ozoneTemplate: name of the ozone template table (see :func:`readOzoneTemplate`). By default, use the provided ozone template. :param lrefAero: aerosol reference wavelength [AA] """ if lbda is None: self.lbda = N.arange(3200, 10001, 10, dtype=float) else: self.lbda = N.asanyarray(lbda) # Wavelength [AA] # Rayleigh extinction template [mag/airmass] for a pressure of 1 mbar self.rayleigh = self.rayleigh_HT74(self.lbda, 1.) # Ozone extinction [mag/airmass] self.ozoneName = ozoneTemplate if ozoneTemplate else O3Template # Read transmission ozone template, interpolate at input # wavelengthes, and convert to extinction [mag/airmass] for an # ozone column density of 1 DU self.ozone, self.ozoneNorm = readOzoneTemplate(self.ozoneName, self.lbda) self.ozone /= self.ozoneNorm # Aerosols self.lrefAero = lrefAero # Aerosol reference wavelength [AA] self.lbdaN = self.lbda / self.lrefAero def __str__(self): s = """\ Wavelength domain: %(m).1f-%(M).1f A by step of %(s).1f A (%(n)d px) Ozone template: %(template)s (%(column)s DU) Aerosol reference wavelength: %(l).0f A """ % dict(m=self.lbda[0], M=self.lbda[-1], s=self.lbda[1]-self.lbda[0], n=len(self.lbda), template=self.ozoneName, column=self.ozoneNorm, l=self.lrefAero) if hasattr(self, 'p'): p, o3, tau, ang = self.p dp, do3, dtau, dang = self.dp s += """\ Input extinction parameters: Pressure: %(p).0f +/- %(dp).0f mbar Ozone: %(o).0f +/- %(do).0f DU Aerosols: optical depth @ refLbda: %(t).2g +/- %(dt).2g angstrom exponent: %(a).2f +/- %(da).2f """ % dict(p=p, dp=dp, o=o3, do=do3, t=tau, dt=dtau, a=ang, da=dang) else: s += """\ Input extinction parameters: not set yet """ return s
[docs] def setParams(self, pars, dpars=None): """ Set physical extinction parameters: pressure, ozone column density [Dobson units], aerosol optical depth at reference wavelength and aerosol angstrom exponent. :param pars: extinction parameters (*pr*, *oi*, *ai*, *ap*) where: - *pr*: surface pressure [mbar] - *oi*: ozone intensity [Dobson units] - *ai*: aerosol optical depth at reference wavelength - *ap*: aerosol angstrom exponent :param dpars: associated standard errors The total atmospheric extinction will then be the sum of three components: - Rayleigh extinction: `pr[mbar] * HT74(1 mbar)` - Ozone extinction: `oi[DU] * OzoneTemplate(1 DU)` - Aerosol extinction: `ai/EXT2OPT * (lbda/lRef)**(-ap)` .. Note:: if `ndim(dpars)==2`, `dpars` is considered as the *covariance* matrix of input extinction parameters. Therefore, when `ndim(dpars)==1`, `self.extinctionErrors(pars, dpars)` is equivalent to `self.extinctionErrors(pars, N.diag(dpars)**2)` (note the square power). """ self.p = N.asanyarray(pars) if dpars is None: self.dp = N.zeros(4) else: self.dp = N.asanyarray(dpars)
[docs] def setDefaultParams(self, location='Mauna Kea'): """ Set default physical extinction parameters from predefined location. :param location: predefined location. ================================= ================= Parameter Value ± Error ================================= ================= *Mauna Kea* ---------------------------------------------------- Pressure 616 ± 2 mbar Ozone column 257 ± 23 DU Aerosols optical depth @ 1 micron 0.0076 ± 0.0014 Aerosols angstrom exponent 1.26 ± 1.33 ================================= ================= """ if location == "Mauna Kea": o3, do3 = 257., 23. # Ozone column density [DU] ang, dang = 1.26, 1.33 # Ångström exponent tau, dtau = 7.6e-3, 1.4e-3 # Aerosol optical depth at 1 micron p, dp = 616., 2. # Surface pressure [mbar] else: raise ValueError("Unknown location '%s'" % location) self.setParams([ p, o3, tau, ang], dpars=[dp, do3, dtau, dang])
[docs] def extinctionComponents(self): """ Compute extinction individual components from extinction parameters (see :meth:`setParams`) :return: extinction components 2D-array [rayleigh,ozone,aerosols] """ return N.array([ self.p[0] * self.rayleigh, # Rayleigh component self.p[1] * self.ozone, # Ozone component self.p[2] / EXT2OPT * self.lbdaN**(-self.p[3]), # Aerosols component ])
[docs] def extinctionErrors(self): """ Compute total extinction (diagonal) standard error from extinction parameters and associated standard errors (see :meth:`setParams`) :return: total extinction standard error """ jac = self.jac() if N.ndim(self.dp) == 1: # dp is a vector of std (independant) errors vExt = N.dot(self.dp**2, jac**2) elif N.ndim(self.dp) == 2: # dp is actually a covariance matrix vExt = N.dot(N.dot(jac.T, self.dp), jac).diagonal() return N.sqrt(vExt)
[docs] def extinction(self, pars=None, dpars=None, components=False): """ Compute total extinction (and associated standard error) from extinction parameters (and associated standard errors). :param pars: extinction parameters (see :meth:`setParams`) :param dpars: extinction parameter errors (see :meth:`setParams`) :param components: return individual extinction components if True :return: 2D-array [ext,dext,[components]] """ if None not in (pars, dpars): self.setParams(pars, dpars) comp = self.extinctionComponents() # (ncomp,nlbda) ext = comp.sum(axis=0) # (nlbda,) dext = self.extinctionErrors() # (nlbda,) if not components: # Return [lbda,ext,dext] return N.vstack((ext, dext)) else: # Return individual components as well return N.vstack((ext, dext, comp))
[docs] def jac(self): """Jacobian of total extinction with respect to extinction parameters. :return: jacobian 2D-array (nparam=4,nlbda) """ jac = N.empty((len(self.p), len(self.lbda)), 'd') jac[0] = self.rayleigh # dext/dP jac[1] = self.ozone # dext/do3 jac[2] = self.lbdaN**(-self.p[3]) / EXT2OPT # dext/dtau jac[3] = -self.p[2] * jac[2] * N.log(self.lbdaN) # dext/dang return jac
@staticmethod
[docs] def rayleigh_HT74(lbda, pressure): """ Rayleigh extinction from `Hansen & Travis (1974) <http://cdsads.u-strasbg.fr/abs/1974SSRv...16..527H>`_. :param lbda: wavelength vector [AA] :param pressure: effective surface pressure [mbar] :return: Rayleigh extinction [mag/airmass] """ lm = lbda * 1e-4 # Wavelength from A to microns # Optical depth tau = 0.008569 / lm**4 * (1 + 0.0113 / lm**2 + 0.00013 / lm**4) tau *= pressure / 1013.25 return tau / EXT2OPT # Convert to attenuation [mag/airmass]
[docs] def write(self, outname, ext=None, format='txt'): """ Write extinction curve in output file. :param outname: output filename :param ext: explicit extinction curve(s) to be written out :param format: output file format ('txt' or 'fits') """ if ext is None: if hasattr(self, 'p'): ext = self.extinction(components=True) else: raise RuntimeError("Cannot evaluate extinction " "without extinction parameters") if format == 'txt': # ASCII table ext = N.absolute(ext.round(6)) # Avoid rounding imprecisions outFile = open(outname, 'w') # Header outFile.write('\n# '.join([''] + str(self).split('\n')) + '\n') outFile.write('# Reference: Buton et al. (2013A&A...549A...8B)\n') outFile.write('# Wavelength in AA\n') outFile.write('# Extinctions in mag/airmass\n') outFile.write('# lbda Ext dExt Ray O3 Aero \n') # Values for l, e, de, r, o, a in zip(self.lbda, ext[0], ext[1], ext[2], ext[3], ext[4]): outFile.write(' %5d %.3f %.3f %.3f %.3f %.3f\n' % (l, e, de, r, o, a)) outFile.close() elif format == 'fits': # FITS table p, o3, tau, ang = self.p dp, do3, dtau, dang = self.dp keywords = [ # Generic keywords ('EXTMODEL', "Rayleigh+Ozone+Aerosols", "Extinction model"), ('EXTREF', "Buton et al. (2013A&A...549A...8B)", "Bibliographical ref."), # Extinction parameters and errors ('RA_P', p, "Surface pressure [mbar]"), ('RA_DP', dp, "Pressure stddev [mbar]"), ('OZ_INT', o3, "Ozone intensity [DU]"), ('OZ_DINT', do3, "Ozone intensity stddev [DU]"), ('AE_TAU', tau, "Aerosol optical depth"), ('AE_DTAU', dtau, "Aerosol optical depth stddev"), ('AE_ANG', ang, "Aerosol Angstrom exponent"), ('AE_DANG', dang, "Aerosol Angstrom exponent stddev"), ('AE_LREF', self.lrefAero, "Aerosol ref. wavelength [AA]"), ] # Extinction table arrays = [self.lbda, ext[0], ext[1], ext[2], ext[3], ext[4]] names = ['LAMBDA', 'EXT', 'DEXT', 'RAYLEIGH', 'OZONE', 'AEROSOLS'] units = ['Angstrom'] + ['mag/airmass']*5 # Extinction BinTableHDU, with keywords table = createTable(arrays, names, units=units, keywords=keywords, extname='EXTINCTION') table.writeto(outname, clobber=True) else: raise IOError("Unknown output format '%s'" % format)
[docs] def plot(self, ext=None, ax=None, components=True, transmission=False): """ Plot the atmospheric extinction/transmission and its physical components. :param ext: extinctions to be plotted (or None) :param ax: matplotlib Axes instance (or None) :param components: display individual components if True :param transmission: display transmission rather than extinction :return: matplotlib Axes instance """ if ext is None: if hasattr(self, 'p'): ext = self.extinction(components=True) else: raise RuntimeError("Cannot evaluate extinction " "without extinction parameters") p, o3, tau, ang = self.p # Non-default colors blue, red, green, orange = ('#0066CC', '#CC0033', '#009966', '#FF9900') if transmission: # Extinction [mag/airmass] -> Transmission title = "SNfactory atmospheric transmission" ylbl = "Transmission" ext[0] = 10**(-0.4 * ext[0]) # Total transmission ext[1] *= -EXT2OPT * ext[0] # Error on total transmission ext[2:] = 10**(-0.4 * ext[2:]) # Component transmissions else: title = "SNfactory atmospheric extinction" ylbl = "Extinction [mag/airmass]" title += " (Buton et al., 2013A&A...549A...8B)" if ax is None: # Create a default axes import matplotlib.pyplot as P fig = P.figure(figsize=(8, 5)) ax = fig.add_subplot(1, 1, 1, title=title, xlabel=u"Wavelength [Å]", xlim=(self.lbda[0], self.lbda[-1]), ylabel=ylbl) # ax.ticklabel_format(style='plain') # Total extinction and errorband ax.plot(self.lbda, ext[0], color=green, lw=2, label='Total') xp, yp = P.mlab.poly_between(self.lbda, ext[0] - ext[1], ext[0] + ext[1]) ax.fill(xp, yp, alpha=0.3, fc=green, ec=green, label='_') if components: # Physical components ax.plot(self.lbda, ext[2], color=red, ls='--', label='Rayleigh [%.0f mbar]' % p) ax.plot(self.lbda, ext[3], color=blue, ls=':', label='Ozone [%.0f DU]' % o3) ax.plot(self.lbda, ext[4], color=orange, ls='-.', label=u'Aerosols [τ=%.4f, å=%.2f]' % (tau, ang)) ax.legend(loc='best', frameon=False) return ax
# Functions ==================================================================
[docs]def readOzoneTemplate(ozoneName, lbda, colLbda='LAMBDA', colTrans='OZONE', ext=1): """ Read ozone transmission template, interpolate over wavelengthes, and convert to extinction [mag/airmass]. :param ozoneName: input FITS table, with columns *colLbda* (wavelength in AA) and *colTrans* (fractional transmission), and key 'REFO3COL' specifing the reference ozone column density [DU] :param lbda: output wavelengthes [AA] :param colLbda: name of the wavelength (in AA) column :param colTrans: name of the ozone transmission column :param ext: extension in which to look for wavelength and transmission columns :return: ozone extinction [mag/airmass], refO3col """ # Read wavelength and transmission columns ffile = F.open(ozoneName) x = ffile[ext].data.field(colLbda) # Wavelength y = ffile[ext].data.field(colTrans) # Transmission refO3col = ffile[ext].header["REFO3COL"] # Interpolate transmissions over lbda from scipy.interpolate import UnivariateSpline trans = UnivariateSpline(x, y, s=0)(lbda) # Convert to extinction [mag/airmass] return N.absolute(-2.5 * N.log10(trans)), refO3col
[docs]def createTable(arrays, names, units=None, formats=None, keywords=(), extname=None): """ Create a FITS-table from a set of arrays. :param arrays: list of input arrays (ncols,) :param names: list of column names (ncols,) :param units: list of column units (ncols,) ('none' by default) :param formats: list of column formats (ncols,) ('1E' by default) :param keywords: list of keys (key,val[,comment]) to add to table header :param extname: name of the binary table extension :return: table HDU """ assert len(arrays) == len(names) for arr in arrays[1:]: assert len(arr) == len(arrays[0]) if units is None: units = ['none'] * len(arrays) else: assert len(units) == len(arrays) if formats is None: formats = ['1E'] * len(arrays) else: assert len(formats) == len(arrays) cols = [ F.Column(array=a, name=n, unit=u, format=f) for a, n, u, f in zip(arrays, names, units, formats) ] thdu = F.new_table(cols) if extname is not None: thdu.header['EXTNAME'] = extname thdu.header['FCLASS'] = (26, 'Table file class') for key, val, cmt in keywords: # Add some keywords if any thdu.header[key] = (val, cmt) return thdu
# End of AtmosphericExtinction.py ==============================================