Completed
Pull Request — develop (#10)
by Angeline
03:45
created

aacgmv2.wrapper.convert_mlt()   B

Complexity

Conditions 6

Size

Total Lines 59
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 59
rs 7.795
c 0
b 0
f 0
cc 6
nop 5

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# -*- coding: utf-8 -*-
0 ignored issues
show
Bug introduced by
There seems to be a cyclic import (aacgmv2 -> aacgmv2.wrapper).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
Bug introduced by
There seems to be a cyclic import (aacgmv2 -> aacgmv2.deprecated).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

Loading history...
2
"""Pythonic wrappers for AACGM-V2 C functions.
3
4
Functions
5
--------------
6
set_coeff_path : Set the coefficient paths using default or supplied values
7
convert_latlon : Converts scalar location
8
convert_latlon_arr : Converts array location
9
get_aacgm_coord : Get scalar magnetic lat, lon, mlt from geographic location
10
get_aacgm_coord_arr : Get array magnetic lat, lon, mlt from geographic location
11
convert_str_to_bit : Convert human readible AACGM flag to bits
12
convert_bool_to_bit : Convert boolian flags to bits
13
convert_mlt : Get array mlt
14
--------------
15
"""
16
17
from __future__ import division, absolute_import, unicode_literals
18
import datetime as dt
19
import numpy as np
0 ignored issues
show
introduced by
Unable to import 'numpy'
Loading history...
20
import logbook as logging
0 ignored issues
show
introduced by
Unable to import 'logbook'
Loading history...
21
import aacgmv2
22
import aacgmv2._aacgmv2 as c_aacgmv2
0 ignored issues
show
introduced by
Unable to import 'aacgmv2._aacgmv2'
Loading history...
23
24
def set_coeff_path(igrf_file=None, coeff_prefix=None):
25
    """This routine sets the two path variables.
26
27
    Parameters
28
    -----------
29
    igrf_file : (str or NoneType)
30
        Full filename of IGRF coefficient file or None to use
31
        aacgmv2.IGRF_12_COEFFS. (default=None)
32
    coeff_prefix : (str or NoneType)
33
        Location and file prefix for aacgm coefficient files or None to use
34
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
35
36
    Returns
37
    ---------
38
    igrf_file : (str)
39
        Full filename of IGRF coefficient file 
0 ignored issues
show
Coding Style introduced by
Trailing whitespace
Loading history...
40
    coeff_prefix : (str)
41
        Location and file prefix for aacgm coefficient files
42
    """
43
    # Define coefficient file prefix if not supplied
44
    if coeff_prefix is None:
45
        coeff_prefix = aacgmv2.AACGM_V2_DAT_PREFIX
46
47
    # Define IGRF file if not supplied
48
    if igrf_file is None:
49
        igrf_file = aacgmv2.IGRF_12_COEFFS
50
51
    return igrf_file, coeff_prefix
52
53
def convert_latlon(in_lat, in_lon, height, dtime, code="G2A", igrf_file=None,
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
54
                   coeff_prefix=None):
55
    """Converts between geomagnetic coordinates and AACGM coordinates
56
57
    Parameters
58
    ------------
59
    in_lat : (float)
60
        Input latitude in degrees N (code specifies type of latitude)
61
    in_lon : (float)
62
        Input longitude in degrees E (code specifies type of longitude)
63
    height : (float)
64
        Altitude above the surface of the earth in km
65
    dtime : (datetime)
66
        Datetime for magnetic field
67
    code : (str or int)
68
        Bit code or string denoting which type(s) of conversion to perform
69
        G2A        - geographic (geodetic) to AACGM-v2
70
        A2G        - AACGM-v2 to geographic (geodetic)
71
        TRACE      - use field-line tracing, not coefficients
72
        ALLOWTRACE - use trace only above 2000 km
73
        BADIDEA    - use coefficients above 2000 km
74
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
75
        (default is "G2A")
76
    igrf_file : (str or NoneType)
77
        Full filename of IGRF coefficient file or None to use
78
        aacgmv2.IGRF_12_COEFFS. (default=None)
79
    coeff_prefix : (str or NoneType)
80
        Location and file prefix for aacgm coefficient files or None to use
81
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
82
83
    Returns
84
    -------
85
    out_lat : (float)
86
        Output latitude in degrees
87
    out_lon : (float)
88
        Output longitude in degrees
89
    out_r : (float)
90
        Geocentric radial distance in R
91
    """
92
    # Define coefficient file prefix if not supplied
93
    igrf_file, coeff_prefix = set_coeff_path(igrf_file=igrf_file,
94
                                             coeff_prefix=coeff_prefix)
95
96
    # Test time
97
    if isinstance(dtime, dt.date):
98
        dtime = dt.datetime.combine(dtime, dt.time(0))
99
100
    assert isinstance(dtime, dt.datetime), \
101
        logging.error('time must be specified as datetime object')
102
103
    # Test height
104
    if height < 0:
105
        logging.warn('conversion not intended for altitudes < 0 km')
106
107
    # Initialise output
108
    lat_out = np.nan
109
    lon_out = np.nan
110
    r_out = np.nan
111
112
    # Test code
113
    try:
114
        code = code.upper()
115
116
        if(height > 2000 and code.find("TRACE") < 0 and
117
           code.find("ALLOWTRACE") < 0 and code.find("BADIDEA")):
118
            estr = 'coefficients are not valid for altitudes above 2000 km. You'
119
            estr += ' must either use field-line tracing (trace=True '
120
            estr += 'or allowtrace=True) or indicate you know this '
121
            estr += 'is a bad idea'
122
            logging.error(estr)
123
            return lat_out, lon_out, r_out
124
125
        # make flag
126
        bit_code = convert_str_to_bit(code)
127
    except AttributeError:
128
        bit_code = code
129
130
    assert isinstance(bit_code, int), \
131
        logging.error("unknown code {:}".format(bit_code))
132
133
    # Test latitude range
134
    if abs(in_lat) > 90.0:
135
        assert abs(in_lat) <= 90.1, logging.error('unrealistic latitude')
136
        in_lat = np.sign(in_lat) * 90.0
137
138
    # Constrain longitudes between -180 and 180
139
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
140
141
    # Set current date and time
142
    c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
143
                           dtime.minute, dtime.second, coeff_prefix)
