├── readme.txt ├── def_legendre_polynomial.py ├── def_single_scattering_up.py ├── def_single_scattering_down.py ├── def_source_function_integrate_down.py ├── def_gauss_zeroes_weights.py ├── def_source_function_integrate_up.py ├── test_gsit_R&A_srf.txt ├── def_schmidt_polynomial.py ├── gsit.py └── def_gauss_seidel_iterations_m.py /readme.txt: -------------------------------------------------------------------------------- 1 | Python source code for: 2 | S.Korkin, A.M.Sayer, A.Ibrahim, and A.Lyapustin, 3 | "A practical guide to writing a radiative transfer code", 4 | Computer Physics Communications, COMPHY-D-21-00145 (2021) 5 | -------------------------------------------------------------------------------- /def_legendre_polynomial.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from numba import jit 4 | # 5 | @jit(nopython=True, cache=True) 6 | def legendre_polynomial(x, kmax): 7 | ''' 8 | Task: 9 | To compute the Legendre polynomials, Pk(x), for all orders k=0:kmax and a 10 | single point 'x' within [-1:+1] 11 | In: 12 | x f abscissa 13 | kmax i maximum order, k = 0,1,2...kmax 14 | Out: 15 | pk [kmax+1] Legendre polynomials 16 | Tree: 17 | - 18 | Notes: 19 | The Bonnet recursion formula [1, 2]: 20 | 21 | (k+1)P{k+1}(x) = (2k+1)*P{k}(x) - k*P{k-1}(x), (1) 22 | 23 | where k = 0:K, P{0}(x) = 1.0, P{1}(x) = x. 24 | For fast summation over k, this index changes first. 25 | Refs: 26 | 1. https://en.wikipedia.org/wiki/Legendre_polynomials 27 | 2. http://mathworld.wolfram.com/LegendrePolynomial.html 28 | 3. https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.legendre.html 29 | 4. https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.eval_legendre.html 30 | ''' 31 | nk = kmax+1 32 | pk = np.zeros(nk) 33 | if kmax == 0: 34 | pk[0] = 1.0 35 | elif kmax == 1: 36 | pk[0] = 1.0 37 | pk[1] = x 38 | else: 39 | pk[0] = 1.0 40 | pk[1] = x 41 | for ik in range(2, nk): 42 | pk[ik] = (2.0 - 1.0/ik)*x*pk[ik-1] - (1.0 - 1.0/ik)*pk[ik-2] 43 | return pk 44 | #============================================================================== 45 | # 46 | if __name__ == "__main__": 47 | # 48 | # 49 | mu = np.linspace(-1.0, 1.0, 1001) 50 | nmu = len(mu) 51 | kmax = 2000 52 | pkmu = np.zeros((nmu, kmax+1)) 53 | time_start = time.time() 54 | for imu in range(nmu): 55 | pkmu[imu, :] = legendre_polynomial(mu[imu], kmax) 56 | time_end = time.time() 57 | # 58 | print("polleg = %.3f sec."%(time_end-time_start)) 59 | #============================================================================== -------------------------------------------------------------------------------- /def_single_scattering_up.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from numba import jit 4 | from def_legendre_polynomial import legendre_polynomial 5 | # 6 | @jit(nopython=True, cache=True) 7 | def single_scattering_up(mu, mu0, azr, tau0, xk): 8 | ''' 9 | Task: 10 | To compute single scattering at top of a homogeneous atmosphere. 11 | In: 12 | mu d cos(vza_up) < 0 13 | mu0 d cos(sza) > 0 14 | azr d[naz] relative azimuths in radians; naz = len(azr) 15 | tau0 d total atmosphere optical thickness 16 | xk d[nk] expansion moments * ssa/2, (2k+1) included, nk=len(xk) 17 | Out: 18 | I11up d Itoa=f(mu, mu0, azr) 19 | Tree: 20 | - 21 | Note: 22 | TOA scaling factor = 2pi; 23 | Refs: 24 | 1. - 25 | ''' 26 | # 27 | # Parameters: 28 | nk = len(xk) 29 | # 30 | smu = np.sqrt(1.0 - mu*mu) 31 | smu0 = np.sqrt(1.0 - mu0*mu0) 32 | nu = mu*mu0 + smu*smu0*np.cos(azr) 33 | p = np.zeros_like(nu) 34 | for inu, nui in enumerate(nu): 35 | pk = legendre_polynomial(nui, nk-1) 36 | p[inu] = np.dot(xk, pk) 37 | # 38 | mup = -mu 39 | I11up = p*mu0/(mu0 + mup)*(1.0 - np.exp(-tau0/mup - tau0/mu0)) 40 | # 41 | return I11up 42 | #============================================================================== 43 | # 44 | if __name__ == "__main__": 45 | # 46 | tau0 = 1.0/3.0 47 | xk = np.array([1.0, 0.0, 0.5]) 48 | mu0 = np.linspace(0.1, 1.0, 91) 49 | mu = -mu0 50 | azr = np.linspace(0.0, np.pi, 1801) 51 | nmu0 = len(mu0) 52 | nmu = len(mu) 53 | naz = len(azr) 54 | I1up = np.zeros((nmu0, nmu, naz)) 55 | time_start = time.time() 56 | for imu0 in range(len(mu0)): 57 | for imu in range(len(mu)): 58 | I1up[imu0, imu, :] = single_scattering_up(mu[imu], mu0[imu0], azr, tau0, xk) 59 | time_end = time.time() 60 | # 61 | print("sglsup = %.3f sec."%(time_end-time_start)) 62 | #============================================================================== -------------------------------------------------------------------------------- /def_single_scattering_down.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from numba import jit 4 | from def_legendre_polynomial import legendre_polynomial 5 | # 6 | @jit(nopython=True, cache=True) 7 | def single_scattering_down(mu, mu0, azr, tau0, xk): 8 | ''' 9 | Task: 10 | To compute single scattering at bottom of a homogeneous atmosphere. 11 | In: 12 | mu d cos(vza_up) > 0 13 | mu0 d cos(sza) > 0 14 | azr d[naz] relative azimuths in radians; naz = len(azr) 15 | tau0 d total atmosphere optical thickness 16 | xk d[nk] expansion moments * ssa/2, (2k+1) included, nk=len(xk) 17 | Out: 18 | I11dn d Iboa=f(mu, mu0, azr) 19 | Tree: 20 | - 21 | Note: 22 | TOA scaling factor = 2pi; 23 | Refs: 24 | 1. - 25 | ''' 26 | # 27 | # Parameters: 28 | nk = len(xk) 29 | tiny = 1.0e-8 30 | # 31 | smu = np.sqrt(1.0 - mu*mu) 32 | smu0 = np.sqrt(1.0 - mu0*mu0) 33 | nu = mu*mu0 + smu*smu0*np.cos(azr) 34 | p = np.zeros_like(nu) 35 | for inu, nui in enumerate(nu): 36 | pk = legendre_polynomial(nui, nk-1) 37 | p[inu] = np.dot(xk, pk) 38 | # 39 | if np.abs(mu - mu0) < tiny: 40 | I11dn = p*tau0*np.exp(-tau0/mu0)/mu0 41 | else: 42 | I11dn = p*mu0/(mu0 - mu)*(np.exp(-tau0/mu0) - np.exp(-tau0/mu)) 43 | # 44 | return I11dn 45 | #============================================================================== 46 | # 47 | if __name__ == "__main__": 48 | # 49 | tau0 = 1.0/3.0 50 | xk = np.array([1.0, 0.0, 0.5]) 51 | mu0 = np.linspace(0.1, 1.0, 91) 52 | mu = mu0 53 | azr = np.linspace(0.0, np.pi, 1801) 54 | nmu0 = len(mu0) 55 | nmu = len(mu) 56 | naz = len(azr) 57 | I1dn = np.zeros((nmu0, nmu, naz)) 58 | time_start = time.time() 59 | for imu0 in range(len(mu0)): 60 | for imu in range(len(mu)): 61 | I1dn[imu0, imu, :] = single_scattering_down(mu[imu], mu0[imu0], azr, tau0, xk) 62 | time_end = time.time() 63 | # 64 | print("sglsup = %.3f sec."%(time_end-time_start)) 65 | #============================================================================== -------------------------------------------------------------------------------- /def_source_function_integrate_down.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import jit 3 | from def_legendre_polynomial import legendre_polynomial 4 | from def_schmidt_polynomial import schmidt_polynomial 5 | # 6 | @jit(nopython=True, cache=True) 7 | def source_function_integrate_down(m, mu, mu0, nlr, dtau, xk, mug, wg, Ig05): 8 | ''' 9 | Task: 10 | Source function integration: down. 11 | In: 12 | m i Fourier moment: m = 0, 1, 2, .... 13 | mu d upward LOS cos(VZA) < 0 14 | nlr i number of layer elements dtau, tau0 = dtau*nlr 15 | dtau d thickness of element layer (integration step over tau) 16 | xk d[nk] expansion moments*ssa/2, (2k+1) included, nk=len(xk) 17 | mug d[ng2] Gauss nodes 18 | wg d[ng2] Gauss weights 19 | Ig05 d[nlr, ng2] RTE solution at Gauss nodes & at midpoint of every layer dtau 20 | Out: 21 | Iboa d Itoa=f(mu) 22 | Tree: 23 | - 24 | Note: 25 | TOA scaling factor = 2pi; 26 | Refs: 27 | 1. - 28 | ''' 29 | # 30 | # Parameters: 31 | tiny = 1.0e-8 32 | ng2 = len(wg) 33 | nk = len(xk) 34 | nb = nlr+1 35 | tau0 = nlr*dtau 36 | tau = np.linspace(0.0, tau0, nb) 37 | # 38 | pk = np.zeros((ng2, nk)) 39 | if m == 0: 40 | pk0 = legendre_polynomial(mu0, nk-1) 41 | pku = legendre_polynomial(mu, nk-1) 42 | for ig in range(ng2): 43 | pk[ig, :] = legendre_polynomial(mug[ig], nk-1) 44 | else: 45 | pk0 = schmidt_polynomial(m, mu0, nk-1) 46 | pku = schmidt_polynomial(m, mu, nk-1) 47 | for ig in range(ng2): 48 | pk[ig, :] = schmidt_polynomial(m, mug[ig], nk-1) 49 | p = np.dot(xk, pku*pk0) 50 | # 51 | if np.abs(mu - mu0) < tiny: 52 | I11dn = p*dtau*np.exp(-dtau/mu0)/mu0 53 | else: 54 | I11dn = p*mu0/(mu0 - mu)*(np.exp(-dtau/mu0) - np.exp(-dtau/mu)) 55 | # 56 | I1dn = np.zeros(nb) 57 | I1dn[1] = I11dn 58 | for ib in range(2, nb): 59 | I1dn[ib] = I1dn[ib-1]*np.exp(-dtau/mu) + I11dn*np.exp(-tau[ib-1]/mu0) 60 | # 61 | wpij = np.zeros(ng2) # sum{xk*pk(mu)*pk(muj)*wj, k=0:nk} 62 | for jg in range(ng2): 63 | wpij[jg] = wg[jg]*np.dot(xk, pku[:]*pk[jg, :]) 64 | # 65 | Idn = np.copy(I1dn) # boundary condition: Idn[0, :] = 0.0 66 | J = np.dot(wpij, Ig05[0, :]) 67 | Idn[1] = I11dn + (1.0 - np.exp(-dtau/mu))*J 68 | for ib in range(2, nb): 69 | J = np.dot(wpij, Ig05[ib-1, :]) 70 | Idn[ib] = Idn[ib-1]*np.exp(-dtau/mu) + \ 71 | I11dn*np.exp(-tau[ib-1]/mu0) + \ 72 | (1.0 - np.exp(-dtau/mu))*J 73 | # 74 | # Subtract SS & extract BOA value 75 | Ims = Idn - I1dn 76 | Iboa = Ims[nb-1] 77 | return Iboa 78 | #============================================================================== -------------------------------------------------------------------------------- /def_gauss_zeroes_weights.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from numba import jit 5 | # 6 | @jit(nopython=True, cache=True) 7 | def gauss_zeroes_weights(x1, x2, n): 8 | ''' 9 | Task: 10 | To compute 'n' Gauss nodes and weights within (x1, x2). 11 | In: 12 | x1, x2 d interval 13 | n i number of nodes 14 | Out: 15 | x, w d[n] zeros and weights 16 | Tree: 17 | - 18 | Notes: 19 | Tested only for x1 < x2. To test run, e.g., 20 | ng = 64 21 | x, w = gauszw(-1.0, 1.0, ng) 22 | and compare vs [1]. 23 | Refs: 24 | 1. https://pomax.github.io/bezierinfo/legendre-gauss.html 25 | ''' 26 | const_yeps = 3.0e-14 27 | x = np.zeros(n) 28 | w = np.zeros(n) 29 | m = int((n+1)/2) 30 | yxm = 0.5*(x2 + x1) 31 | yxl = 0.5*(x2 - x1) 32 | for i in range(m): 33 | yz = np.cos(np.pi*(i + 0.75)/(n + 0.5)) 34 | while True: 35 | yp1 = 1.0 36 | yp2 = 0.0 37 | for j in range(n): 38 | yp3 = yp2 39 | yp2 = yp1 40 | yp1 = ((2.0*j + 1.0)*yz*yp2 - j*yp3 )/(j+1) 41 | ypp = n*(yz*yp1 - yp2)/(yz*yz - 1.0) 42 | yz1 = yz 43 | yz = yz1 - yp1/ypp 44 | if (np.abs(yz - yz1) < const_yeps): 45 | break # exit while loop 46 | x[i] = yxm - yz*yxl 47 | x[n-1-i] = yxm + yxl*yz 48 | w[i] = 2.0*yxl/((1.0 - yz*yz)*ypp*ypp) 49 | w[n-1-i] = w[i] 50 | return x, w 51 | #============================================================================== 52 | # 53 | if __name__ == "__main__": 54 | # 55 | # 56 | size = 24 57 | n = 10 58 | n2 = n*2 59 | zs, ws = gauss_zeroes_weights(-1.0, 1.0, n) 60 | zd = np.zeros(n2) 61 | wd = np.zeros(n2) 62 | zd = (zs + 1.0)/2.0 63 | wd = ws/2.0 64 | zdzd = np.concatenate((np.flip(-zd), zd)) 65 | wdwd = np.concatenate((wd, wd)) 66 | z2, w2 = gauss_zeroes_weights(-1.0, 1.0, n2) 67 | fig = plt.figure() 68 | plt.plot(z2, w2, color=(0.5, 0.5, 0.5)) 69 | plt.plot(-zdzd, wdwd, color=(0.75, 0.75, 0.75)) 70 | plt.plot(z2, w2, 'or', -zdzd, wdwd, 'ob') 71 | plt.grid(True) 72 | plt.xlabel('Zeros', fontsize=18) 73 | plt.ylabel('Weights', fontsize=18) 74 | plt.tick_params(axis='x', labelsize=14) 75 | plt.tick_params(axis='y', labelsize=14) 76 | # plt.title('Gaussian quadratures', size=16) 77 | fig.set_size_inches((12, 6)) 78 | plt.tight_layout() 79 | # plt.legend(['Single', 'Double'], fontsize=16) 80 | plt.savefig('gauss.tiff', dpi=600) 81 | 82 | 83 | z, w = gauss_zeroes_weights(0.0, 1.0, n) 84 | for i in range(n): 85 | print(w[i] - wd[i], z[i] - zd[i]) 86 | # 87 | n = 1024 88 | time_start = time.time() 89 | zn, wn = gauss_zeroes_weights(-1.0, 1.0, n) 90 | time_end = time.time() 91 | # 92 | print("gauszw runtime = %.3f sec."%(time_end-time_start)) 93 | #============================================================================== -------------------------------------------------------------------------------- /def_source_function_integrate_up.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numba import jit 3 | from def_legendre_polynomial import legendre_polynomial 4 | from def_schmidt_polynomial import schmidt_polynomial 5 | # 6 | @jit(nopython=True, cache=True) 7 | def source_function_integrate_up(m, mu, mu0, srfa, nlr, dtau, xk, mug, wg, Ig05, Igboa): 8 | ''' 9 | Task: 10 | Source function integration: up. 11 | In: 12 | m i Fourier moment: m = 0, 1, 2, .... 13 | mu d upward LOS cos(VZA) < 0 14 | mu0 d cos(SZA) > 0 15 | srfa d Lambertian surface albedo 16 | nlr i number of layer elements dtau, tau0 = dtau*nlr 17 | dtau d thickness of element layer (integration step over tau) 18 | ssa d single scattering albedo 19 | xk d[nk] expansion moments*ssa/2, (2k+1) included, nk=len(xk) 20 | mug d[ng2] Gauss nodes 21 | wg d[ng2] Gauss weights 22 | Ig05 d[nlr, ng2] RTE solution at Gauss nodes & at midpoint of every layer dtau 23 | Igboa d[ng1] Same as Ig05, except for downward at BOA 24 | Out: 25 | Itoa d Itoa=f(mu) 26 | Tree: 27 | - 28 | Note: 29 | TOA scaling factor = 2pi; 30 | Refs: 31 | 1. - 32 | Revision History: 33 | 2020-06-29: 34 | New input parameter: m; 35 | Removed input paramter: ssa; 36 | Pk(x) or Qkm(x) is now called depending on m; 37 | 2020-06-18: 38 | Changes similar to gsitm() 39 | 2020-06-14: 40 | First created and tested vs IPOL for R&A 41 | ''' 42 | # 43 | # parameters: 44 | ng2 = len(wg) 45 | ng1 = ng2//2 46 | nk = len(xk) 47 | mup = -mu 48 | nb = nlr+1 49 | tau0 = nlr*dtau 50 | tau = np.linspace(0.0, tau0, nb) 51 | # 52 | pk = np.zeros((ng2, nk)) 53 | if m == 0: 54 | pk0 = legendre_polynomial(mu0, nk-1) 55 | pku = legendre_polynomial(mu, nk-1) 56 | for ig in range(ng2): 57 | pk[ig, :] = legendre_polynomial(mug[ig], nk-1) 58 | else: 59 | pk0 = schmidt_polynomial(m, mu0, nk-1) 60 | pku = schmidt_polynomial(m, mu, nk-1) 61 | for ig in range(ng2): 62 | pk[ig, :] = schmidt_polynomial(m, mug[ig], nk-1) 63 | p = np.dot(xk, pku*pk0) 64 | # 65 | I11up = p*mu0/(mu0 + mup)*(1.0 - np.exp(-dtau/mup - dtau/mu0)) 66 | # 67 | I1up = np.zeros(nb) 68 | if m == 0 and srfa > 0.0: 69 | I1up[nb-1] = 2.0*srfa*mu0*np.exp(-tau0/mu0) 70 | I1up[nb-2] = I1up[nb-1]*np.exp(-dtau/mup) + I11up*np.exp(-tau[nb-2]/mu0) 71 | else: 72 | I1up[nb-2] = I11up*np.exp(-tau[nb-2]/mu0) 73 | for ib in range(nb-3, -1, -1): 74 | I1up[ib] = I1up[ib+1]*np.exp(-dtau/mup) + I11up*np.exp(-tau[ib]/mu0) 75 | # 76 | wpij = np.zeros(ng2) # sum{xk*pk(mu)*pk(muj)*wj, k=0:nk} 77 | for jg in range(ng2): 78 | wpij[jg] = wg[jg]*np.dot(xk, pku[:]*pk[jg, :]) 79 | # 80 | Iup = np.copy(I1up) 81 | if m == 0 and srfa > 0.0: 82 | Iup[nb-1] = 2.0*srfa*np.dot(Igboa, mug[ng1:ng2]*wg[ng1:ng2]) + \ 83 | 2.0*srfa*mu0*np.exp(-tau0/mu0) 84 | J = np.dot(wpij, Ig05[nb-2, :]) 85 | Iup[nb-2] = Iup[nb-1]*np.exp(-dtau/mup) + \ 86 | I11up*np.exp(-tau[nb-2]/mu0) + \ 87 | (1.0 - np.exp(-dtau/mup))*J 88 | for ib in range(nb-3, -1, -1): 89 | J = np.dot(wpij, Ig05[ib, :]) 90 | Iup[ib] = Iup[ib+1]*np.exp(-dtau/mup) + \ 91 | I11up*np.exp(-tau[ib]/mu0) + \ 92 | (1.0 - np.exp(-dtau/mup))*J 93 | # 94 | # Subtract SS (including surface) & extract TOA value 95 | Ims = Iup - I1up 96 | Itoa = Ims[0] 97 | return Itoa 98 | #============================================================================== -------------------------------------------------------------------------------- /test_gsit_R&A_srf.txt: -------------------------------------------------------------------------------- 1 | # SZA AZA mu Rayleigh Aerosol+Surface 2 | 45 0 -0.2 1.31857E-01 1.87775E-01 3 | 45 0 -0.3 1.19717E-01 1.65770E-01 4 | 45 0 -0.4 1.07597E-01 1.43919E-01 5 | 45 0 -0.5 9.66534E-02 1.24914E-01 6 | 45 0 -0.6 8.73238E-02 1.09385E-01 7 | 45 0 -0.7 7.97436E-02 9.70957E-02 8 | 45 0 -0.8 7.40228E-02 8.75384E-02 9 | 45 0 -0.9 7.05627E-02 8.01726E-02 10 | 45 45 -0.2 1.22785E-01 1.37432E-01 11 | 45 45 -0.3 1.13270E-01 1.29299E-01 12 | 45 45 -0.4 1.03404E-01 1.18425E-01 13 | 45 45 -0.5 9.43558E-02 1.07659E-01 14 | 45 45 -0.6 8.65802E-02 9.81360E-02 15 | 45 45 -0.7 8.02184E-02 9.01544E-02 16 | 45 45 -0.8 7.53500E-02 8.36614E-02 17 | 45 45 -0.9 7.22449E-02 7.84738E-02 18 | 45 90 -0.2 1.17632E-01 8.90322E-02 19 | 45 90 -0.3 1.11960E-01 9.04951E-02 20 | 45 90 -0.4 1.05141E-01 8.90036E-02 21 | 45 90 -0.5 9.84058E-02 8.63224E-02 22 | 45 90 -0.6 9.22541E-02 8.33561E-02 23 | 45 90 -0.7 8.68261E-02 8.05225E-02 24 | 45 90 -0.8 8.21122E-02 7.79903E-02 25 | 45 90 -0.9 7.80481E-02 7.58029E-02 26 | 45 135 -0.2 1.36168E-01 7.50742E-02 27 | 45 135 -0.3 1.30806E-01 7.86107E-02 28 | 45 135 -0.4 1.23649E-01 7.95813E-02 29 | 45 135 -0.5 1.16028E-01 7.91781E-02 30 | 45 135 -0.6 1.08491E-01 7.80961E-02 31 | 45 135 -0.7 1.01158E-01 7.67484E-02 32 | 45 135 -0.8 9.39060E-02 7.54002E-02 33 | 45 135 -0.9 8.63151E-02 7.42491E-02 34 | 45 180 -0.2 1.50785E-01 7.33228E-02 35 | 45 180 -0.3 1.44516E-01 7.65379E-02 36 | 45 180 -0.4 1.36227E-01 7.74871E-02 37 | 45 180 -0.5 1.27303E-01 7.79067E-02 38 | 45 180 -0.6 1.18310E-01 7.90677E-02 39 | 45 180 -0.7 1.09356E-01 7.97093E-02 40 | 45 180 -0.8 1.00265E-01 7.65957E-02 41 | 45 180 -0.9 9.04610E-02 7.36062E-02 42 | 45 0 0.2 9.01739E-02 2.98164E-01 43 | 45 0 0.3 9.71640E-02 3.49971E-01 44 | 45 0 0.4 9.90347E-02 4.08993E-01 45 | 45 0 0.5 9.76689E-02 4.85050E-01 46 | 45 0 0.6 9.43856E-02 5.74586E-01 47 | 45 0 0.7 8.98800E-02 6.11615E-01 48 | 45 0 0.8 8.43994E-02 4.97495E-01 49 | 45 0 0.9 7.77215E-02 2.97635E-01 50 | 45 45 0.2 8.38213E-02 1.87334E-01 51 | 45 45 0.3 8.99192E-02 1.98213E-01 52 | 45 45 0.4 9.14588E-02 2.04493E-01 53 | 45 45 0.5 9.02424E-02 2.09220E-01 54 | 45 45 0.6 8.74911E-02 2.13163E-01 55 | 45 45 0.7 8.38433E-02 2.14146E-01 56 | 45 45 0.8 7.95429E-02 2.06018E-01 57 | 45 45 0.9 7.44621E-02 1.78106E-01 58 | 45 90 0.2 7.57570E-02 1.03884E-01 59 | 45 90 0.3 7.99524E-02 1.01180E-01 60 | 45 90 0.4 8.03044E-02 9.61953E-02 61 | 45 90 0.5 7.86288E-02 9.11179E-02 62 | 45 90 0.6 7.60858E-02 8.71624E-02 63 | 45 90 0.7 7.32872E-02 8.50085E-02 64 | 45 90 0.8 7.05331E-02 8.51769E-02 65 | 45 90 0.9 6.79616E-02 8.83164E-02 66 | 45 135 0.2 7.79772E-02 7.79305E-02 67 | 45 135 0.3 8.06257E-02 7.29980E-02 68 | 45 135 0.4 7.92412E-02 6.67581E-02 69 | 45 135 0.5 7.59467E-02 6.08436E-02 70 | 45 135 0.6 7.20904E-02 5.61450E-02 71 | 45 135 0.7 6.84131E-02 5.32465E-02 72 | 45 135 0.8 6.53628E-02 5.29343E-02 73 | 45 135 0.9 6.33962E-02 5.74111E-02 74 | 45 180 0.2 8.19092E-02 7.31254E-02 75 | 45 180 0.3 8.40211E-02 6.77385E-02 76 | 45 180 0.4 8.17565E-02 6.11800E-02 77 | 45 180 0.5 7.74517E-02 5.49971E-02 78 | 45 180 0.6 7.26058E-02 5.00169E-02 79 | 45 180 0.7 6.80585E-02 4.67738E-02 80 | 45 180 0.8 6.43457E-02 4.60250E-02 81 | 45 180 0.9 6.20719E-02 5.00716E-02 82 | -------------------------------------------------------------------------------- /def_schmidt_polynomial.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from numba import jit 4 | # 5 | @jit(nopython=True, cache=True) 6 | def schmidt_polynomial(m, x, kmax): 7 | ''' 8 | Task: 9 | To compute the Qkm(x) plynomials for all k = m:kmax & Fourier order 10 | m > 0. Qkm(x) = 0 is returned for all k < m. 11 | In: 12 | m i Fourier order (as in theory cos(m*phi)): m = 1,2,3.... 13 | x f abscissa 14 | kmax i maximum order, k = 0,1,2...kmax 15 | Out: 16 | pk [kmax+1] polynomials 17 | Tree: 18 | - 19 | Notes: 20 | Think me: provide only non-zero polynomials, k>=m, on output. 21 | Definition: 22 | 23 | Qkm(x) = sqrt[(k-m)!/(k+m)!]*Pkm, (1) 24 | Pkm(x) = (1-x2)^(m/2)*(dPk(x)/dx)^m, (2) 25 | 26 | where Pk(x) are the Legendre polynomials. Note, unlike in [2] (-1)^m is 27 | omitted in Qkm(x). Refer to [1-4] for details. 28 | 29 | Qkm(x) for a few initial values of m > 0 and k for testing: 30 | m = 1: 31 | Q01 = 0.0 // k = 0 32 | Q11 = sqrt( 0.5*(1.0 - x2) ) // k = 1 33 | Q21 = 3.0*x*sqrt( (1.0 - x2)/6.0 ) // k = 2 34 | Q31 = (3.0/4.0)*(5.0*x2 - 1.0)*sqrt( (1.0 - x2)/3.0 ) // k = 3 35 | m = 2: 36 | Q02 = 0.0 // k = 0 37 | Q12 = 0.0 // k = 1 38 | Q22 = 3.0/(2.0*sqrt(6.0))*(1.0 - x2); // k = 2 39 | Q32 = 15.0/sqrt(120.0)*x*(1.0 - x2); // k = 3 40 | Q42 = 15.0/(2.0*sqrt(360.0))*(7.0*x2 - 1.0)*(1.0 - x2) // k = 4 41 | m = 3: 42 | Q03 = 0.0 // k = 0 43 | Q13 = 0.0 // k = 1 44 | Q23 = 0.0 // k = 2 45 | Q33 = 15.0/sqrt(720.0)*(1.0 - x2)*sqrt(1.0 - x2); // k = 3 46 | Q43 = 105.0/sqrt(5040.0)*(1.0 - x2)*x*sqrt(1.0 - x2) // k = 4 47 | 48 | Data for stress test: POLQKM.f90 (agrees with polqkm.cpp) 49 | k = 512 (in Fortran 513), m = 256 50 | x POLQKM.f90 def polqkm |err| 51 | -1.00 0.000000000000000E+000 -0.0000000000000000e+00 0.0 52 | -0.50 (!) -2.601822304856592E-002 -2.6018223048565915e-02 3.5e-18 53 | 0.00 3.786666189291950E-002 3.7866661892919498e-02 0.0 54 | 0.25 9.592316443679009E-003 9.5923164436790085e-03 0.0 55 | 0.50 (!) -2.601822304856592E-002 -2.6018223048565915e-02 3.5e-18 56 | 0.75 -2.785756308806302E-002 -2.7857563088063021e-02 0.0 57 | 1.00 0.000000000000000E+000 0.0000000000000000e+00 0.0 58 | Refs: 59 | 1. Gelfand IM et al., 1963: Representations of the rotation and Lorentz 60 | groups and their applications. Oxford: Pergamon Press. 61 | 2. Hovenier JW et al., 2004: Transfer of Polarized Light in Planetary 62 | Atmosphere. Basic Concepts and Practical Methods, Dordrecht: Kluwer 63 | Academic Publishers. 64 | 3. http://mathworld.wolfram.com/AssociatedLegendrePolynomial.html 65 | 4. http://www.mathworks.com/help/matlab/ref/legendre.html 66 | ''' 67 | # 68 | nk = kmax+1 69 | qk = np.zeros(nk) 70 | # 71 | # k=m: Qmm(x)=c0*[sqrt(1-x2)]^m 72 | c0 = 1.0 73 | for ik in range(2, 2*m+1, 2): 74 | c0 = c0 - c0/ik 75 | qk[m] = np.sqrt(c0)*np.power(np.sqrt( 1.0 - x*x ), m) 76 | # 77 | # Q{k-1}m(x), Q{k-2}m(x) -> Qkm(x) 78 | m1 = m*m - 1.0 79 | m4 = m*m - 4.0 80 | for ik in range(m+1, nk): 81 | c1 = 2.0*ik - 1.0 82 | c2 = np.sqrt( (ik + 1.0)*(ik - 3.0) - m4 ) 83 | c3 = 1.0/np.sqrt( (ik + 1.0)*(ik - 1.0) - m1 ) 84 | qk[ik] = ( c1*x*qk[ik-1] - c2*qk[ik-2] )*c3 85 | return qk 86 | #============================================================================== 87 | # 88 | if __name__ == "__main__": 89 | # 90 | # 91 | mu = np.linspace(-1.0, 1.0, 1001) 92 | nmu = len(mu) 93 | kmax = 256 94 | nm = 128 95 | qkmu = np.zeros((nm, nmu, kmax+1)) 96 | time_start = time.time() 97 | for im in range(nm): 98 | for imu in range(nmu): 99 | qkmu[im, imu, :] = schmidt_polynomial(im, mu[imu], kmax) 100 | time_end = time.time() 101 | # 102 | print("polqkm = %.3f sec."%(time_end-time_start)) 103 | #============================================================================== -------------------------------------------------------------------------------- /gsit.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from def_gauss_seidel_iterations_m import gauss_seidel_iterations_m 4 | from def_source_function_integrate_up import source_function_integrate_up 5 | from def_source_function_integrate_down import source_function_integrate_down 6 | from def_single_scattering_up import single_scattering_up 7 | from def_single_scattering_down import single_scattering_down 8 | #============================================================================== 9 | # 10 | if __name__ == "__main__": 11 | # 12 | prnt_scrn = False # print out intensities on the screen 13 | fname_bmark = 'test_gsit_R&A_srf.txt' # file with benchmark data 14 | # 15 | phasefun = 'a' # 'r': rayleigh; aerosol otherwise (case sensitive) 16 | nit = 10 # number of iterations 17 | ng1 = 8 # number of Gauss nodes per hemisphere 18 | nlr = 100 # number of layer elements 19 | dtau = 0.01 # integration step over tau 20 | ssa = 0.99999999 # single scattering albedo 21 | sza = 45.0 # solar zenith angle, degrees 22 | muup = np.linspace(-0.2, -0.9, num=8) # cos(view zenith angle): upward 23 | azd = np.array([0.0, 45.0, 90.0, 135.0, 180.0]) # relative azimuths 24 | # 25 | if phasefun == 'r': 26 | print("Rayleigh:") 27 | nm = 3 28 | xk = np.array([1.0, 0.0, 0.5]) 29 | srfa = 0.0 # surface albedo 30 | icol = 3 31 | else: 32 | print("Aerosol:") 33 | nm = 10 34 | xk = np.array([1.000000, 2.084911, 2.459134, 2.234752, \ 35 | 1.873098, 1.492956, 1.164725, 0.881976, \ 36 | 0.689172, 0.506016, 0.402247, 0.289742, \ 37 | 0.232980, 0.165427, 0.133290, 0.093127, \ 38 | 0.074444, 0.050682, 0.039800, 0.025983, \ 39 | 0.019929, 0.012274, 0.009257, 0.005312, \ 40 | 0.004015, 0.002120, 0.001638, 0.000764, \ 41 | 0.000602, 0.000211, 0.000178, 0.000033, \ 42 | 0.000037, 0.000004, 0.000005, 0.000000]) 43 | srfa = 0.3 # surface albedo 44 | icol = 4 45 | #------------------------------------------------------------------------------ 46 | # 47 | time_start = time.time() 48 | # 49 | mu0 = np.cos(np.radians(sza)) 50 | mudn = -muup 51 | nmu = len(muup) 52 | azr = np.radians(azd) 53 | naz = len(azd) 54 | nrows = nmu*naz 55 | # 56 | # Compute SS at TOA & BOA 57 | Itoa = np.zeros((nmu, naz)) 58 | if srfa > 0.0: 59 | for imu, mu in enumerate(muup): 60 | Itoa[imu, :] = single_scattering_up(mu, mu0, azr, nlr*dtau, 0.5*ssa*xk) + \ 61 | 2.0*srfa*mu0*np.exp(-nlr*dtau/mu0)*np.exp(nlr*dtau/mu) 62 | else: 63 | for imu, mu in enumerate(muup): 64 | Itoa[imu, :] = single_scattering_up(mu, mu0, azr, nlr*dtau, 0.5*ssa*xk) 65 | # 66 | Iboa = np.zeros((nmu, naz)) 67 | for imu, mu in enumerate(mudn): 68 | Iboa[imu, :] = single_scattering_down(mu, mu0, azr, nlr*dtau, 0.5*ssa*xk) 69 | # 70 | # Compute Fourier moments for MS 71 | time_gsitm = 0.0 72 | deltm0 = 1.0 73 | for m in range(nm): 74 | # 75 | # Solve RTE at Gauss nodes & all boundaries 76 | t1 = time.time() 77 | mug, wg, Igup, Igdn = gauss_seidel_iterations_m(m, mu0, srfa, nit, ng1, nlr, dtau, 0.5*ssa*xk) 78 | t2 = time.time() 79 | time_gsitm += t2 - t1 80 | # 81 | # Compute intensity at midpoint of every layer, both up & down 82 | Ig05 = np.zeros((nlr, 2*ng1)) 83 | for ilr in range(nlr): 84 | Iup05 = 0.5*(Igup[ilr, :] + Igup[ilr+1, :]) 85 | Idn05 = 0.5*(Igdn[ilr, :] + Igdn[ilr+1, :]) 86 | Ig05[ilr, :] = np.concatenate((Iup05, Idn05)) 87 | # 88 | # Accumulate Fourier series 89 | cma = deltm0*np.cos(m*azr) 90 | for imu, mu in enumerate(muup): 91 | Ims_toa = source_function_integrate_up(m, mu, mu0, srfa, nlr, dtau, 0.5*ssa*xk, mug, wg, Ig05, Igdn[nlr, :]) 92 | Itoa[imu, :] += Ims_toa*cma 93 | for imu, mu in enumerate(mudn): 94 | Ims_boa = source_function_integrate_down(m, mu, mu0, nlr, dtau, 0.5*ssa*xk, mug, wg, Ig05) 95 | Iboa[imu, :] += Ims_boa*cma 96 | # 97 | # Kronecker delta = 2 for m > 0 98 | deltm0 = 2.0 99 | print('m =', m) 100 | # end for m 101 | # 102 | # Scale to unit flux on TOA 103 | Itoa *= 0.5/np.pi 104 | Iboa *= 0.5/np.pi 105 | # 106 | time_end = time.time() 107 | # 108 | #------------------------------------------------------------------------------ 109 | # 110 | # Test vs benchmark: 111 | dat = np.loadtxt(fname_bmark, comments='#', skiprows=1) 112 | Ibmup = dat[0:nrows, icol] 113 | Ibmdn = dat[nrows:, icol] 114 | # 115 | print("\nTOA:") 116 | Ibup = np.transpose(np.reshape(Ibmup, (naz, nmu))) 117 | err = 100.0*(Itoa/Ibup - 1.0) 118 | if prnt_scrn: 119 | print(" azd mu gsit err, %") 120 | for iaz, az in enumerate(azd): 121 | for imu, mu in enumerate(muup): 122 | print(" %5.1f %5.1f %.4e %.2f" %(az, mu, Itoa[imu, iaz], err[imu, iaz])) 123 | emax = np.amax(np.abs(err)) 124 | eavr = np.average(np.abs(err)) 125 | print(" max & avr errs: %.2f %.2f" %(emax, eavr)) 126 | # 127 | print("\nBOA:") 128 | Ibdn = np.transpose(np.reshape(Ibmdn, (naz, nmu))) 129 | err = 100.0*(Iboa/Ibdn - 1.0) 130 | if prnt_scrn: 131 | print(" azd mu gsit err, %") 132 | for iaz, az in enumerate(azd): 133 | for imu, mu in enumerate(mudn): 134 | print(" %5.1f %5.1f %.4e %.2f" %(az, mu, Iboa[imu, iaz], err[imu, iaz])) 135 | emax = np.amax(np.abs(err)) 136 | eavr = np.average(np.abs(err)) 137 | print(" max & avr errs: %.2f %.2f" %(emax, eavr)) 138 | # 139 | print("\nmultiple scattering runtime = %.2f sec."%time_gsitm) 140 | print("gsit total runtime = %.2f sec."%(time_end-time_start)) 141 | #============================================================================== -------------------------------------------------------------------------------- /def_gauss_seidel_iterations_m.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | from numba import jit 4 | from def_gauss_zeroes_weights import gauss_zeroes_weights 5 | from def_legendre_polynomial import legendre_polynomial 6 | from def_schmidt_polynomial import schmidt_polynomial 7 | # 8 | @jit(nopython=True, cache=True) 9 | def gauss_seidel_iterations_m(m, mu0, srfa, nit, ng1, nlr, dtau, xk): 10 | ''' 11 | Task: 12 | Solve RTE in a basic scenario using Gauss-Seidel (GS) iterations. 13 | In: 14 | m i Fourier moment: m = 0, 1, 2, ... len(xk)-1 15 | mu0 d cos(SZA) > 0 16 | srfa d Lambertian surface albedo 17 | nit i number of iterations, nit > 0 18 | ng1 i number of gauss nodes per hemisphere 19 | nlr i number of layer elements dtau, tau0 = dtau*nlr 20 | dtau d thickness of element layer (integration step over tau) 21 | xk d[nk] expansion moments*ssa/2, (2k+1) included, nk=len(xk) 22 | Out: 23 | mug, wg d[ng1*2] Gauss nodes & weights 24 | Iup, Idn d[nlr+1, ng1] intensity, I = f(tau), at Gauss nodes 25 | Tree: 26 | gsit() 27 | > gauszw() - computes Gauss zeros and weights 28 | > polleg() - computes ordinary Legendre polynomilas Pk(x) 29 | > polqkm() - computes *renormalized* associated Legendre polynomilas Qkm(x) 30 | Note: 31 | TOA scaling factor = 2pi; 32 | Refs: 33 | 1. - 34 | ''' 35 | # 36 | # Parameters: 37 | tiny = 1.0e-8 38 | nb = nlr+1 39 | nk = len(xk) 40 | ng2 = ng1*2 41 | tau0 = nlr*dtau 42 | tau = np.linspace(0.0, tau0, nb) 43 | # 44 | # Gauss nodes and weights: mup - positive Gauss nodes; mug - all Gauss nodes: 45 | mup, w = gauss_zeroes_weights(0.0, 1.0, ng1) 46 | mug = np.concatenate((-mup, mup)) 47 | wg = np.concatenate((w, w)) 48 | # 49 | pk = np.zeros((ng2, nk)) 50 | p = np.zeros(ng2) 51 | if m == 0: 52 | pk0 = legendre_polynomial(mu0, nk-1) 53 | for ig in range(ng2): 54 | pk[ig, :] = legendre_polynomial(mug[ig], nk-1) 55 | p[ig] = np.dot(xk, pk[ig, :]*pk0) 56 | else: 57 | pk0 = schmidt_polynomial(m, mu0, nk-1) 58 | for ig in range(ng2): 59 | pk[ig, :] = schmidt_polynomial(m, mug[ig], nk-1) 60 | p[ig] = np.dot(xk, pk[ig, :]*pk0) 61 | # 62 | # SS down: 63 | I11dn = np.zeros(ng1) 64 | for ig in range(ng1): 65 | mu = mup[ig] 66 | if (np.abs(mu0 - mu) < tiny): 67 | I11dn[ig] = p[ng1+ig]*dtau*np.exp(-dtau/mu0)/mu0 68 | else: 69 | I11dn[ig] = p[ng1+ig]*mu0/(mu0 - mu)*(np.exp(-dtau/mu0) - np.exp(-dtau/mu)) 70 | I1dn = np.zeros((nb, ng1)) 71 | I1dn[1, :] = I11dn 72 | for ib in range(2, nb): 73 | I1dn[ib, :] = I1dn[ib-1, :]*np.exp(-dtau/mup) + I11dn*np.exp(-tau[ib-1]/mu0) 74 | # 75 | # SS up: 76 | I11up = p[0:ng1]*mu0/(mu0 + mup)*(1.0 - np.exp(-dtau/mup - dtau/mu0)) 77 | I1up = np.zeros_like(I1dn) 78 | if m == 0 and srfa > tiny: 79 | I1up[nb-1, :] = 2.0*srfa*mu0*np.exp(-tau0/mu0) 80 | I1up[nb-2, :] = I1up[nb-1, :]*np.exp(-dtau/mup) + I11up*np.exp(-tau[nb-2]/mu0) 81 | else: 82 | I1up[nb-2, :] = I11up*np.exp(-tau[nb-2]/mu0) 83 | for ib in range(nb-3, -1, -1): 84 | I1up[ib, :] = I1up[ib+1, :]*np.exp(-dtau/mup) + I11up*np.exp(-tau[ib]/mu0) 85 | # 86 | # MS: only odd/even k are needed for up/down - not yet applied 87 | wpij = np.zeros((ng2, ng2)) # sum{xk*pk(mui)*pk(muj)*wj, k=0:nk} 88 | for ig in range(ng2): 89 | for jg in range(ng2): 90 | wpij[ig, jg] = wg[jg]*np.dot(xk, pk[ig, :]*pk[jg, :]) # thinkme: use matrix formalism? 91 | # 92 | # MOM: [Jup; Jdn] = [[Tup Rup]; [Rdn Tdn]]*[Iup Idn]; Tup = Tdn = T; Rup = Rdn = R 93 | T = wpij[0:ng1, 0:ng1].copy() # T.flags.c_contiguous = True 94 | R = wpij[0:ng1, ng1:ng2].copy() # R.flags.c_contiguous = True 95 | # 96 | Iup = np.copy(I1up) 97 | Idn = np.copy(I1dn) 98 | for itr in range(nit): 99 | # Down: 100 | Iup05 = 0.5*(Iup[0, :] + Iup[1, :]) 101 | Idn05 = 0.5*(Idn[0, :] + Idn[1, :]) # Idn[0, :] = 0.0 102 | J = np.dot(R, Iup05) + np.dot(T, Idn05) 103 | Idn[1, :] = I11dn + (1.0 - np.exp(-dtau/mup))*J 104 | for ib in range(2, nb): 105 | Iup05 = 0.5*(Iup[ib-1, :] + Iup[ib, :]) 106 | Idn05 = 0.5*(Idn[ib-1, :] + Idn[ib, :]) 107 | J = np.dot(R, Iup05) + np.dot(T, Idn05) 108 | Idn[ib, :] = Idn[ib-1, :]*np.exp(-dtau/mup) + \ 109 | I11dn*np.exp(-tau[ib-1]/mu0) + \ 110 | (1.0 - np.exp(-dtau/mup))*J 111 | # Up: 112 | # Lambertian surface 113 | if m == 0 and srfa > tiny: 114 | Iup[nb-1, :] = 2.0*srfa*np.dot(Idn[nb-1, :], mup*w) + 2.0*srfa*mu0*np.exp(-tau0/mu0) 115 | Iup05 = 0.5*(Iup[nb-2, :] + Iup[nb-1, :]) # Iup[nb-1, :] = 0.0 116 | Idn05 = 0.5*(Idn[nb-2, :] + Idn[nb-1, :]) 117 | J = np.dot(T, Iup05) + np.dot(R, Idn05) 118 | Iup[nb-2, :] = Iup[nb-1, :]*np.exp(-dtau/mup) + \ 119 | I11up*np.exp(-tau[nb-2]/mu0) + \ 120 | (1.0 - np.exp(-dtau/mup))*J 121 | for ib in range(nb-3, -1, -1): # going up, ib = 0 (TOA) must be included 122 | Iup05 = 0.5*(Iup[ib, :] + Iup[ib+1, :]) 123 | Idn05 = 0.5*(Idn[ib, :] + Idn[ib+1, :]) 124 | J = np.dot(T, Iup05) + np.dot(R, Idn05) 125 | Iup[ib, :] = Iup[ib+1, :]*np.exp(-dtau/mup) + \ 126 | I11up*np.exp(-tau[ib]/mu0) + \ 127 | (1.0 - np.exp(-dtau/mup))*J 128 | # print("iter=%i"%itr) 129 | return mug, wg, Iup[:, :], Idn[:, :] 130 | #============================================================================== 131 | # 132 | if __name__ == "__main__": 133 | # 134 | m = 0 135 | mu0 = 0.6 136 | srfa = 0.3 137 | nit = 100 138 | ng1 = 32 139 | nlr = 100 140 | dtau = 0.01 141 | ssa = 1.0 142 | xk = 0.5*ssa*np.array([1.0, 0.0, 0.5]) 143 | # 144 | time_start = time.time() 145 | mug, wg, Iup, Idn = gsitm(m, mu0, srfa, nit, ng1, nlr, dtau, xk) 146 | time_end = time.time() 147 | print("1st call of gsitm: runtime = %.3f sec."%(time_end-time_start)) 148 | # 149 | time_start = time.time() 150 | ng1 += 1 151 | mug, wg, Iup, Idn = gsitm(m, mu0, srfa, nit, ng1, nlr, dtau, xk) 152 | time_end = time.time() 153 | print("2nd call of gsitm: runtime = %.3f sec."%(time_end-time_start)) 154 | # 155 | time_start = time.time() 156 | ng1 -= 1 157 | mug, wg, Iup, Idn = gsitm(m, mu0, srfa, nit, ng1, nlr, dtau, xk) 158 | time_end = time.time() 159 | print("3rd call of gsitm: runtime = %.3f sec."%(time_end-time_start)) 160 | # 161 | time_start = time.time() 162 | ng1 += 1 163 | mug, wg, Iup, Idn = gsitm(m, mu0, srfa, nit, ng1, nlr, dtau, xk) 164 | time_end = time.time() 165 | print("4th call of gsitm: runtime = %.3f sec."%(time_end-time_start)) 166 | # 167 | np.savez('gsitm_output', mu0=mu0, srfa=srfa, ng1=ng1, dtau=dtau, ssa=ssa, xk=xk, \ 168 | mug=mug, wg=wg, Iup=Iup, Idn=Idn) 169 | print('done: gsit output saved') 170 | # 171 | #============================================================================== --------------------------------------------------------------------------------