Passed
Push — master ( d1ac5b...af9f8c )
by Stefan
03:57
created

sciapy.level2.igrf._load_igrf_file()   A

Complexity

Conditions 2

Size

Total Lines 28
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 11
nop 1
dl 0
loc 28
ccs 11
cts 11
cp 1
crap 2
rs 9.85
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
# vim:fileencoding=utf-8
3
#
4
# Copyright (c) 2017-2018 Stefan Bender
5
#
6
# This file is part of sciapy.
7
# sciapy is free software: you can redistribute it or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, version 2.
10
# See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html.
11 1
"""IGRF geomagnetic coordinates
12
13
This is a python (mix) version of GMPOLE and GMCOORD from
14
http://www.ngdc.noaa.gov/geomag/geom_util/utilities_home.shtml
15
to transform geodetic to geomagnetic coordinates.
16
It uses the IGRF 2012 model and coefficients [#]_.
17
18
.. [#] Thébault et al. 2015,
19
	International Geomagnetic Reference Field: the 12th generation.
20
	Earth, Planets and Space, 67 (79)
21
	http://nora.nerc.ac.uk/id/eprint/511258
22
	https://doi.org/10.1186/s40623-015-0228-9
23
"""
24 1
from __future__ import absolute_import, division, print_function
25
26 1
import logging
27 1
from collections import namedtuple
28 1
from pkg_resources import resource_filename
29
30 1
import numpy as np
31 1
from scipy.interpolate import interp1d
32 1
from scipy.special import lpmn
33
34 1
__all__ = ['gmpole', 'gmag_igrf']
35
36
# The WGS84 reference ellipsoid
37 1
Earth_ellipsoid = {
38
	"a": 6378.137,  # semi-major axis of the ellipsoid in km
39
	"b": 6356.7523142,  # semi-minor axis of the ellipsoid in km
40
	"fla": 1. / 298.257223563,  # flattening
41
	"re": 6371.2  # Earth's radius in km
42
}
43
44 1
def _ellipsoid(ellipsoid_data=Earth_ellipsoid):
45
	# extends the dictionary with the eccentricities
46 1
	ell = namedtuple('ellip', ["a", "b", "fla", "eps", "epssq", "re"])
47 1
	ell.a = ellipsoid_data["a"]
48 1
	ell.b = ellipsoid_data["b"]
49 1
	ell.fla = ellipsoid_data["fla"]
50 1
	ell.re = ellipsoid_data["re"]
51
	# first eccentricity squared
52 1
	ell.epssq = 1. - ell.b**2 / ell.a**2
53
	# first eccentricity
54 1
	ell.eps = np.sqrt(ell.epssq)
55 1
	return ell
56
57 1
def _date_to_frac_year(year, month, day):
58
	# fractional year by dividing the day of year by the overall
59
	# number of days in that year
60 1
	extraday = 0
61 1
	if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0):
62 1
		extraday = 1