144
145
    # convert location
146
    try:
147
        lat_out, lon_out, r_out = c_aacgmv2.convert(in_lat, in_lon, height,
148
                                                    bit_code, igrf_file)
149
    except:
0 ignored issues
show
Coding Style Best Practice introduced by
General except handlers without types should be used sparingly.

Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.

Loading history...
150
        pass
151
152
    return lat_out, lon_out, r_out
153
154
def convert_latlon_arr(in_lat, in_lon, height, dtime, code="G2A",
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (16/15).
Loading history...
155
                       igrf_file=None, coeff_prefix=None):
156
    """Converts between geomagnetic coordinates and AACGM coordinates.  At least
157
    one of in_lat, in_lon, and height must be a list or array
158
159
    Parameters
160
    ------------
161
    in_lat : (np.ndarray or list or float)
162
        Input latitude in degrees N (code specifies type of latitude)
163
    in_lon : (np.ndarray or list or float)
164
        Input longitude in degrees E (code specifies type of longitude)
165
    height : (np.ndarray or list or float)
166
        Altitude above the surface of the earth in km
167
    dtime : (datetime)
168
        Single datetime object for magnetic field
169
    code : (int or str)
170
        Bit code or string denoting which type(s) of conversion to perform
171
        G2A        - geographic (geodetic) to AACGM-v2
172
        A2G        - AACGM-v2 to geographic (geodetic)
173
        TRACE      - use field-line tracing, not coefficients
174
        ALLOWTRACE - use trace only above 2000 km
175
        BADIDEA    - use coefficients above 2000 km
176
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
177
        (default = "G2A")
178
    igrf_file : (str or NoneType)
179
        Full filename of IGRF coefficient file or None to use
180
        aacgmv2.IGRF_12_COEFFS. (default=None)
181
    coeff_prefix : (str or NoneType)
182
        Location and file prefix for aacgm coefficient files or None to use
183
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
184
185
    Returns
186
    -------
187
    out_lat : (np.ndarray)
188
        Output latitudes in degrees
189
    out_lon : (np.ndarray)
190
        Output longitudes in degrees
191
    out_r : (np.ndarray)
192
        Geocentric radial distances in R
193
    """
194
    # If a list was entered instead of a numpy array, recast it here
195
    if isinstance(in_lat, list):
196
        in_lat = np.array(in_lat)
197
198
    if isinstance(in_lon, list):
199
        in_lon = np.array(in_lon)
200
201
    if isinstance(height, list):
202
        height = np.array(height)
203
204
    # If one or two of these elements is a float or int, create an array
205
    test_array = np.array([hasattr(in_lat, "shape"), hasattr(in_lon, "shape"),
206
                           hasattr(height, "shape")])
207
    if not test_array.all():
208
        if test_array.any():
