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
|
|
|
|