"""International Geomagnetic Reference Field model
This module is intended primarily to support :mod:`~spacepy.coordinates`
rather than for direct end use, and the interface is subject to change.
"""
# std. lib.
import os
import datetime as dt
import warnings
# scientific stack
import numpy as np
# third-party or current
from spacepy import __path__ as basepath
from spacepy import DOT_FLN
import spacepy.time as spt
[docs]
class IGRFCoefficients():
"""Read and store IGRF coefficients from data file
Other Parameters
----------------
fname : str, optional
Filename to read from; defaults from the .spacepy data directory.
"""
[docs]
def __init__(self, fname=None):
# Store coefficients and SV in nested arrays
# This is triangular, e.g.,
# g[2] has 3 elements (g[2][0:2])
# h[5] has 5 elements (h[5][0:5])
# Store top level as object arrays so the arrays inside
# can have staggered lengths
self.coeffs = {'g': np.empty(14, dtype=object),
'h': np.empty(14, dtype=object),
'g_SV': np.empty(14, dtype=object),
'h_SV': np.empty(14, dtype=object),
}
# Open IGRF coefficients file...
if fname is None:
# Updates shoud be downloaded by toolbox.update and
# put in the .spacepy/data directory
# TODO: write getter for toolbox.update
fname = os.path.join(DOT_FLN, 'data', 'igrfcoeffs.txt')
if not os.path.exists(fname):
# Fall back to IGRF13 coefficients
fname = os.path.join('{0}'.format(basepath[0]), 'data', 'igrf13coeffs.txt')
with open(fname) as fh:
header = [fh.readline() for i in range(4)]
data = fh.readlines()
epochs = [float(yy) for yy in header[-1].strip().split()[3:-1]]
self.epochs = np.array(epochs)
self.datelow = dt.datetime(int(self.epochs[0]), 1, 1)
self.lastepoch = dt.datetime(int(self.epochs[-1]), 1, 1)
self.datehigh = dt.datetime(int(self.epochs[-1])+5, 1, 1)
n_epochs = len(epochs)
# Now we know how many epochs are in the current IGRF file,
# we can construct the arrays to store the coefficients
for n_idx in range(0, 14):
self.coeffs['g'][n_idx] = np.empty((n_idx+1, n_epochs))
self.coeffs['h'][n_idx] = np.empty((n_idx+1, n_epochs))
self.coeffs['g_SV'][n_idx] = np.empty((n_idx+1))
self.coeffs['h_SV'][n_idx] = np.empty((n_idx+1))
# Parse the file and populat the coefficient arrays
for line in data:
line = line.strip().split()
# Read g and h coefficients into tables
vals = [float(v) for v in line[3:]]
n_idx = int(line[1])
m_idx = int(line[2])
self.coeffs[line[0]][n_idx][m_idx, ...] = vals[:-1]
svtag = 'g_SV' if line[0] == 'g' else 'h_SV'
self.coeffs[svtag][n_idx][m_idx] = vals[-1]
# set arrays to read only
for key in ['g', 'h', 'g_SV', 'h_SV']:
for arr in self.coeffs[key]:
arr.setflags(write=0)
# load coefficients so it doesn't get done on instantiation of IGRF class
igrfcoeffs = IGRFCoefficients()
[docs]
class IGRF():
"""International Geomagnetic Reference Field model
Notes
-----
.. versionadded:: 0.3.0
"""
dipole = {}
"""Characteristics of dipole (`dict`)."""
moment = {}
"""Dipole moments (`dict`)."""
[docs]
def __init__(self):
self.__status = {'coeffs': False,
'init': False,
'time_set': False,
}
self.__coeffs = igrfcoeffs.coeffs
self.__status['coeffs'] = True
[docs]
def initialize(self, time, limits='warn'):
"""Initialize model state to a particular time.
Parameters
----------
time : `~datetime.datetime`
Time for which to initialize the model
Other Parameters
----------------
limits : `str`, optional
Set to ``warn`` to warn about out-of-range times (default);
any other value to error.
"""
errmsg = 'IGRF: Requested time is outside valid range.\n'
errmsg += 'Valid range is [{0}, {1}]'.format(igrfcoeffs.datelow, igrfcoeffs.datehigh)
self.time = time
if time < igrfcoeffs.datelow or time > igrfcoeffs.datehigh:
if limits.lower() == 'warn':
errmsg += '\nProceeding using effective date {0}'.format(igrfcoeffs.datehigh)
warnings.warn(errmsg, stacklevel=2)
self.time = igrfcoeffs.datehigh
else:
errmsg += '''\nUse "limits='warn'" to force out-of-range times '''
errmsg += 'to use the nearest valid endpoint.'
raise ValueError(errmsg)
self.__valid_extrap = True if (self.time <= igrfcoeffs.datehigh
and self.time > igrfcoeffs.lastepoch) else False
self.__status['time_set'] = False
self.__status['init'] = True
self.calcDipoleAxis()
self.__status['time_set'] = True
[docs]
def calcDipoleAxis(self):
"""Calculates dipole axis for initialized time.
Populates :data:`moment` and :data:`dipole`.
"""
if self.__status['time_set']:
return
if not self.__status['init']:
warnings.warn('IGRF: Initialize for a time before invoking this method.',
stacklevel=2)
return
# Find the indices for the epochs surrounding input time
utc = self.time
tstamp = spt.Ticktock(utc, 'UTC').MJD[0]
# Compute the various IGRF dependent things like position of CD
# TODO: So that full IGRF can be used eventually, for each in
# __coeffs, interp to time and save as '[g|h][n][m]' e.g., 'h32' (entries in self.coeffs)
if not self.__valid_extrap:
# Interpolate coefficients linearly
numepochs = [spt.Ticktock(dt.datetime(int(val), 1, 1)).MJD[0] for val in igrfcoeffs.epochs]
g10 = np.interp(tstamp, numepochs, self.__coeffs['g'][1][0])
g11 = np.interp(tstamp, numepochs, self.__coeffs['g'][1][1])
h11 = np.interp(tstamp, numepochs, self.__coeffs['h'][1][1])
else:
# Extrapolate using secular variation
ref_ep = spt.Ticktock(dt.datetime(int(igrfcoeffs.epochs[-1]), 1, 1)).MJD[0]
diff_years = ((tstamp % 1 + tstamp//1) - (ref_ep % 1 + ref_ep//1))/365.25 # in years
g10 = self.__coeffs['g_SV'][1][0]*diff_years + self.__coeffs['g'][1][0][-1]
g11 = self.__coeffs['g_SV'][1][1]*diff_years + self.__coeffs['g'][1][1][-1]
h11 = self.__coeffs['h_SV'][1][1]*diff_years + self.__coeffs['h'][1][1][-1]
mom_sq = g10*g10 + g11*g11 + h11*h11
mom = np.sqrt(mom_sq)
# Compute dipole moments.
self.moment = {'cd': mom,
'cd_McIlwain': 31165.3,
'cd_2010': 29950.126496,
}
self.dipole = dict()
self.dipole['cd_gcolat_rad'] = np.pi - np.arccos(g10/mom)
self.dipole['cd_glon_rad'] = np.arctan(h11/g11)
self.dipole['cd_gcolat'] = np.rad2deg(self.dipole['cd_gcolat_rad'])
self.dipole['cd_glon'] = np.rad2deg(self.dipole['cd_glon_rad'])