209
            arr_shape = in_lat.shape if test_array.argmax() == 0 else \
210
                        (in_lon.shape if test_array.argmax() == 1 else
211
                         height.shape)
212
            if not test_array[0]:
213
                in_lat = np.ones(shape=arr_shape, dtype=float) * in_lat
214
            if not test_array[1]:
215
                in_lon = np.ones(shape=arr_shape, dtype=float) * in_lon
216
            if not test_array[2]:
217
                height = np.ones(shape=arr_shape, dtype=float) * height
218
        else:
219
            logging.info("for a single location, consider using convert_latlon")
220
            in_lat = np.array([in_lat])
221
            in_lon = np.array([in_lon])
222
            height = np.array([height])
223
224
    # Ensure that lat, lon, and height are the same length or if the lengths
225
    # differ that the different ones contain only a single value
226
    if not (in_lat.shape == in_lon.shape and in_lat.shape == height.shape):
227
        ulen = np.unique([in_lat.shape, in_lon.shape, height.shape])
228
        if ulen.min() != (1,):
229
            logging.error("mismatched input arrays")
230
            return None, None, None
231
232
    # Define coefficient file prefix if not supplied
233
    igrf_file, coeff_prefix = set_coeff_path(igrf_file=igrf_file,
234
                                             coeff_prefix=coeff_prefix)
235
236
    # Test time
237
    if isinstance(dtime, dt.date):
238
        dtime = dt.datetime.combine(dtime, dt.time(0))
239
240
    assert isinstance(dtime, dt.datetime), \
241
        logging.error('time must be specified as datetime object')
242
243
    # Test height
244
    if np.min(height) < 0:
245
        logging.warn('conversion not intended for altitudes < 0 km')
246
247
    # Initialise output
248
    lat_out = np.empty(shape=in_lat.shape, dtype=float) * np.nan
249
    lon_out = np.empty(shape=in_lon.shape, dtype=float) * np.nan
250
    r_out = np.empty(shape=height.shape, dtype=float) * np.nan
251
252
    # Test code
253
    try:
254
        code = code.upper()
255
256
        if(np.nanmax(height) > 2000 and code.find("TRACE") < 0 and
257
           code.find("ALLOWTRACE") < 0 and code.find("BADIDEA")):
258
            estr = 'coefficients are not valid for altitudes above 2000 km. You'
259
            estr += ' must either use field-line tracing (trace=True '
260
            estr += 'or allowtrace=True) or indicate you know this '
261
            estr += 'is a bad idea'
262
            logging.error(estr)
263
            return lat_out, lon_out, r_out
264
265
        # make flag
266
        bit_code = convert_str_to_bit(code)
267
    except AttributeError:
268
        bit_code = code
269
270
    assert isinstance(bit_code, int), \
271
        logging.error("unknown code {:}".format(bit_code))
272
273
    # Test latitude range
274
    if np.abs(in_lat).max() > 90.0:
275
        assert np.abs(in_lat).max() <= 90.1, \
276
            logging.error('unrealistic latitude')
277
        in_lat = np.clip(in_lat, -90.0, 90.0)
278
279
    # Constrain longitudes between -180 and 180
280
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
281
282
    # Set current date and time
283
    c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
284
                           dtime.minute, dtime.second, coeff_prefix)
285
286
    # Vectorise the AACGM code
287
    convert_vectorised = np.vectorize(c_aacgmv2.convert)
288
289
    # convert
290
    try:
291
        lat_out, lon_out, r_out = convert_vectorised(in_lat, in_lon, height,
292
                                                     bit_code, igrf_file)
293
    except:
0 ignored issues
show
Coding Style Best Practice introduced by
General except handlers without types should be used sparingly.

Typically, you would use general except handlers when you intend to specifically handle all types of errors, f.e. when logging. Otherwise, such general error handlers can mask errors in your application that you want to know of.

Loading history...
294
        pass
295
296
    return lat_out, lon_out, r_out
297
298
def get_aacgm_coord(glat, glon, height, dtime, method="TRACE",
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
299
                    igrf_file=None, coeff_prefix=None):
