|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
|
|
'''This module provides a user-friendly pythonic wrapper for the low-level C interface functions.''' |
|
3
|
|
|
|
|
4
|
|
|
from __future__ import division, print_function, absolute_import, unicode_literals |
|
5
|
|
|
|
|
6
|
|
|
import datetime as dt |
|
7
|
|
|
import calendar |
|
8
|
|
|
import warnings |
|
9
|
|
|
|
|
10
|
|
|
import numpy as np |
|
11
|
|
|
|
|
12
|
|
|
from aacgm2._aacgmv2 import A2G, TRACE, BADIDEA, ALLOWTRACE, GEOCENTRIC, setDateTime, aacgmConvert |
|
13
|
|
|
from aacgm2 import IGRF_12_COEFFS |
|
14
|
|
|
|
|
15
|
|
|
aacgmConvert_vectorized = np.vectorize(aacgmConvert) |
|
16
|
|
|
|
|
17
|
|
|
|
|
18
|
|
|
def convert(lat, lon, alt, date=None, a2g=False, trace=False, allowtrace=False, badidea=False, geocentric=False): |
|
19
|
|
|
'''Converts to/from geomagnetic coordinates. |
|
20
|
|
|
|
|
21
|
|
|
This is a user-friendly pythonic wrapper for the low-level C interface |
|
22
|
|
|
functions available in :mod:`aacgm2._aacgmv2`. |
|
23
|
|
|
|
|
24
|
|
|
Parameters |
|
25
|
|
|
========== |
|
26
|
|
|
lat,lon,alt : array_like |
|
27
|
|
|
Input latitude(s), longitude(s) and altitude(s). They must be |
|
28
|
|
|
`broadcastable to the same shape <http://docs.scipy.org/doc/numpy/user/basics.broadcasting.html>`_. |
|
29
|
|
|
date : :class:`datetime.date`/:class:`datetime.datetime`, optional |
|
30
|
|
|
The date/time to use for the magnetic field model, default ``None`` (uses |
|
31
|
|
|
current time). Must be between 1900 and 2020. |
|
32
|
|
|
a2g : bool, optional |
|
33
|
|
|
Convert from AACGM-v2 to geographic coordinates, default ``False`` |
|
34
|
|
|
(converts geographic to AACGM-v2). |
|
35
|
|
|
trace : bool, optional |
|
36
|
|
|
Use field-line tracing, default ``False`` (uses coefficients). Tracing |
|
37
|
|
|
is more precise and needed at altitudes > 2000 km, but significantly |
|
38
|
|
|
slower. |
|
39
|
|
|
allowtrace : bool, optional |
|
40
|
|
|
Automatically use field-line tracing above 2000 km, default ``False`` |
|
41
|
|
|
(raises an exception for these altitudes unless ``trace=True`` or |
|
42
|
|
|
``badidea=True``). |
|
43
|
|
|
badidea : bool, optional |
|
44
|
|
|
Allow use of coefficients above 2000 km (bad idea!) |
|
45
|
|
|
geocentric : bool, optional |
|
46
|
|
|
Assume inputs are geocentric with Earth radius 6371.2 km. |
|
47
|
|
|
|
|
48
|
|
|
Returns |
|
49
|
|
|
======= |
|
50
|
|
|
|
|
51
|
|
|
lat_out : ``numpy.ndarray`` |
|
52
|
|
|
Converted latitude |
|
53
|
|
|
lon_out : ``numpy.ndarray`` |
|
54
|
|
|
Converted longitude |
|
55
|
|
|
alt_out : ``numpy.ndarray`` |
|
56
|
|
|
Converted altitude |
|
57
|
|
|
|
|
58
|
|
|
Raises |
|
59
|
|
|
====== |
|
60
|
|
|
|
|
61
|
|
|
ValueError |
|
62
|
|
|
if max(alt) > 2000 and neither of `trace`, `allowtrace`, or `badidea` is ``True`` |
|
63
|
|
|
ValueError |
|
64
|
|
|
if latitude is outside the range -90 to +90 degrees |
|
65
|
|
|
RuntimeError |
|
66
|
|
|
if there was a problem in the C extension |
|
67
|
|
|
|
|
68
|
|
|
Notes |
|
69
|
|
|
===== |
|
70
|
|
|
|
|
71
|
|
|
This function exclusively relies on the `AACGM-v2 C library |
|
72
|
|
|
<https://engineering.dartmouth.edu/superdarn/aacgm.html>`_. Specifically, |
|
73
|
|
|
it calls the functions :func:`_aacgmv2.setDateTime` and |
|
74
|
|
|
:func:`_aacgmv2.aacgmConvert`, which are simple interfaces to the |
|
75
|
|
|
C library functions :func:`AACGM_v2_SetDateTime` and |
|
76
|
|
|
:func:`AACGM_v2_Convert`. Details of the techniques used to derive the |
|
77
|
|
|
AACGM-v2 coefficients are described by Shepherd, 2014 [1]_. |
|
78
|
|
|
|
|
79
|
|
|
.. [1] Shepherd, S. G. (2014), Altitude-adjusted corrected geomagnetic |
|
80
|
|
|
coordinates: Definition and functional approximations, |
|
81
|
|
|
J. Geophys. Res. Space Physics, 119, 7501--7521, |
|
82
|
|
|
doi:`10.1002/2014JA020264 <http://dx.doi.org/10.1002/2014JA020264>`_. |
|
83
|
|
|
|
|
84
|
|
|
''' |
|
85
|
|
|
|
|
86
|
|
|
# check values |
|
87
|
|
|
if np.min(alt) < 0: |
|
88
|
|
|
warnings.warn('Coordinate transformations are not intended for altitudes < 0 km', UserWarning) |
|
89
|
|
|
|
|
90
|
|
|
if np.max(alt) > 2000 and not (trace or allowtrace or badidea): |
|
91
|
|
|
raise ValueError('Coefficients are not valid for altitudes above 2000 km. You must either use field-line ' |
|
92
|
|
|
'tracing (trace=True or allowtrace=True) or indicate you know this is a bad idea ' |
|
93
|
|
|
'(badidea=True)') |
|
94
|
|
|
|
|
95
|
|
|
# check if latitudes are > 90.1 (to allow some room for rounding errors, which will be clipped) |
|
96
|
|
|
if np.max(np.abs(lat)) > 90.1: |
|
97
|
|
|
raise ValueError('Latitude must be in the range -90 to +90 degrees') |
|
98
|
|
|
np.clip(lat, -90, 90) |
|
99
|
|
|
|
|
100
|
|
|
# constrain longitudes between -180 and 180 |
|
101
|
|
|
lon = ((np.asarray(lon) + 180) % 360) - 180 |
|
102
|
|
|
|
|
103
|
|
|
# set to current date if none is given |
|
104
|
|
|
if date is None: |
|
105
|
|
|
date = dt.datetime.now() |
|
106
|
|
|
|
|
107
|
|
|
# add time info if only date is given |
|
108
|
|
|
if isinstance(date, dt.date): |
|
109
|
|
|
date = dt.datetime.combine(date, dt.time(0)) |
|
110
|
|
|
|
|
111
|
|
|
# set current date and time |
|
112
|
|
|
setDateTime(date.year, date.month, date.day, date.hour, date.minute, date.second) |
|
113
|
|
|
|
|
114
|
|
|
# make flag |
|
115
|
|
|
flag = A2G*a2g + TRACE*trace + ALLOWTRACE*allowtrace + BADIDEA*badidea + GEOCENTRIC*geocentric |
|
116
|
|
|
|
|
117
|
|
|
# convert |
|
118
|
|
|
lat_out, lon_out, alt_out = aacgmConvert_vectorized(lat, lon, alt, flag) |
|
119
|
|
|
|
|
120
|
|
|
return lat_out, lon_out, alt_out |
|
121
|
|
|
|
|
122
|
|
|
|
|
123
|
|
|
def convert_mlt(arr, datetime, m2a=False): |
|
124
|
|
|
'''Converts between magnetic local time (MLT) and AACGM-v2 longitude. |
|
125
|
|
|
|
|
126
|
|
|
.. note:: This function is not related to the AACGM-v2 C library, but is provided as |
|
127
|
|
|
a convenience in the hopes that it might be useful for some purposes. |
|
128
|
|
|
|
|
129
|
|
|
Parameters |
|
130
|
|
|
========== |
|
131
|
|
|
arr : array_like or float |
|
132
|
|
|
Magnetic longitudes or MLTs to convert. |
|
133
|
|
|
datetime : :class:`datetime.datetime` |
|
134
|
|
|
Date and time for MLT conversion in Universal Time (UT). |
|
135
|
|
|
m2a : bool |
|
136
|
|
|
Convert MLT to AACGM-v2 longitude (default is ``False``, which implies |
|
137
|
|
|
conversion from AACGM-v2 longitude to MLT). |
|
138
|
|
|
|
|
139
|
|
|
Returns |
|
140
|
|
|
======= |
|
141
|
|
|
out : numpy.ndarray |
|
142
|
|
|
Converted coordinates/MLT |
|
143
|
|
|
|
|
144
|
|
|
Notes |
|
145
|
|
|
===== |
|
146
|
|
|
|
|
147
|
|
|
The MLT conversion is not part of the AACGM-v2 C library and is instead based |
|
148
|
|
|
on Laundal et al., 2016 [1]_. A brief summary of the method is provided below. |
|
149
|
|
|
|
|
150
|
|
|
MLT is defined as |
|
151
|
|
|
|
|
152
|
|
|
MLT = (magnetic longitude - magnetic noon meridian longitude) / 15 + 12 |
|
153
|
|
|
|
|
154
|
|
|
where the magnetic noon meridian longitude is the centered dipole longitude |
|
155
|
|
|
of the subsolar point. |
|
156
|
|
|
|
|
157
|
|
|
There are two important reasons for using centered dipole instead of AACGM for |
|
158
|
|
|
this calculation. One reason is that the AACGM longitude of the subsolar point |
|
159
|
|
|
is often undefined (being at low latitudes). More importantly, if the subsolar |
|
160
|
|
|
point close to ground was used, the MLT at polar latitudes would be affected |
|
161
|
|
|
by non-dipole features at low latitudes, such as the South Atlantic Anomaly. |
|
162
|
|
|
This is not desirable; since the Sun-Earth interaction takes place at polar |
|
163
|
|
|
field lines, it is these field lines the MLT should describe. |
|
164
|
|
|
|
|
165
|
|
|
In calculating the centered dipole longitude of the subsolar point, we use |
|
166
|
|
|
the first three IGRF Gauss coefficients, using linear interpolation between |
|
167
|
|
|
the model updates every five years. |
|
168
|
|
|
|
|
169
|
|
|
Both input and output MLON are taken modulo 360 to ensure they are between |
|
170
|
|
|
0 and 360 degrees. Similarly, input/output MLT are taken modulo 24. |
|
171
|
|
|
For implementation of the subsolar point calculation, see :func:`subsol`. |
|
172
|
|
|
|
|
173
|
|
|
.. [1] Laundal, K. M. and A. D. Richmond (2016), Magnetic Coordinate Systems, |
|
174
|
|
|
Space Sci. Rev., doi:`10.1007/s11214-016-0275-y <http://dx.doi.org/10.1007/s11214-016-0275-y>`_. |
|
175
|
|
|
|
|
176
|
|
|
''' |
|
177
|
|
|
d2r = np.pi/180 |
|
178
|
|
|
|
|
179
|
|
|
# find subsolar point |
|
180
|
|
|
yr = datetime.year |
|
181
|
|
|
doy = datetime.timetuple().tm_yday |
|
182
|
|
|
ssm = datetime.hour*3600 + datetime.minute*60 + datetime.second |
|
183
|
|
|
subsol_lon, subsol_lat = subsol(yr, doy, ssm) |
|
184
|
|
|
|
|
185
|
|
|
# unit vector pointing at subsolar point: |
|
186
|
|
|
s = np.array([np.cos(subsol_lat * d2r) * np.cos(subsol_lon * d2r), |
|
187
|
|
|
np.cos(subsol_lat * d2r) * np.sin(subsol_lon * d2r), |
|
188
|
|
|
np.sin(subsol_lat * d2r)]) |
|
189
|
|
|
|
|
190
|
|
|
# convert subsolar coordinates to centered dipole coordinates |
|
191
|
|
|
z = igrf_dipole_axis(datetime) # Cartesian axis pointing at Northern dipole pole |
|
192
|
|
|
y = np.cross(np.array([0, 0, 1]), z) |
|
193
|
|
|
y = y/np.linalg.norm(y) |
|
194
|
|
|
x = np.cross(y, z) |
|
195
|
|
|
R = np.vstack((x, y, z)) |
|
196
|
|
|
s_cd = R.dot(s) |
|
197
|
|
|
|
|
198
|
|
|
# centered dipole longitude of subsolar point: |
|
199
|
|
|
mlon_subsol = np.arctan2(s_cd[1], s_cd[0])/d2r |
|
200
|
|
|
|
|
201
|
|
|
# convert the input array |
|
202
|
|
|
if m2a: # MLT to AACGM |
|
203
|
|
|
mlt = np.asarray(arr) % 24 |
|
204
|
|
|
mlon = (15*(mlt - 12) + mlon_subsol) % 360 |
|
205
|
|
|
return mlon |
|
206
|
|
|
else: # AACGM to MLT |
|
207
|
|
|
mlon = np.asarray(arr) % 360 |
|
208
|
|
|
mlt = ((mlon - mlon_subsol)/15 + 12) % 24 |
|
209
|
|
|
return mlt |
|
210
|
|
|
|
|
211
|
|
|
|
|
212
|
|
|
def subsol(year, doy, ut): |
|
213
|
|
|
'''Finds subsolar geocentric longitude and latitude. |
|
214
|
|
|
|
|
215
|
|
|
Helper function for :func:`convert_mlt`. |
|
216
|
|
|
|
|
217
|
|
|
Parameters |
|
218
|
|
|
========== |
|
219
|
|
|
year : int [1601, 2100] |
|
220
|
|
|
Calendar year |
|
221
|
|
|
doy : int [1, 365/366] |
|
222
|
|
|
Day of year |
|
223
|
|
|
ut : float |
|
224
|
|
|
Seconds since midnight on the specified day |
|
225
|
|
|
|
|
226
|
|
|
Returns |
|
227
|
|
|
======= |
|
228
|
|
|
sbsllon : float |
|
229
|
|
|
Subsolar longitude for the given date/time |
|
230
|
|
|
sbsllat : float |
|
231
|
|
|
Subsolar latitude for the given date/time |
|
232
|
|
|
|
|
233
|
|
|
Notes |
|
234
|
|
|
===== |
|
235
|
|
|
|
|
236
|
|
|
Based on formulas in Astronomical Almanac for the year 1996, p. C24. |
|
237
|
|
|
(U.S. Government Printing Office, 1994). Usable for years 1601-2100, |
|
238
|
|
|
inclusive. According to the Almanac, results are good to at least 0.01 |
|
239
|
|
|
degree latitude and 0.025 degrees longitude between years 1950 and 2050. |
|
240
|
|
|
Accuracy for other years has not been tested. Every day is assumed to have |
|
241
|
|
|
exactly 86400 seconds; thus leap seconds that sometimes occur on December |
|
242
|
|
|
31 are ignored (their effect is below the accuracy threshold of the |
|
243
|
|
|
algorithm). |
|
244
|
|
|
|
|
245
|
|
|
After Fortran code by A. D. Richmond, NCAR. Translated from IDL |
|
246
|
|
|
by K. Laundal. |
|
247
|
|
|
|
|
248
|
|
|
''' |
|
249
|
|
|
|
|
250
|
|
|
from numpy import sin, cos, pi, arctan2, arcsin |
|
251
|
|
|
|
|
252
|
|
|
yr = year - 2000 |
|
253
|
|
|
|
|
254
|
|
|
if year >= 2101: |
|
255
|
|
|
print('subsol.py: subsol invalid after 2100. Input year is:', year) |
|
256
|
|
|
|
|
257
|
|
|
nleap = np.floor((year-1601)/4) |
|
258
|
|
|
nleap = nleap - 99 |
|
259
|
|
|
if year <= 1900: |
|
260
|
|
|
if year <= 1600: |
|
261
|
|
|
print('subsol.py: subsol invalid before 1601. Input year is:', year) |
|
262
|
|
|
ncent = np.floor((year-1601)/100) |
|
263
|
|
|
ncent = 3 - ncent |
|
264
|
|
|
nleap = nleap + ncent |
|
265
|
|
|
|
|
266
|
|
|
l0 = -79.549 + (-0.238699*(yr-4*nleap) + 3.08514e-2*nleap) |
|
267
|
|
|
|
|
268
|
|
|
g0 = -2.472 + (-0.2558905*(yr-4*nleap) - 3.79617e-2*nleap) |
|
269
|
|
|
|
|
270
|
|
|
# Days (including fraction) since 12 UT on January 1 of IYR: |
|
271
|
|
|
df = (ut/86400 - 1.5) + doy |
|
272
|
|
|
|
|
273
|
|
|
# Addition to Mean longitude of Sun since January 1 of IYR: |
|
274
|
|
|
lf = 0.9856474*df |
|
275
|
|
|
|
|
276
|
|
|
# Addition to Mean anomaly since January 1 of IYR: |
|
277
|
|
|
gf = 0.9856003*df |
|
278
|
|
|
|
|
279
|
|
|
# Mean longitude of Sun: |
|
280
|
|
|
l = l0 + lf |
|
281
|
|
|
|
|
282
|
|
|
# Mean anomaly: |
|
283
|
|
|
g = g0 + gf |
|
284
|
|
|
grad = g*pi/180 |
|
285
|
|
|
|
|
286
|
|
|
# Ecliptic longitude: |
|
287
|
|
|
lmbda = l + 1.915*sin(grad) + 0.020*sin(2*grad) |
|
288
|
|
|
lmrad = lmbda*pi/180 |
|
289
|
|
|
sinlm = sin(lmrad) |
|
290
|
|
|
|
|
291
|
|
|
# Days (including fraction) since 12 UT on January 1 of 2000: |
|
292
|
|
|
n = df + 365*yr + nleap |
|
293
|
|
|
|
|
294
|
|
|
# Obliquity of ecliptic: |
|
295
|
|
|
epsilon = 23.439 - 4e-7*n |
|
296
|
|
|
epsrad = epsilon*pi/180 |
|
297
|
|
|
|
|
298
|
|
|
# Right ascension: |
|
299
|
|
|
alpha = arctan2(cos(epsrad)*sinlm, cos(lmrad)) * 180/pi |
|
300
|
|
|
|
|
301
|
|
|
# Declination: |
|
302
|
|
|
delta = arcsin(sin(epsrad)*sinlm) * 180/pi |
|
303
|
|
|
|
|
304
|
|
|
# Subsolar latitude: |
|
305
|
|
|
sbsllat = delta |
|
306
|
|
|
|
|
307
|
|
|
# Equation of time (degrees): |
|
308
|
|
|
etdeg = l - alpha |
|
309
|
|
|
nrot = round(etdeg/360) |
|
310
|
|
|
etdeg = etdeg - 360*nrot |
|
311
|
|
|
|
|
312
|
|
|
# Apparent time (degrees): |
|
313
|
|
|
aptime = ut/240 + etdeg # Earth rotates one degree every 240 s. |
|
314
|
|
|
|
|
315
|
|
|
# Subsolar longitude: |
|
316
|
|
|
sbsllon = 180 - aptime |
|
317
|
|
|
nrot = round(sbsllon/360) |
|
318
|
|
|
sbsllon = sbsllon - 360*nrot |
|
319
|
|
|
|
|
320
|
|
|
return sbsllon, sbsllat |
|
321
|
|
|
|
|
322
|
|
|
|
|
323
|
|
|
def gc2gd_lat(gc_lat): |
|
324
|
|
|
'''Convert geocentric latitude to geodetic latitude using WGS84. |
|
325
|
|
|
|
|
326
|
|
|
Parameters |
|
327
|
|
|
========== |
|
328
|
|
|
gc_lat : array_like or float |
|
329
|
|
|
Geocentric latitude |
|
330
|
|
|
|
|
331
|
|
|
Returns |
|
332
|
|
|
======= |
|
333
|
|
|
gd_lat : same as input |
|
334
|
|
|
Geodetic latitude |
|
335
|
|
|
|
|
336
|
|
|
''' |
|
337
|
|
|
WGS84_e2 = 0.006694379990141317 |
|
338
|
|
|
return np.rad2deg(-np.arctan(np.tan(np.deg2rad(gc_lat))/(WGS84_e2 - 1))) |
|
339
|
|
|
|
|
340
|
|
|
|
|
341
|
|
|
def igrf_dipole_axis(date): |
|
342
|
|
|
'''Get Cartesian unit vector pointing at dipole pole in the north, according to IGRF |
|
343
|
|
|
|
|
344
|
|
|
Parameters |
|
345
|
|
|
========== |
|
346
|
|
|
date : :class:`datetime.datetime` |
|
347
|
|
|
Date and time |
|
348
|
|
|
|
|
349
|
|
|
Returns |
|
350
|
|
|
======= |
|
351
|
|
|
m: numpy.ndarray |
|
352
|
|
|
Cartesian 3 element vector pointing at dipole pole in the north (geocentric coords) |
|
353
|
|
|
|
|
354
|
|
|
Notes |
|
355
|
|
|
===== |
|
356
|
|
|
IGRF coefficients are read from the igrf12coeffs.txt file. It should also work after IGRF updates. |
|
357
|
|
|
The dipole coefficients are interpolated to the date, or extrapolated if date > latest IGRF model |
|
358
|
|
|
''' |
|
359
|
|
|
|
|
360
|
|
|
# get time in years, as float: |
|
361
|
|
|
year = date.year |
|
362
|
|
|
doy = date.timetuple().tm_yday |
|
363
|
|
|
year = year + doy/(365 + calendar.isleap(year)) |
|
364
|
|
|
|
|
365
|
|
|
# read the IGRF coefficients |
|
366
|
|
|
with open(IGRF_12_COEFFS, 'r') as f: |
|
367
|
|
|
lines = f.readlines() |
|
368
|
|
|
|
|
369
|
|
|
years = lines[3].split()[3:][:-1] |
|
370
|
|
|
years = np.array(years, dtype=float) # time array |
|
371
|
|
|
|
|
372
|
|
|
g10 = lines[4].split()[3:] |
|
373
|
|
|
g11 = lines[5].split()[3:] |
|
374
|
|
|
h11 = lines[6].split()[3:] |
|
375
|
|
|
|
|
376
|
|
|
# secular variation coefficients (for extrapolation) |
|
377
|
|
|
g10sv = np.float32(g10[-1]) |
|
378
|
|
|
g11sv = np.float32(g11[-1]) |
|
379
|
|
|
h11sv = np.float32(h11[-1]) |
|
380
|
|
|
|
|
381
|
|
|
# model coefficients: |
|
382
|
|
|
g10 = np.array(g10[:-1], dtype=float) |
|
383
|
|
|
g11 = np.array(g11[:-1], dtype=float) |
|
384
|
|
|
h11 = np.array(h11[:-1], dtype=float) |
|
385
|
|
|
|
|
386
|
|
|
# get the gauss coefficient at given time: |
|
387
|
|
|
if year <= years[-1]: # regular interpolation |
|
388
|
|
|
g10 = np.interp(year, years, g10) |
|
389
|
|
|
g11 = np.interp(year, years, g11) |
|
390
|
|
|
h11 = np.interp(year, years, h11) |
|
391
|
|
|
else: # extrapolation |
|
392
|
|
|
dt = year - years[-1] |
|
393
|
|
|
g10 = g10[-1] + g10sv * dt |
|
394
|
|
|
g11 = g11[-1] + g11sv * dt |
|
395
|
|
|
h11 = h11[-1] + h11sv * dt |
|
396
|
|
|
|
|
397
|
|
|
# calculate pole position |
|
398
|
|
|
B0 = np.sqrt(g10**2 + g11**2 + h11**2) |
|
399
|
|
|
|
|
400
|
|
|
return -np.array([g11, h11, g10])/B0 |
|
401
|
|
|
|