#!/usr/bin/env python
# coding: utf-8
#
IAGA Summer School 2019
#
# Spherical harmonic models 1
# In[ ]:
# Import notebook dependencies
import os
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
sys.path.append('..')
from src import sha_lib as sha, mag_lib as mag
IGRF12_FILE = os.path.abspath('../data/external/igrf12coeffs.txt')
# # 1. Spherical harmonics and representing the geomagnetic field
# The north (X), east (Y) and vertical (Z) (downwards) components of the internally-generated geomagnetic field at colatitude $\theta$, longitude $\phi$ and radial distance $r$ (in geocentric coordinates with reference radius $a=6371.2$ km for the IGRF) are written as follows:
#
# $$\begin{align}
# &X= \sum_{n=1}^N\left(\frac{a}{r}\right)^{n+2}\left[ g_n^0X_n^0+\sum_{m=1}^n\left( g_n^m\cos m\phi+h_n^m\sin m\phi \right)X_n^m\right]\\[6pt]
# &Y= \sum_{n=1}^N\left(\frac{a}{r}\right)^{n+2} \sum_{m=1}^n \left(g_n^m\sin m\phi-h_n^m\cos m\phi \right)Y_n^m \\[6pt]
# &Z= \sum_{n=1}^N\left(\frac{a}{r}\right)^{n+2} \left[g_n^0Z_n^0+\sum_{m=1}^n\left( g_n^m\cos m\phi+h_n^m\sin m\phi \right)Z_n^m\right]\\[6pt]
# \text{with}&\\[6pt]
# &X_n^m=\frac{dP_n^n}{d\theta}\\[6pt]
# &Y_n^m=\frac{m}{\sin \theta}P_n^m \kern{10ex} \text{(Except at the poles where $Y_n^m=X_n^m\cos \theta$.)}\\[6pt]
# &Z_n^m=-(n+1)P_n^m
# \end{align}$$
#
#
# where $n$ and $m$ are spherical harmonic degree and order, respectively, and the ($g_n^m, h_n^m$) are the Gauss coefficients for a particular model (e.g. the IGRF).
#
# The Associated Legendre functions of degree $n$ and order $m$ are defined, in Schmidt semi-normalised form by
#
# $$P^m_n(x) = \frac{1}{2^n n!}\left[ \frac{(2-\delta_{0m})(n-m)!\left(1-x^2\right)^m}{(n+m)!} \right]^{1/2}\frac{d^{n+m}}{dx^{n+m}}\left(1-x^2\right)^{n},$$
#
#
# where $x = \cos(\theta)$. The following recurrence relations
#
# $$\begin{align}
# P_n^n&=\left(1-\frac{1}{2n}\right)^{1/2}\sin \theta \thinspace P_{n-1}^{n-1} \\[6pt]
# P_n^m&=\left[\left(2n-1\right) \cos \theta \thinspace P_{n-1}^m-\left[ \left(n-1\right)^2-m^2\right]^{1/2}P_{n-2}^m\right]\left(n^2-m^2\right)^{-1/2},\\[6pt]
# \end{align}$$
#
# (e.g. Malin and Barraclough, 1981) may be used to calculate the $X^m_n$, $Y^m_n$ and $Z^m_n$, for example
#
# $$\begin{align}
# X_n^n&=\left(1-\frac{1}{2n}\right)^{1/2}\left( \sin \theta \thinspace X_{n-1}^{n-1}+ \cos \theta \thinspace P_{n-1}^{n-1} \right)\\[6pt]
# X_n^m&=\left[\left(2n-1\right) \cos \theta \thinspace X_{n-1}^m- \sin \theta \thinspace P_{n-1}^m\right] - \left[ \left(n-1\right)^2-m^2\right]^{1/2}X_{n-2}^m\left(n^2-m^2\right)^{-1/2}.
# \end{align}$$
# # 2. Plotting $P_n^m$ and $X_n^m$
# The $P_n^m(\theta)$ and $X_n^m(\theta)$ are building blocks for computing geomagnetic field models given a spherical harmonic model. It's instructive to visualise these functions and below you can experiment by setting different values of spherical harmonic degree ($n$) and order ($m \le n$). Note how the choice of $n$ and $m$ affects the number of zeroes of the functions.
#
# The functions are plotted on a semi-circle representing the surface of the Earth, with the inner core added for cosmetic purposes only! Again, purely for cosmetic purposes, the functions are scaled to fit within $\pm$10% of the Earth's surface.
# ### >> USER INPUT HERE: Set the spherical harmonic degree and order for the plot
# In[ ]:
degree = 13
order = 5
# In[ ]:
# Calculate Pnm and Xmn values every 0.5 degrees
colat = np.linspace(0,180,361)
pnmvals = np.zeros(len(colat))
xnmvals = np.zeros(len(colat))
idx = sha.pnmindex(degree,order)
for i, cl in enumerate(colat):
p,x = sha.pxyznm_calc(degree, cl)[0:2]
pnmvals[i] = p[idx]
xnmvals[i] = x[idx]
theta = np.deg2rad(colat)
ct = np.cos(theta)
st = np.sin(theta)
# Numbers mimicking the Earth's surface and outer core radii
e_rad = 6.371
c_rad = 3.485
# Scale values to fit within 10% of "Earth's surface". Firstly the P(n,m),
shell = 0.1*e_rad
pmax = np.abs(pnmvals).max()
pnmvals = pnmvals*shell/pmax + e_rad
xp = pnmvals*st
yp = pnmvals*ct
# and now the X(n,m)
xmax = np.abs(xnmvals).max()
xnmvals = xnmvals*shell/xmax + e_rad
xx = xnmvals*st
yx = xnmvals*ct
# Values to draw the Earth's and outer core surfaces as semi-circles
e_xvals = e_rad*st
e_yvals = e_rad*ct
c_xvals = e_xvals*c_rad/e_rad
c_yvals = e_yvals*c_rad/e_rad
# Earth-like background framework for plots
def eplot(ax):
ax.set_aspect('equal')
ax.set_axis_off()
ax.plot(e_xvals,e_yvals, color='blue')
ax.plot(c_xvals,c_yvals, color='black')
ax.fill_between(c_xvals, c_yvals, y2=0, color='lightgrey')
ax.plot((0, 0), (-e_rad, e_rad), color='black')
# Plot the P(n,m) and X(n,m)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(18, 8))
fig.suptitle('Degree (n) = '+str(degree)+', order (m) = '+str(order), fontsize=20)
axes[0].plot(xp,yp, color='red')
axes[0].set_title('P('+ str(degree)+',' + str(order)+')', fontsize=16)
eplot(axes[0])
axes[1].plot(xx,yx, color='red')
axes[1].set_title('X('+ str(degree)+',' + str(order)+')', fontsize=16)
eplot(axes[1])
# # 3. The International Geomagnetic Reference Field
# The latest version of the IGRF is IGRF12 which consists of a main-field model every five years from 1900.0 to 2015.0 and a secular variation model for 2015-2020. The main field models have spherical harmonic degree (n) and order (m) 10 up to 1995 and n=m=13 from 2000 onwards. The secular variation model has n=m=8.
#
# The coefficients are first loaded into a pandas database:
# In[ ]:
igrf12 = pd.read_csv(IGRF12_FILE, delim_whitespace=True, header=3)
igrf12.head() # Check the values have loaded correctly
# ## a) Calculating geomagnetic field values using the IGRF
# The function below calculates geomagnetic field values at a point defined by its colatitude, longitude and altitude, using a spherical harmonic model of maximum degree _nmax_ supplied as an array _gh_. The parameter _coord_ is a string specifying whether the input position is in geocentric coordinates (when _altitude_ should be the geocentric distance in km) or geodetic coordinates (when altitude is distance above mean sea level in km).
#
# It's unconventional, but I've chosen to include a monopole term, set to zero, at index zero in the _gh_ array.
# In[ ]:
def shm_calculator(gh, nmax, altitude, colat, long, coord):
RREF = 6371.2 #The reference radius assumed by the IGRF
degree = nmax
phi = long
if (coord == 'Geodetic'):
# Geodetic to geocentric conversion using the WGS84 spheroid
rad, theta, sd, cd = sha.gd2gc(altitude, colat)
else:
rad = altitude
theta = colat
# Function 'rad_powers' to create an array with values of (a/r)^(n+2) for n = 0,1, 2 ..., degree
rpow = sha.rad_powers(degree, RREF, rad)
# Function 'csmphi' to create arrays with cos(m*phi), sin(m*phi) for m = 0, 1, 2 ..., degree
cmphi, smphi = sha.csmphi(degree,phi)
# Function 'gh_phi_rad' to create arrays with terms such as [g(3,2)*cos(2*phi) + h(3,2)*sin(2*phi)]*(a/r)**5
ghxz, ghy = sha.gh_phi_rad(gh, degree, cmphi, smphi, rpow)
# Function 'pnm_calc' to calculate arrays of the Associated Legendre Polynomials for n (&m) = 0,1, 2 ..., degree
pnm, xnm, ynm, znm = sha.pxyznm_calc(degree, theta)
# Geomagnetic field components are calculated as a dot product
X = np.dot(ghxz, xnm)
Y = np.dot(ghy, ynm)
Z = -np.dot(ghxz, znm)
# Convert back to geodetic (X, Y, Z) if required
if (coord == 'Geodetic'):
t = X
X = X*cd + Z*sd
Z = Z*cd - t*sd
return((X, Y, Z))
# ### >> >> USER INPUT HERE: Set the input parameters
# In[ ]:
location = 'Erehwon'
ctype = 'Geodetic' # coordinate type
altitude = 0 # in km above the spheroid if ctype = 'Geodetic', radial distance if ctype = 'Geocentric'
colat = 35 # NB colatitude, not latitude
long = -3 # longitude
NMAX = 13 # Maxiimum spherical harmonic degree of the model
date = 2015.0 # Date for the field estimates
# Now calculate the IGRF geomagnetic field estimates.
# In[ ]:
# Calculate the gh values for the supplied date
if date == 2015.0:
gh = igrf12['2015.0']
elif date < 2015.0:
date_1 = (date//5)*5
date_2 = date_1 + 5
w1 = date-date_1
w2 = date_2-date
gh = np.array((w2*igrf12[str(date_1)] + w1*igrf12[str(date_2)])/(w1+w2))
elif date > 2015.0:
gh =np.array(igrf12['2015.0'] + (date-2015.0)*igrf12['2015-20'])
gh = np.append(0., gh) # Add a zero monopole term corresponding to g(0,0)
bxyz = shm_calculator(gh, NMAX, altitude, colat, long, ctype)
dec, hoz ,inc , eff = mag.xyz2dhif(bxyz[0], bxyz[1], bxyz[2])
print('\nGeomagnetic field values at: ', location+', '+ str(date), '\n')
print('Declination (D):', '{: .1f}'.format(dec), 'degrees')
print('Inclination (I):', '{: .1f}'.format(inc), 'degrees')
print('Horizontal intensity (H):', '{: .1f}'.format(hoz), 'nT')
print('Total intensity (F) :', '{: .1f}'.format(eff), 'nT')
print('North component (X) :', '{: .1f}'.format(bxyz[0]), 'nT')
print('East component (Y) :', '{: .1f}'.format(bxyz[1]), 'nT')
print('Vertical component (Z) :', '{: .1f}'.format(bxyz[2]), 'nT')
# # b) Maps of the IGRF
# Now draw maps of the IGRF at the date selected above. The latitude range is set at -85 degrees to +85 degrees and the longitude range -180 degrees to +180 degrees and IGRF values for (X, Y, Z) are calculated on a 5 degree grid (this may take a few seconds to complete).
#
# ## >> >> USER INPUT HERE: Set the element to plot
# Enter the geomagnetic element to plot below:
# D = declination
# H = horizontal intensity
# I = inclination
# X = north component
# Y = east component
# Z = vertically (downwards) component
# F = total intensity.)
# In[ ]:
el2plot = 'H'
# In[ ]:
def IGRF_plotter(el_name, vals, date):
if el_name=='D':
cvals = np.arange(-25,30,5)
else:
cvals = 15
fig, ax = plt.subplots(figsize=(16, 8))
cplt = ax.contour(longs, lats, vals, levels=cvals)
ax.clabel(cplt, cplt.levels, inline=True, fmt='%d', fontsize=10)
ax.set_title('IGRF: '+ el_name + ' (' + str(date) + ')', fontsize=20)
ax.set_xlabel('Longitude', fontsize=16)
ax.set_ylabel('Latitude', fontsize=16)
# In[ ]:
longs = np.linspace(-180, 180, 73)
lats = np.linspace(-85, 85, 35)
Bx, By, Bz = zip(*[sha.shm_calculator(gh,13,6371.2,90-lat,lon,'Geocentric') \
for lat in lats for lon in longs])
X = np.asarray(Bx).reshape(35,73)
Y = np.asarray(By).reshape(35,73)
Z = np.asarray(Bz).reshape(35,73)
D, H, I, F = [mag.xyz2dhif(X, Y, Z)[el] for el in range(4)]
el_dict={'X':X, 'Y':Y, 'Z':Z, 'D':D, 'H':H, 'I':I, 'F':F}
IGRF_plotter(el2plot, el_dict[el2plot], date)
# ### Exercise
# Produce a similar plot for the secular variation according to the IGRF.
# ### References
#
# Malin, S. R. . and Barraclough, D., (1981). An algorithm for synthesizing the geomagnetic field, Computers & Geosciences. Pergamon, 7(4), pp. 401–405. doi: 10.1016/0098-3004(81)90082-0.