300
    """Get AACGM latitude, longitude, and magnetic local time
301
302
    Parameters
303
    ------------
304
    glat : (float)
305
        Geodetic latitude in degrees N
306
    glon : (float)
307
        Geodetic longitude in degrees E
308
    height : (float)
309
        Altitude above the surface of the earth in km
310
    dtime : (datetime)
311
        Date and time to calculate magnetic location
312
    method : (str)
313
        String denoting which type(s) of conversion to perform
314
        TRACE      - use field-line tracing, not coefficients
315
        ALLOWTRACE - use trace only above 2000 km
316
        BADIDEA    - use coefficients above 2000 km
317
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
318
        (default = "TRACE")
319
    igrf_file : (str or NoneType)
320
        Full filename of IGRF coefficient file or None to use
321
        aacgmv2.IGRF_12_COEFFS. (default=None)
322
    coeff_prefix : (str or NoneType)
323
        Location and file prefix for aacgm coefficient files or None to use
324
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
325
326
    Returns
327
    -------
328
    mlat : (float)
329
        magnetic latitude in degrees
330
    mlon : (float)
331
        magnetic longitude in degrees
332
    mlt : (float)
333
        magnetic local time in hours
334
    """
335
    # Define coefficient file prefix if not supplied
336
    igrf_file, coeff_prefix = set_coeff_path(igrf_file=igrf_file,
337
                                             coeff_prefix=coeff_prefix)
338
339
    # Initialize code
340
    code = "G2A|{:s}".format(method)
341
342
    # Get magnetic lat and lon.
343
    mlat, mlon, _ = convert_latlon(glat, glon, height, dtime, code=code,
344
                                   igrf_file=igrf_file,
345
                                   coeff_prefix=coeff_prefix)
346
    # Get magnetic local time
347
    if np.isnan(mlon):
348
        mlt = np.nan
349
    else:
350
        mlt = convert_mlt(mlon, dtime, m2a=False, coeff_prefix=coeff_prefix,
351
                          igrf_file=igrf_file)
352
353
    return mlat, mlon, mlt
354
355
356
def get_aacgm_coord_arr(glat, glon, height, dtime, method="TRACE",
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
357
                        igrf_file=None, coeff_prefix=None):
358
    """Get AACGM latitude, longitude, and magnetic local time
359
360
    Parameters
361
    ------------
362
    glat : (np.array or list)
363
        Geodetic latitude in degrees N
364
    glon : (np.array or list)
365
        Geodetic longitude in degrees E
366
    height : (np.array or list)
367
        Altitude above the surface of the earth in km
368
    dtime : (datetime)
369
        Date and time to calculate magnetic location
370
    method : (str)
371
        String denoting which type(s) of conversion to perform
372
        TRACE      - use field-line tracing, not coefficients
373
        ALLOWTRACE - use trace only above 2000 km
374
        BADIDEA    - use coefficients above 2000 km
375
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
376
        (default = "TRACE")
377
    igrf_file : (str or NoneType)
378
        Full filename of IGRF coefficient file or None to use
379
        aacgmv2.IGRF_12_COEFFS. (default=None)
380
    coeff_prefix : (str or NoneType)
381
        Location and file prefix for aacgm coefficient files or None to use
382
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
383
384
    Returns
385
    -------
386
    mlat : (float)
387
        magnetic latitude in degrees
388
    mlon : (float)
389
        magnetic longitude in degrees
390
    mlt : (float)
391
        magnetic local time in hours
392
    """
393
    # Define coefficient file prefix if not supplied
394
    igrf_file, coeff_prefix = set_coeff_path(igrf_file=igrf_file,
395
                                             coeff_prefix=coeff_prefix)
396
397
    # Initialize code
398
    code = "G2A|{:s}".format(method)
399
400
    # Get magnetic lat and lon.
401
    mlat, mlon, _ = convert_latlon_arr(glat, glon, height, dtime, code=code,
402
                                       igrf_file=igrf_file,
403
                                       coeff_prefix=coeff_prefix)
404
405
    if np.all(np.isnan(mlon)):
406
        mlt = np.empty(shape=mlat.shape, dtype=float) * np.nan
407
    else:
408
        # Get magnetic local time
409
        mlt = convert_mlt(mlon, dtime, m2a=False, coeff_prefix=coeff_prefix,
410
                          igrf_file=igrf_file)
411
412
    return mlat, mlon, mlt
413
414
def convert_str_to_bit(code):
415
    """convert string code specification to bit code specification
416
417
    Parameters
418
    ------------
419
    code : (str)
420
        Bitwise code for passing options into converter (default=0)
421
        G2A        - geographic (geodetic) to AACGM-v2
422
        A2G        - AACGM-v2 to geographic (geodetic)
423
        TRACE      - use field-line tracing, not coefficients
424
        ALLOWTRACE - use trace only above 2000 km
425
        BADIDEA    - use coefficients above 2000 km
426
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
427
428
    Returns
429
    --------
430
    bit_code : (int)
431
        code specification in bits
432
433
    Notes
434
    --------
435
    Multiple codes should be seperated by pipes '|'.  Invalid parts of the code
436
    are ignored and no code defaults to 'G2A'.
437
    """
