#!/usr/bin/env Python3 import numpy as np from scipy import integrate import os from .ciddorModel import Observatory ''' @ Author: Joost van den Born @ Contact: born@astron.nl @ Description: This script offers a complete model of the atmospheric refraction and dispersion. It uses the Ciddor1996 paper to calculate the index of refraction for a wide range of conditions, with corresponding error propagation if needed. The actual refraction is calculated by any of the following models: - Plane parallel atmosphere - Cassini spherical homogeneous atmosphere - Other methods described in Corbard et al., 2019. ''' class dispersion(Observatory): def __init__(self, lat, h): Observatory.__init__(self) self.h = h # height above sea level in meters self.lat = lat # Latitude of the observer in degrees self.rc = self.set_rc() * 1000 # Radius of curvature of the earth at the observer def setReducedHeight(self, P, rho): self.H = self.set_H(P, rho) # Reduced height of the atmosphere assuming ideal gas law def planeParallel(self, n1, n2, zenith): ''' Refraction based on a flat atmosphere, easily derived from Snell's law. ''' _R1 = np.arcsin(n1 * np.sin( np.deg2rad(zenith) )) - np.deg2rad(zenith) _R2 = np.arcsin(n2 * np.sin( np.deg2rad(zenith) )) - np.deg2rad(zenith) return np.degrees(_R1) - np.degrees(_R2) def cassini(self, n1, n2, zenith, reduced_height=None): ''' Refraction of spherical atmosphere, derived from geometry using Sine law ''' if reduced_height: H = reduced_height else: H = self.H _R1 = np.arcsin(n1 * self.rc * np.sin(np.deg2rad(zenith)) / (self.rc + H)) - np.arcsin(self.rc * np.sin(np.deg2rad(zenith)) / (self.rc + H) ) _R2 = np.arcsin(n2 * self.rc * np.sin(np.deg2rad(zenith)) / (self.rc + H)) - np.arcsin(self.rc * np.sin(np.deg2rad(zenith)) / (self.rc + H) ) return np.degrees(_R1) - np.degrees(_R2) def tan5(self, n1, n2, zenith, reduced_height=None): ''' Same as oriani's theorem, but including a higher order term. Found in Corbard et al., 2019. ''' if reduced_height: H = reduced_height else: H = self.H _rList = [] for _n in [n1, n2]: _a = _n - 1 _b = H / self.rc _R = _a * (1 - _b) * np.tan( np.deg2rad(zenith) ) - _a * (_b - _a/2) * np.tan( np.deg2rad(zenith) )**3 + 3 * _a * (_b - _a/2)**2 * np.tan( np.deg2rad(zenith) )**5 _rList.append(np.degrees(_R)) return _rList[0] - _rList[1] def oriani(self, n1, n2, zenith, reduced_height=None): ''' Classic refraction formula with two tan(z) terms. This does not assume any information about the structure of the atmosphere. Found with Oriani's theorem. ''' if reduced_height: H = reduced_height else: H = self.H _rList = [] for _n in [n1, n2]: _a = _n - 1 _b = H / self.rc _R = _a * (1 - _b) * np.tan( np.deg2rad(zenith) ) - _a * (_b - _a/2) * np.tan( np.deg2rad(zenith) )**3 _rList.append(np.degrees(_R)) return _rList[0] - _rList[1] def corbard(self, n1, n2, zenith, reduced_height=None): ''' Corbard et al., 2019, mention an additional formula based on the error function. Oriani's theorem can be derived from this equation by 'keeping only the three first terms of its asymptotic expansion'. ''' if reduced_height: H = reduced_height else: H = self.H _rList = [] for _n in [n1, n2]: _a = _n - 1 _b = H / self.rc _R = _a * ( (2 - _a) / (np.sqrt( 2*_b - _a )) ) * np.sin( np.deg2rad(zenith) ) * self.psi( (np.cos(np.deg2rad(zenith))) / np.sqrt(2*_b - _a) ) _rList.append(np.degrees(_R)) return _rList[0] - _rList[1] def refractionIntegral(self, n1, n2, z0, R0=None, useStandardAtmosphere=True, heightData=None, rhoData=None): ''' Using data that gives temperature, pressure and density as a function of height, the gladstone-dale relation is invoked to calculate the path of a light ray along the atmosphere. This is allows us to also include atmospheric activity in different atmosphere layers. Recommended use with the US Standard Atmosphere from 1976. USSA1976 data generated from code supplied by http://www.pdas.com/atmos.html The method described in eq. 3 in Auer & Standish (2000) is used for the calculation. ''' if not R0: R0 = self.rc if useStandardAtmosphere: _datafileLocation = os.path.join(os.path.dirname(__file__), 'data/1976USSA.txt') _h, _, _, _, _Temp, _Press, _Dens,_,_,_ = np.genfromtxt(_datafileLocation, unpack=True, skip_header=6) else: _Dens = rhoData _h = heightData _rList = [] z0 = np.deg2rad(z0) # Integration of the refraction integral over the atmosphere layers. for _n in [n1, n2]: _Dens = np.asarray(_Dens) _h = np.asarray(_h) _R = R0 + _h * 1000 # Gladstone-Dale relation between refractive index and density _n_h = 1 + (_n - 1) / _Dens[0] * _Dens # Calculation of the zenith angle for given height and refractive index, from the refractive invariant _z = np.arcsin( (_n * R0) / (_n_h * _R) * np.sin(z0) ) # Calculation of log space derivative of n and r _lnn = np.log(_n_h) _lnr = np.log(_R) _dlnn = np.asarray([_lnn[i+1] - _lnn[i] for i in range(len(_lnn)-1)]) _dlnr = np.asarray([_lnr[i+1] - _lnr[i] for i in range(len(_lnr)-1)]) _dz = np.asarray([(_z[i] - _z[i+1]) for i in range(len(_z)-1)]) # Calculation of the integrand in eq. 3 in Auer & Standish _Integrand = -1*(_dlnn/_dlnr) / (1 + _dlnn/_dlnr) * _dz _rList.append(np.degrees(np.sum(_Integrand))) return _rList[0] - _rList[1] def psi(self, x): _f = lambda a: np.exp(-a**2) _int = integrate.quad(_f, x, np.inf) return np.exp(x**2) * _int[0] def set_rc(self): ''' Returns the radius of the curvature at the observer in km, taking into account the latitude of the observer and the corresponding ellipsoidality of the earth. ''' _A = 6378.137 # km _B = 6356.752 # km _phi = np.deg2rad(self.lat) _rc0 = (_A*_B)**2 / (_A**2 * np.cos(_phi)**2 + _B**2 * np.sin(_phi)**2)**(3/2) return _rc0 + self.h/1000 def set_H(self, P, rho): ''' Calculates the reduced height of the atmosphere, assuming ideal gas law. ''' _phi = np.deg2rad(self.lat) _c1 = 5.2790414e-3 _c2 = 2.32718e-5 _c3 = 1.262e-7 _c4 = 7e-10 _g0 = 9.780327 # m/s^2 _g0_local = _g0 * (1 + _c1 * np.sin(_phi)**2 + _c2 * np.sin(_phi)**4 + _c3 * np.sin(_phi)**6 + _c4 * np.sin(_phi)**8) _g = _g0_local - (3.0877e-6 - 4.3e-9 * np.sin(_phi)**2) * self.h + 7.2e-13 * self.h**2 return P / (rho * _g) def H_isotherm(self, T): ''' Calculates the reduced height of the atmosphere, assuming an isotherm distribution. ''' _kb = 1.38064852e-23 # kg m2 s-2 K-1 _m = 4.809651698e-26 # kg (weight per molecule of air, assuming dry air) _phi = np.deg2rad(self.lat) _c1 = 5.2790414e-3 _c2 = 2.32718e-5 _c3 = 1.262e-7 _c4 = 7e-10 _g0 = 9.780327 # m/s^2 _g0_local = _g0 * (1 + _c1 * np.sin(_phi)**2 + _c2 * np.sin(_phi)**4 + _c3 * np.sin(_phi)**6 + _c4 * np.sin(_phi)**8) return (_kb*T) / (_m * _g0_local) def cassiniError(self, z, l1, l2, T, p, RH, xc, dl1=0, dl2=0, dT=0.2, dP=20, dRH=0.02, dCO2=20, dz=0, lat=None, h=None): ''' Calculates the uncertainty in the atmospheric dispersion, following the Cassini model, by using fully propagated error analysis. This involves almost endless use of the chain rule and evaluation of both the moist air and dry air components. Note: dry air does not contain any water, i.e. H=0, x_w=0, etc. Not taken into account is the errror propagation in the scale height of the atmosphere or the zenith error, from my results this should give a small or even negligible difference. Parameters: z - Zenith angle in degrees l1 - Bluest wavelength in micron (0.4 - 2.5 micron) l2 - Reddest wavelength in micron (0.4 - 2.5 micron) T - Temperature in Kelvin p - Pressure in Pascals RH - Relative humidity (no units, don't use percentage but value 0