63 1
	month_days = [0, 31, 28 + extraday, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
64 1
	doy = np.sum(month_days[:month]) + day
65 1
	return year + (doy - 1) / (365.0 + extraday)
66
67 1
def _load_igrf_file(filename="IGRF.tab"):
68
	"""Load IGRF coefficients
69
70
	Parameters
71
	----------
72
	filename: str, optional
73
		The file with the IGRF coefficients.
74
75
	Returns
76
	-------
77
	interpol: `scipy.interpolate.interp1d` instance
78
		Interpolator instance, called with the fractional
79
		year to obtain the IGRF coefficients for the epoch.
80
	"""
81 1
	igrf_tab = np.genfromtxt(filename, skip_header=3, dtype=None)
82
83 1
	sv = igrf_tab[igrf_tab.dtype.names[-1]][1:].astype(np.float)
84
85 1
	years = np.asarray(igrf_tab[0].tolist()[3:-1])
86 1
	years = np.append(years, [years[-1] + 5])
87
88 1
	coeffs = []
89 1
	for i in range(1, len(igrf_tab)):
90 1
		coeff = np.asarray(igrf_tab[i].tolist()[3:-1])
91 1
		coeff = np.append(coeff, np.array([5]) * sv[0] + coeff[-1])
92 1
		coeffs.append(coeff)
93
94 1
	return interp1d(years, coeffs)
95
96 1
def _geod_to_spher(phi, lon, Ellip, HeightAboveEllipsoid=0.):
97
	"""Convert geodetic to spherical coordinates
98
99
	Converts geodetic coordinates on the WGS-84 reference ellipsoid
100
	to Earth-centered Earth-fixed Cartesian coordinates,
101
	and then to spherical coordinates.
102
	"""
103 1
	CosLat = np.cos(np.radians(phi))
104 1
	SinLat = np.sin(np.radians(phi))
105
106
	# compute the local radius of curvature on the WGS-84 reference ellipsoid
107 1
	rc = Ellip.a / np.sqrt(1.0 - Ellip.epssq * SinLat**2)
108
109
	# compute ECEF Cartesian coordinates of specified point (for longitude=0)
110 1
	xp = (rc + HeightAboveEllipsoid) * CosLat
111 1
	zp = (rc * (1.0 - Ellip.epssq) + HeightAboveEllipsoid) * SinLat
112
113
	# compute spherical radius and angle phi of specified point
114 1
	rg = np.sqrt(xp**2 + zp**2)
115
	# geocentric latitude
116 1
	phig = np.degrees(np.arcsin(zp / rg))
117 1
	return phig, lon, rg
118
119 1
def _igrf_model(coeffs, Lmax, r, theta, phi, R_E=Earth_ellipsoid["re"]):
120
	"""Evaluates the IGRF model function at the given location
121
	"""
122
	rho = R_E / r
123
	sin_theta = np.sin(theta)
124
	cos_theta = np.cos(theta)
125
	Plm, dPlm = lpmn(Lmax, Lmax, cos_theta)
126
	logging.debug("R_E: %s, r: %s, rho: %s", R_E, r, rho)
127
	logging.debug("rho: %s, theta: %s, sin_theta: %s, cos_theta: %s",
128
			rho, theta, sin_theta, cos_theta)
129
	Bx, By, Bz = 0., 0., 0.  # Btheta, Bphi, Br
130
	idx = 0
131
	rho_l = rho
132
	K_l1 = 1.
133
	for l in range(1, Lmax + 1):
134
		rho_l *= rho  # rho^(l+1)
135
		# m = 0 values, h_l^m = 0
136
		Bxl = rho_l * coeffs[idx] * dPlm[0, l] * (-sin_theta)
137
		Byl = 0.
138
		Bzl = -(l + 1) * rho_l * coeffs[idx] * Plm[0, l]
139
		if l > 1:
140
			K_l1 *= np.sqrt((l - 1) / (l + 1))
141
		idx += 1
142
		K_lm = -K_l1
143
		for m in range(1, l + 1):
144
			cfi = K_lm * rho_l * (coeffs[idx] * np.cos(m * phi)
145
						+ coeffs[idx + 1] * np.sin(m * phi))
146
			Bxl += cfi * dPlm[m, l] * (-sin_theta)
147
			Bzl += -(l + 1) * cfi * Plm[m, l]
148
			if sin_theta != 0:
149
				Byl += K_lm * rho_l * m * Plm[m, l] * (
150
						- coeffs[idx] * np.sin(m * phi) +
151
						coeffs[idx + 1] * np.cos(m * phi))
152
			else:
153
				Byl += 0.
154
			if m < l:
155
				# K_lm for the next m
156
				K_lm /= -np.sqrt((l + m + 1) * (l - m))
157
			idx += 2
158
		Bx += Bxl
159
		By += Byl
160
		Bz += Bzl
161
	Bx = rho * Bx
162
	By = -rho * By / sin_theta
163
	Bz = rho * Bz
164
	return Bx, By, Bz
165
166 1
def igrf_mag(date, lat, lon, alt, filename="IGRF.tab"):
167
	"""Evaluate the local magnetic field using the IGRF model
168
169
	Evaluates the IGRF coefficients to calculate the Earth's
170
	magnetic field at the given location.
171
	The results agree to within a few decimal places with
172
	https://ngdc.noaa.gov/geomag-web/
173
174
	Parameters
175
	----------
176
	date: `datetime.date` or `datetime.datetime` instance
177
		The date for the evaluation.
178
	lat: float
179
		Geographic latitude in degrees north
180
	lon: float
181
		Geographic longitude in degrees east
182
	alt: float
183
		Altitude above ground in km.
184
	filename: str, optional
185
		File containing the IGRF coefficients.
186
187
	Returns
188
	-------
189
	Bx: float
190
		Northward component of the magnetic field, B_N.
191
	By: float
192
		Eastward component of the magnetic field, B_E.
193
	Bz: float
194
		Downward component of the magnetic field, B_D.
195
	"""
196
	ellip = _ellipsoid()
197
	# date should be datetime.datetime or datetime.date instance,
198
	# or something else that provides .year, .month, and .day attributes
199
	frac_year = _date_to_frac_year(date.year, date.month, date.day)
200
	glat, glon, grad = _geod_to_spher(lat, lon, ellip, alt)
201
	sin_theta = np.sin(np.radians(90. - glat))
202
	cos_theta = np.cos(np.radians(90. - glat))
203
204
	rho = np.sqrt((ellip.a * sin_theta)**2 + (ellip.b * cos_theta)**2)
205
	r = np.sqrt(alt**2 + 2 * alt * rho +
206
			(ellip.a**4 * sin_theta**2 + ellip.b**4 * cos_theta**2) / rho**2)
207
	cd = (alt + rho) / r
208
	sd = (ellip.a**2 - ellip.b**2) / rho * cos_theta * sin_theta / r
209
	logging.debug("rho: %s, r: %s, (alt + rho) / r: %s, R_E / (R_E + h): %s, R_E / r: %s",
210
			rho, r, cd, ellip.re / (ellip.re + alt), ellip.re / r)
211
212
	cos_theta, sin_theta = cos_theta*cd - sin_theta*sd, sin_theta*cd + cos_theta*sd
213
	logging.debug("r: %s, spherical coordinates (radius, rho, theta, lat): %s, %s, %s, %s",
214
			r, grad, rho, np.degrees(np.arccos(cos_theta)), 90. - glat)
215
216
	# evaluate the IGRF model in spherical coordinates
217
	igrf_file = resource_filename(__name__, filename)
218
	igrf_coeffs = _load_igrf_file(igrf_file)(frac_year)
219
	Bx, By, Bz = _igrf_model(igrf_coeffs, 13, r, np.radians(90. - glat), np.radians(glon))
220
	logging.debug("spherical geomagnetic field (Bx, By, Bz): %s, %s, %s", Bx, By, Bz)
221
	logging.debug("spherical dip coordinates: lat %s, lon %s",
222
			np.degrees(np.arctan2(0.5 * Bz,  np.sqrt(Bx**2 + By**2))),
223
			np.degrees(np.arctan2(-By, Bz)))
224
	# back to geodetic coordinates
225
	Bx, Bz = cd * Bx + sd * Bz, cd * Bz - sd * Bx
226
	logging.debug("geodetic geomagnetic field (Bx, By, Bz): %s, %s, %s", Bx, By, Bz)
227
	logging.debug("geodetic dip coordinates: lat %s, lon %s",
228
			np.degrees(np.arctan2(0.5 * Bz, np.sqrt(Bx**2 + By**2))),
229
			np.degrees(np.arctan2(-By, Bz)))
230
	return Bx, By, Bz
231
232 1
def gmpole(date, r_e=Earth_ellipsoid["re"], filename="IGRF.tab"):
233
	"""Centered dipole geomagnetic pole coordinates
234
235
	Parameters
236
	----------
237
	date: datetime.datetime or datetime.date
238
	r_e: float, optional
239
		Earth radius to evaluate the dipole's off-centre shift.
240
	filename: str, optional
241
		File containing the IGRF parameters.
242
243
	Returns
244
	-------
245
	(lat_n, phi_n): tuple of floats
246
		Geographic latitude and longitude of the centered dipole
247
		magnetic north pole.
248
	(lat_s, phi_s): tuple of floats
249
		Geographic latitude and longitude of the centered dipole
250
		magnetic south pole.
251
	(dx, dy, dz): tuple of floats
252
		Magnetic variations in Earth-centered Cartesian coordinates
253
		for shifting the dipole off-center.
254
	(dX, dY, dZ): tuple of floats
255
		Magnetic variations in centered-dipole Cartesian coordinates
256
		for shifting the dipole off-center.
257
	B_0: float
258
		The magnitude of the magnetic field.
259
	"""
260 1
	igrf_file = resource_filename(__name__, filename)
261 1
	gh_func = _load_igrf_file(igrf_file)
262
263 1
	frac_year = _date_to_frac_year(date.year, date.month, date.day)
264 1
	logging.debug("fractional year: %s", frac_year)
265 1
	g10, g11, h11, g20, g21, h21, g22, h22 = gh_func(frac_year)[:8]
266
267
	# This function finds the location of the north magnetic pole in spherical coordinates.
268
	# The equations are from Wallace H. Campbell's "Introduction to Geomagnetic Fields"
269
	# and Fraser-Smith 1987, Eq. (5).
270
	# For the minus signs see also
271
	# Laundal and Richmond, Space Sci. Rev. (2017) 206:27--59,
272
	# doi:10.1007/s11214-016-0275-y: (p. 31)
273
	# "The Earth’s field is such that the dipole axis points roughly southward,
274
	# so that the dipole North Pole is really in the Southern Hemisphere (SH).
275
	# However convention dictates that the axis of the geomagnetic dipole is
276
	# positive northward, hence the negative sign in the definition of mˆ."
277
	# Note that hence Phi_N in their Eq. (14) is actually Phi_S.
278 1
	B_0_sq = g10**2 + g11**2 + h11**2
279 1
	theta_n = np.arccos(-g10 / np.sqrt(B_0_sq))
280 1
	phi_n = np.arctan2(-h11, -g11)
281 1
	lat_n = 0.5 * np.pi - theta_n
282 1
	logging.debug("centered dipole north pole coordinates "
283
			"(lat, theta, phi): %s, %s, %s",
284
			np.degrees(lat_n), np.degrees(theta_n), np.degrees(phi_n))
285
286
	# dipole offset according to Fraser-Smith, 1987
287 1
	L_0 = 2 * g10 * g20 + np.sqrt(3) * (g11 * g21 + h11 * h21)
288 1
	L_1 = -g11 * g20 + np.sqrt(3) * (g10 * g21 + g11 * g22 + h11 * h22)
289 1
	L_2 = -h11 * g20 + np.sqrt(3) * (g10 * h21 - h11 * g22 + g11 * h22)
290 1
	E = (L_0 * g10 + L_1 * g11 + L_2 * h11) / (4 * B_0_sq)
291
292
	# dipole offset in geodetic Cartesian coordinates
293 1
	xi = (L_0 - g10 * E) / (3 * B_0_sq)
294 1
	eta = (L_1 - g11 * E) / (3 * B_0_sq)
295 1
	zeta = (L_2 - h11 * E) / (3 * B_0_sq)
296 1
	dx = eta * r_e
297 1
	dy = zeta * r_e
298 1
	dz = xi * r_e
299 1
	logging.debug("dx, dy, dz: %s, %s, %s", dx, dy, dz)
300
301
	# dipole offset in geodetic spherical coordinates
302
	# Fraser-Smith 1987, Eq. (24)
303 1
	delta = np.sqrt(dx**2 + dy**2 + dz**2)
304 1
	theta_d = np.arccos(dz / delta)
305 1
	lambda_d = 0.5 * np.pi - theta_d
306 1
	phi_d = np.arctan2(dy, dx)
307 1
	logging.debug(
308
		"delta: %s, theta_d: %s, phi_d: %s",
309
		delta, np.degrees(theta_d), np.degrees(phi_d),
310
	)
311
312
	# dipole offset in centred-dipole spherical coordindates
313 1
	sin_lat_ed = (np.sin(lambda_d) * np.sin(lat_n)
314
				+ np.cos(lambda_d) * np.cos(lat_n) * np.cos(phi_d - phi_n))
315 1
	lat_ed = np.arcsin(sin_lat_ed)
316 1
	theta_ed = 0.5 * np.pi - lat_ed
317 1
	sin_lon_ed = np.sin(theta_d) * np.sin(phi_d - phi_n) / np.sin(theta_ed)
318 1
	lon_ed = np.pi - np.arcsin(sin_lon_ed)
319 1
	logging.debug("eccentric dipole offset in CD coordinates "
320
			"(lat, theta, lon): %s, %s, %s",
321
			np.degrees(theta_ed), np.degrees(lat_ed), np.degrees(lon_ed))
322
323
	# dipole offset in centred-dipole Cartesian coordindates
324 1
	dX = delta * np.sin(theta_ed) * np.cos(lon_ed)
325 1
	dY = delta * np.sin(theta_ed) * np.sin(lon_ed)
326 1
	dZ = delta * np.cos(theta_ed)
327 1
	logging.debug("magnetic variations (dX, dY, dZ): %s, %s, %s", dX, dY, dZ)
328
329
	# North pole, south pole coordinates
330 1
	return ((np.degrees(lat_n), np.degrees(phi_n)),
331
			(-np.degrees(lat_n), np.degrees(phi_n + np.pi)),
332
			(dx, dy, dz),
333
			(dX, dY, dZ),
334
			np.sqrt(B_0_sq))
335
336 1
def gmag_igrf(date, lat, lon, alt=0.,
337
		centered_dipole=False,
338
		igrf_name="IGRF.tab"):
339
	"""Centered or eccentric dipole geomagnetic coordinates
340
341
	Parameters
342
	----------
343
	date: datetime.datetime
344
	lat: float
345
		Geographic latitude in degrees north
346
	lon: float
347
		Geographic longitude in degrees east
348
	alt: float, optional
349
		Altitude in km. Default: 0.
350
	centered_dipole: bool, optional
351
		Returns the centered dipole geomagnetic coordinates
352
		if set to True, returns the eccentric dipole
353
		geomagnetic coordinates if set to False.
354
		Default: False
355
	igrf_name: str, optional
356
		Default: "IGRF.tab"
357
358
	Returns
359
	-------
360
	geomag_latitude: numpy.ndarray or float
361
		Geomagnetic latitude in eccentric dipole coordinates,
362
		centered dipole coordinates if `centered_dipole` is True.
363
	geomag_longitude: numpy.ndarray or float
364
		Geomagnetic longitude in eccentric dipole coordinates,
365
		centered dipole coordinates if `centered_dipole` is True.
366
	"""
367 1
	ellip = _ellipsoid()
368 1
	glat, glon, grad = _geod_to_spher(lat, lon, ellip, alt)
369 1
	(lat_GMP, lon_GMP), _, _, (dX, dY, dZ), B_0 = gmpole(date, ellip.re, igrf_name)
370 1
	latr, lonr = np.radians(glat), np.radians(glon)
371 1
	lat_GMPr, lon_GMPr = np.radians(lat_GMP), np.radians(lon_GMP)
372 1
	sin_lat_gmag = (np.sin(latr) * np.sin(lat_GMPr)
373
				+ np.cos(latr) * np.cos(lat_GMPr) * np.cos(lonr - lon_GMPr))
374 1
	lon_gmag_y = np.cos(latr) * np.sin(lonr - lon_GMPr)
375 1
	lon_gmag_x = (np.cos(latr) * np.sin(lat_GMPr) * np.cos(lonr - lon_GMPr)
376
				- np.sin(latr) * np.cos(lat_GMPr))
377 1
	lat_gmag = np.arcsin(sin_lat_gmag)
378 1
	lat_gmag_geod = np.arctan2(np.tan(lat_gmag), (1. - ellip.epssq))
379
380 1
	B_r = -2. * B_0 * sin_lat_gmag / (grad / ellip.re)**3
381 1
	B_th = -B_0 * np.cos(lat_gmag) / (grad / ellip.re)**3
382 1
	logging.debug("B_r: %s, B_th: %s, dip lat: %s",
383
			-B_r, B_th, np.degrees(np.arctan2(0.5 * B_r, B_th)))
384
385 1
	lon_gmag = np.arctan2(lon_gmag_y, lon_gmag_x)
386 1
	logging.debug("centered dipole coordinates: "
387
			"lat_gmag: %s, lon_gmag: %s",
388
			np.degrees(lat_gmag), np.degrees(lon_gmag))
389 1
	logging.debug("lat_gmag_geod: %s, lat_GMPr: %s",
390
			np.degrees(lat_gmag_geod), np.degrees(lat_GMPr))
391 1
	if centered_dipole:
392
		return (np.degrees(lat_gmag), np.degrees(lon_gmag))
393
394
	# eccentric dipole coordinates (shifted dipole)
395 1
	phi_ed = np.arctan2(
396
		(grad * np.cos(lat_gmag) * np.sin(lon_gmag) - dY),
397
		(grad * np.cos(lat_gmag) * np.cos(lon_gmag) - dX)
398
	)
399 1
	theta_ed = np.arctan2(
400
		(grad * np.cos(lat_gmag) * np.cos(lon_gmag) - dX),
401
		(grad * np.sin(lat_gmag) - dZ) * np.cos(phi_ed)
402
	)
403 1
	lat_ed = 0.5 * np.pi - theta_ed
404 1
	lat_ed_geod = np.arctan2(np.tan(lat_ed), (1. - ellip.epssq))
405 1
	logging.debug("lats ed: %s", np.degrees(lat_ed))
406 1
	logging.debug("lats ed geod: %s", np.degrees(lat_ed_geod))
407 1
	logging.debug("phis ed: %s", np.degrees(phi_ed))
408
	return (np.degrees(lat_ed_geod), np.degrees(phi_ed))
409