aacgm2.wrapper   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 118
dl 0
loc 401
rs 10
c 0
b 0
f 0
wmc 19

5 Functions

Rating   Name   Duplication   Size   Complexity  
A igrf_dipole_axis() 0 60 3
A convert_mlt() 0 87 2
B subsol() 0 109 4
C convert() 0 103 9
A gc2gd_lat() 0 16 1
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