438
    convert_code = {"G2A": c_aacgmv2.G2A, "A2G": c_aacgmv2.A2G,
439
                    "TRACE": c_aacgmv2.TRACE, "BADIDEA": c_aacgmv2.BADIDEA,
440
                    "GEOCENTRIC": c_aacgmv2.GEOCENTRIC,
441
                    "ALLOWTRACE": c_aacgmv2.ALLOWTRACE}
442
443
    # Force upper case, remove any spaces, and split along pipes
444
    codes = code.upper().replace(" ", "").split("|")
445
446
    # Add the valid parts of the code, invalid elements are ignored
447
    bit_code = sum([convert_code[k] for k in codes if k in convert_code.keys()])
448
449
    return bit_code
450
451
def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False,
452
                        badidea=False, geocentric=False):
453
    """convert boolian flags to bit code specification
454
455
    Parameters
456
    ----------
457
    a2g : (bool)
458
        True for AACGM-v2 to geographic (geodetic), False otherwise
459
        (default=False)
460
    trace : (bool)
461
        If True, use field-line tracing, not coefficients (default=False)
462
    allowtrace : (bool)
463
        If True, use trace only above 2000 km (default=False)
464
    badidea : (bool)
465
        If True, use coefficients above 2000 km (default=False)
466
    geocentric : (bool)
467
        True for geodetic, False for geocentric w/RE=6371.2 (default=False)
468
469
    Returns
470
    --------
471
    bit_code : (int)
472
        code specification in bits
473
    """
474
    bit_code = c_aacgmv2.A2G if a2g else c_aacgmv2.G2A
475
476
    if trace:
477
        bit_code += c_aacgmv2.TRACE
478
    if allowtrace:
479
        bit_code += c_aacgmv2.ALLOWTRACE
480
    if badidea:
481
        bit_code += c_aacgmv2.BADIDEA
482
    if geocentric:
483
        bit_code += c_aacgmv2.GEOCENTRIC
484
485
    return bit_code
486
487
def convert_mlt(arr, dtime, m2a=False, coeff_prefix=None, igrf_file=None):
488
    """Converts between magnetic local time (MLT) and AACGM-v2 longitude
489
490
    Parameters
491
    ------------
492
    arr : (array_line or float)
493
        Magnetic longitudes or MLTs to convert
494
    dtime : (datetime.datetime)
495
        Date and time for MLT conversion in Universal Time (UT).
496
    m2a : (bool)
497
        Convert MLT to AACGM-v2 longitude (True) or magnetic longitude to MLT
498
        (False).  (default=False)
499
    coeff_prefix : (str or NoneType)
500
        Location and file prefix for aacgm coefficient files or None to use
501
        aacgmv2.AACGM_V2_DAT_PREFIX. (default=None)
502
    igrf_file : (str or NoneType)
503
        Full filename of IGRF coefficient file or None to use
504
        aacgmv2.IGRF_12_COEFFS. (default=None)
505
506
    Returns
507
    --------
508
    out : (np.ndarray)
509
        Converted coordinates/MLT
510
511
    Notes
512
    -------
513
    This routine previously based on Laundal et al. 2016, but now uses the
514
    improved calculation available in AACGM-V2.4.
515
    """
516
    # Define coefficient file prefix if not supplied
517
    igrf_file, coeff_prefix = set_coeff_path(igrf_file=igrf_file,
518
                                             coeff_prefix=coeff_prefix)
519
520
521
    # Test time
522
    if isinstance(dtime, dt.date):
523
        dtime = dt.datetime.combine(dtime, dt.time(0))
524
525
    assert isinstance(dtime, dt.datetime), \
526
        logging.error('time must be specified as datetime object')
527
528
    # Calculate desired location, C routines set date and time
529
    if m2a:
530
        # Get the magnetic longitude
531
        inv_vectorised = np.vectorize(c_aacgmv2.inv_mlt_convert)
532
        out = inv_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
533
                             dtime.minute, dtime.second, arr, coeff_prefix,
534
                             igrf_file)
535
    else:
536
        # Get magnetic local time
537
        mlt_vectorised = np.vectorize(c_aacgmv2.mlt_convert)
538
        out = mlt_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
539
                             dtime.minute, dtime.second, arr, coeff_prefix,
540
                             igrf_file)
541
542
    if hasattr(out, "shape") and out.shape == ():
543
        out = float(out)
544
545
    return out
546