#!/usr/bin/env Python3 import numpy as np from scipy import integrate from pdb import set_trace ''' @ 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 Observatory: def __init__(self, dT=0.2, dP=20, dH=0.02, dCO2=20, dl=0.001): self.dT = dT # in K self.dP = dP # in Pa self.dH = dH # relative humidity between 0 and 1 self.dCO2 = dCO2 # in ppm self.dl = dl # in micron def n_as(self, l): ''' Formula that calculate the refractive index for standard atmospheric conditions, i.e. 15 C, 101,325 Pa, 0% RH, 450 ppm CO2. It returns n_as, not (n_as - 1) * 1e8! ''' _s = 1 / l _k0 = 238.0185 # um^-2 _k1 = 5792105 # um^-2 _k2 = 57.362 # um^-2 _k3 = 167917 # um^-2 return (_k1 / (_k0 - _s**2) + _k3 / (_k2 - _s**2)) / 1e8 + 1 def n_axs(self, l, xc): ''' Formula that calculate the refractive index for standard atmospheric conditions, i.e. 15 C, 101,325 Pa, 0% RH, but with xc ppm of CO2. It returns n_axs, not (n_axs - 1) ''' _n_as = self.n_as(l) return ( (_n_as - 1) * (1 + 0.534e-6 * (xc - 450) ) ) + 1 def n_ws(self, l): ''' Formula that calculates the refractivity of air for standard moist conditions as defined by B&S (1939), i.e. 20 C, 1333 Pa Returns n_ws, not (n_ws - 1) * 1e8 ''' _s = 1 / l _cf = 1.022 _w0 = 295.235 # um^-2 _w1 = 2.6422 # um^-2 _w2 = -0.032380 # um^-4 _w3 = 0.004028 # um^-6 return (_cf * (_w0 + _w1 * _s**2 + _w2 * _s**4 + _w3 * _s**6) ) / 1e8 + 1 def f(self, p, T): ''' Enhancement factor of water vapour in air t in degrees celsius p in Pascal (Pa) ''' _t = T - 273.15 _a = 1.00062 _b = 3.14e-8 # Pa^-1 _c = 5.6e-7 # deg C^-2 return _a + _b * p + _c * _t**2 def svp(self, T): ''' Saturation vapour pressure in Pa. ''' _A = 1.2378847e-5 # K^-2 _B = -1.9121316e-2 # K^-2 _C = 33.93711047 _D = -6.3431645e3 # K^-2 return np.exp(_A*T**2 + _B*T + _C + _D/T) def svp2(self, T): ''' Calculation of svp from IAPWS formula, based on documentation by NIST toolbox. ''' K1 = 1.16705214528e+03 K2 = -7.24213167032e+05 K3 = -1.70738469401e+01 K4 = 1.20208247025e+04 K5 = -3.23255503223e+06 K6 = 1.49151086135e+01 K7 = -4.82326573616e+03 K8 = 4.05113405421e+05 K9 = -2.38555575678e-01 K10 = 6.50175348448e+02 _W = T + K9 / (T - K10) _A = _W**2 + K1*_W + K2 _B = K3*_W**2 + K4*_W + K5 _C = K6*_W**2 + K7*_W + K8 _X = -_B + np.sqrt(_B**2 - 4*_A*_C) return (2*_C / _X)**4 * 1e6 def xw(self, p, T, RH): ''' Molar fraction of water vapour in moist air. ''' _f = self.f(p, T) _svp = self.svp(T) return _f * RH * _svp / p def Z(self, p, T, RH, xw=''): ''' Compressibility of moist air ''' _a0 = 1.58123e-6 # K Pa^-1 _a1 = -2.9331e-8 # Pa^-1 _a2 = 1.1043e-10 # K^-1 Pa^-1 _b0 = 5.707e-6 # K Pa^-1 _b1 = -2.051e-8 # Pa^-1 _c0 = 1.9898e-4 # K Pa^-1 _c1 = -2.376e-6 # Pa^-1 _d = 1.83e-11 # K^2 Pa^-2 _e = -0.765e-8 # K^2 Pa^-2 _t = T - 273.15 if xw == '': _xw = self.xw(p, T, RH) else: _xw = xw Z = 1 - (p/T) * (_a0 + _a1*_t + _a2*_t**2 + (_b0 + _b1*_t)*_xw + (_c0 + _c1*_t)*_xw**2) + (p/T)**2 * (_d + _e*_xw**2) return Z def rho(self, p, T, RH, xc, xw=''): ''' Calculates the density of air for the given parameters. T in Kelvin p in Pascal RH between 0 and 1 xc in ppm ''' _Ma = 1e-3 * (28.9635 + 12.011e-6 * (xc - 400) ) # Molar mass in kg/mol of dry air containing xc ppm of CO2 _Mw = 0.018015 # kg/mol _R = 8.314510 # J mol^-1 K^-1, gas constant if xw == '': _xw = self.xw(p, T, RH) else: _xw = xw _Z = self.Z(p, T, RH, xw=_xw) return ( ( p * _Ma ) / (_Z * _R * T) ) * ( 1 - _xw * (1 - _Mw/_Ma)) def n_tph(self, l, T, p, RH, xc): ''' Calculates the refraction of the atmosphere for any conditions. ''' _n_a = self.n_axs(l, xc) _n_w = self.n_ws(l) _La = (_n_a**2 - 1) / (_n_a**2 + 2) _Lw = (_n_w**2 - 1) / (_n_w**2 + 2) # Calculate the density of the dry air components, using the partial pressure of the dry air, i.e. p_dry = p_atm - p_water _rho_a = self.rho(p=p-RH*self.svp(T), T=T, RH=0, xc=xc) # Dry air component for the actual conditions. _rho_axs = self.rho(p=101325, T=288.15, RH=0, xc=xc) # Dry air component at standard conditions. if RH == 0: # nan is returned for _rho_w if we put in a (partial) pressure of 0 _rho_w = 0 else: # Partial pressure of water vapour is p_water = h*svp _rho_w = self.rho(p=RH*self.svp(T), T=T, RH=RH, xc=xc) # Moist air component for the actual conditions. # _rho_ws = self.rho(p=1333, T=293.15, RH=1/1.7564043320974863, xc=xc) # Moist air component at standard conditions. _rho_ws = self.rho(p=1333, T=293.15, RH=1333/self.svp(293.15), xc=xc) # Moist air component at standard conditions. _Ltotal = ( _rho_a / _rho_axs ) * _La + ( _rho_w / _rho_ws ) * _Lw # print 'Dry air component = ', (_rho_a/_rho_axs)*(_n_a-1)*1e8 # print 'Wet component = ', ( _rho_w / _rho_ws ) * (_n_w - 1)*1e8 # print 'Sum of components = ', (_rho_a/_rho_axs)*(_n_a-1)*1e8+( _rho_w / _rho_ws ) * (_n_w - 1)*1e8 return np.sqrt( (1 + 2 * _Ltotal) / (1 - _Ltotal) ) def dn_tph(self, l, T, p, RH, xc, dl=None, dT=None, dP=None, dRH=None, dCO2=None): ''' Calculates the uncertainty in the refractive index, following the Ciddor 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. Parameters: l - 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