Passed
Pull Request — master (#40)
by Angeline
01:18
created

aacgmv2.wrapper.convert_latlon()   C

Complexity

Conditions 9

Size

Total Lines 95
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 95
rs 6.6586
c 0
b 0
f 0
cc 9
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
# Copyright (C) 2019 NRL 
2
# Author: Angeline Burrell
3
# Disclaimer: This code is under the MIT license, whose details can be found at
4
# the root in the LICENSE file
5
#
6
# -*- coding: utf-8 -*-
7
"""Pythonic wrappers for AACGM-V2 C functions.
8
9
Functions
10
--------------
11
test_time : Test the time and ensure it is a datetime.datetime object
12
test_height : Test the height and see if it is appropriate for the method
13
set_coeff_path : Set the coefficient paths using default or supplied values
14
convert_latlon : Converts scalar location
15
convert_latlon_arr : Converts array location
16
get_aacgm_coord : Get scalar magnetic lat, lon, mlt from geographic location
17
get_aacgm_coord_arr : Get array magnetic lat, lon, mlt from geographic location
18
convert_str_to_bit : Convert human readible AACGM flag to bits
19
convert_bool_to_bit : Convert boolian flags to bits
20
convert_mlt : Get array mlt
21
--------------
22
23
"""
24
25
from __future__ import division, absolute_import, unicode_literals
26
import datetime as dt
27
import numpy as np
28
import os
29
import sys
30
31
import aacgmv2
32
import aacgmv2._aacgmv2 as c_aacgmv2
33
from aacgmv2._aacgmv2 import TRACE, ALLOWTRACE, BADIDEA
34
35
def test_time(dtime):
36
    """ Test the time input and ensure it is a dt.datetime object
37
38
    Parameters
39
    ----------
40
    dtime : (unknown)
41
        Time input in an untested format
42
43
    Returns
44
    -------
45
    dtime : (dt.datetime)
46
        Time as a datetime object
47
48
    Raises
49
    ------
50
    ValueError if time is not a dt.date or dt.datetime object
51
52
    """
53
    if isinstance(dtime, dt.date):
54
        # Because datetime objects identify as both dt.date and dt.datetime,
55
        # you need an extra test here to ensure you don't lose the time
56
        # attributes
57
        if not isinstance(dtime, dt.datetime):
58
            dtime = dt.datetime.combine(dtime, dt.time(0))
59
    elif not isinstance(dtime, dt.datetime):
60
        raise ValueError('time variable (dtime) must be a datetime object')
61
62
    return dtime
63
64
65
def test_height(height, bit_code):
66
    """ Test the input height and ensure it is appropriate for the method
67
68
    Parameters
69
    ----------
70
    height : (float)
71
        Height to test in km
72
    bit_code : (int)
73
        Code string denoting method to use
74
75
    Returns
76
    -------
77
    good_height : (boolean)
78
        True if height and method are appropriate, False if not
79
80
    Notes
81
    -----
82
    Appropriate altitude ranges for the different methods are explored in
83
    Shepherd (2014).  Summarized, they are:
84
    Coefficients: 0-2000 km
85
    Tracing: 0-1 Earth Radius
86
87
    Altitudes below zero will work, but will not provide a good representation
88
    of the magnetic field because it goes beyond the intended scope of these
89
    coordiantes.
90
91
    If you use the 'BADIDEA' code, you can bypass all constraints, but it
92
    is a Bad Idea!  If you include a high enough altiutde, the code may hang.
93
94
    """
95
    # Test for heights that are allowed but not within the intended scope
96
    # of the coordinate system.  The routine will work, but the user should
97
    # be aware that the results are not as reliable
98
    if height < 0:
99
        aacgmv2.logger.warning('conversion not intended for altitudes < 0 km')
100
101
    # Test the conditions for using the coefficient method
102
    if(height > aacgmv2.high_alt_coeff and
103
       not (bit_code & (TRACE|ALLOWTRACE|BADIDEA))):
104
        estr = ''.join(['coefficients are not valid for altitudes above ',
105
                        '{:.0f} km. You '.format(aacgmv2.high_alt_coeff),
106
                        'must either use field-line tracing (trace=True or',
107
                        ' allowtrace=True) or indicate you know this is a',
108
                        ' bad idea'])
109
        aacgmv2.logger.error(estr)
110
        return False
111
112
    # Test the conditions for using the tracing method
113
    if height > aacgmv2.high_alt_trace and not (bit_code & BADIDEA):
114
        estr = ''.join(['these coordinates are not intended for the ',
115
                        'magnetosphere! You must indicate that you know ',
116
                        'this is a bad idea.  If you continue, it is ',
117
                        'possible that the code will hang.'])
118
        aacgmv2.logger.error(estr)
119
        return False
120
121
    return True
122
123
def set_coeff_path(igrf_file=False, coeff_prefix=False):
124
    """Sets the IGRF_COEFF and AACGMV_V2_DAT_PREFIX environment variables.
125
126
    Parameters
127
    -----------
128
    igrf_file : (str or bool)
129
        Full filename of IGRF coefficient file, True to use
130
        aacgmv2.IGRF_COEFFS, or False to leave as is. (default=False)
131
    coeff_prefix : (str or bool)
132
        Location and file prefix for aacgm coefficient files, True to use
133
        aacgmv2.AACGM_V2_DAT_PREFIX, or False to leave as is. (default=False)
134
135
    """
136
137
    # Define coefficient file prefix if requested
138
    if coeff_prefix is not False:
139
        # Use the default value, if one was not supplied (allow None to
140
        # comply with depricated behaviour)
141
        if coeff_prefix is True or coeff_prefix is None:
142
            coeff_prefix = aacgmv2.AACGM_v2_DAT_PREFIX
143
144
        if hasattr(os, "unsetenv"):
145
            os.unsetenv('AACGM_v2_DAT_PREFIX')
146
        else:
147
            del os.environ['AACGM_v2_DAT_PREFIX']
148
        os.environ['AACGM_v2_DAT_PREFIX'] = coeff_prefix
149
150
    # Define IGRF file if requested
151
    if igrf_file is not False:
152
        # Use the default value, if one was not supplied (allow None to
153
        # comply with depricated behaviour)
154
        if igrf_file is True or igrf_file is None:
155
            igrf_file = aacgmv2.IGRF_COEFFS
156
157
        if hasattr(os, "unsetenv"):
158
            os.unsetenv('IGRF_COEFFS')
159
        else:
160
            del os.environ['IGRF_COEFFS']
161
        os.environ['IGRF_COEFFS'] = igrf_file
162
163
    return
164
165
def convert_latlon(in_lat, in_lon, height, dtime, method_code="G2A"):
166
    """Converts between geomagnetic coordinates and AACGM coordinates
167
168
    Parameters
169
    ------------
170
    in_lat : (float)
171
        Input latitude in degrees N (code specifies type of latitude)
172
    in_lon : (float)
173
        Input longitude in degrees E (code specifies type of longitude)
174
    height : (float)
175
        Altitude above the surface of the earth in km
176
    dtime : (datetime)
177
        Datetime for magnetic field
178
    method_code : (str or int)
179
        Bit code or string denoting which type(s) of conversion to perform
180
        G2A        - geographic (geodetic) to AACGM-v2
181
        A2G        - AACGM-v2 to geographic (geodetic)
182
        TRACE      - use field-line tracing, not coefficients
183
        ALLOWTRACE - use trace only above 2000 km
184
        BADIDEA    - use coefficients above 2000 km
185
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
186
        (default is "G2A")
187
188
    Returns
189
    -------
190
    out_lat : (float)
191
        Output latitude in degrees N
192
    out_lon : (float)
193
        Output longitude in degrees E
194
    out_r : (float)
195
        Geocentric radial distance (R_Earth) or altitude above the surface of
196
        the Earth (km)
197
198
    Raises
199
    ------
200
    ValueError if input is incorrect
201
    TypeError or RuntimeError if unable to set AACGMV2 datetime
202
203
    """
204
205
    # Test time
206
    dtime = test_time(dtime)
207
208
    # Initialise output
209
    lat_out = np.nan
210
    lon_out = np.nan
211
    r_out = np.nan
212
213
    # Set the coordinate coversion method code in bits
214
    try:
215
        bit_code = convert_str_to_bit(method_code.upper())
216
    except AttributeError:
217
        bit_code = method_code
218
219
    if not isinstance(bit_code, int):
220
        raise ValueError("unknown method code {:}".format(method_code))
221
222
    # Test height that may or may not cause failure
223
    if not test_height(height, bit_code):
224
        return lat_out, lon_out, r_out
225
226
    # Test latitude range
227
    if abs(in_lat) > 90.0:
228
        # Allow latitudes with a small deviation from the maximum
229
        # (+/- 90 degrees) to be set to 90
230
        if abs(in_lat) > 90.1:
231
            raise ValueError('unrealistic latitude')
232
        in_lat = np.sign(in_lat) * 90.0
233
234
    # Constrain longitudes between -180 and 180
235
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
236
237
    # Set current date and time
238
    try:
239
        c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
240
                               dtime.minute, dtime.second)
241
    except TypeError as terr:
242
        raise TypeError("unable to set time for {:}: {:}".format(dtime, terr))
243
    except RuntimeError as rerr:
244
        raise RuntimeError("unable to set time for {:}: {:}".format(dtime,
245
                                                                    rerr))
246
247
    # convert location
248
    try:
249
        lat_out, lon_out, r_out = c_aacgmv2.convert(in_lat, in_lon, height,
250
                                                    bit_code)
251
    except:
252
        err = sys.exc_info()[0]
253
        estr = "unable to perform conversion at {:.1f},".format(in_lat)
254
        estr = "{:s}{:.1f} {:.1f} km, {:} ".format(estr, in_lon, height, dtime)
255
        estr = "{:s}using method {:}: {:}".format(estr, bit_code, err)
256
        aacgmv2.logger.warning(estr)
257
        pass
258
259
    return lat_out, lon_out, r_out
260
261
def convert_latlon_arr(in_lat, in_lon, height, dtime, method_code="G2A"):
262
    """Converts between geomagnetic coordinates and AACGM coordinates.
263
264
    Parameters
265
    ------------
266
    in_lat : (np.ndarray or list or float)
267
        Input latitude in degrees N (method_code specifies type of latitude)
268
    in_lon : (np.ndarray or list or float)
269
        Input longitude in degrees E (method_code specifies type of longitude)
270
    height : (np.ndarray or list or float)
271
        Altitude above the surface of the earth in km
272
    dtime : (datetime)
273
        Single datetime object for magnetic field
274
    method_code : (int or str)
275
        Bit code or string denoting which type(s) of conversion to perform
276
        G2A        - geographic (geodetic) to AACGM-v2
277
        A2G        - AACGM-v2 to geographic (geodetic)
278
        TRACE      - use field-line tracing, not coefficients
279
        ALLOWTRACE - use trace only above 2000 km
280
        BADIDEA    - use coefficients above 2000 km
281
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
282
        (default = "G2A")
283
284
    Returns
285
    -------
286
    out_lat : (np.ndarray)
287
        Output latitudes in degrees N
288
    out_lon : (np.ndarray)
289
        Output longitudes in degrees E
290
    out_r : (np.ndarray)
291
        Geocentric radial distance (R_Earth) or altitude above the surface of
292
        the Earth (km)
293
294
    Raises
295
    ------
296
    ValueError if input is incorrect
297
    TypeError or RuntimeError if unable to set AACGMV2 datetime
298
299
    Notes
300
    -------
301
    At least one of in_lat, in_lon, and height must be a list or array.
302
303
    """
304
305
    # Recast the data as numpy arrays
306
    in_lat = np.array(in_lat)
307
    in_lon = np.array(in_lon)
308
    height = np.array(height)
309
310
    # If one or two of these elements is a float or int, create an array
311
    test_array = np.array([len(in_lat.shape), len(in_lon.shape),
312
                           len(height.shape)])
313
    if test_array.min() == 0:
314
        if test_array.max() == 0:
315
            aacgmv2.logger.warning("for a single location, consider using " \
316
                                   "convert_latlon or get_aacgm_coord")
317
            in_lat = np.array([in_lat])
318
            in_lon = np.array([in_lon])
319
            height = np.array([height])
320
        else:
321
            imax = test_array.argmax()
322
            max_shape = in_lat.shape if imax == 0 else (in_lon.shape \
323
                                            if imax == 1 else height.shape) 
324
            if not test_array[0]:
325
                in_lat = np.full(shape=max_shape, fill_value=in_lat)
326
            if not test_array[1]:
327
                in_lon = np.full(shape=max_shape, fill_value=in_lon)
328
            if not test_array[2]:
329
                height = np.full(shape=max_shape, fill_value=height)
330
331
    # Ensure that lat, lon, and height are the same length or if the lengths
332
    # differ that the different ones contain only a single value
333
    if not (in_lat.shape == in_lon.shape and in_lat.shape == height.shape):
334
        shape_dict = {'lat': in_lat.shape, 'lon': in_lon.shape,
335
                      'height': height.shape}
336
        ulen = np.unique(shape_dict.values())
337
        array_key = [kk for i, kk in enumerate(shape_dict.keys())
338
                     if shape_dict[kk] != (1,)]
339
        if len(array_key) == 3:
340
            raise ValueError('lat, lon, and height arrays are mismatched')
341
        elif len(array_key) == 2:
342
            if shape_dict[array_key[0]] == shape_dict[array_dict[1]]:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable array_dict does not seem to be defined.
Loading history...
343
                raise ValueError('{:s} and {:s} arrays are mismatched'.format(\
344
                                                                *array_key))
345
346
    # Test time
347
    dtime = test_time(dtime)
348
349
    # Initialise output
350
    lat_out = np.full(shape=in_lat.shape, fill_value=np.nan)
351
    lon_out = np.full(shape=in_lon.shape, fill_value=np.nan)
352
    r_out = np.full(shape=height.shape, fill_value=np.nan)
353
354
    # Test and set the conversion method code
355
    try:
356
        bit_code = convert_str_to_bit(method_code.upper())
357
    except AttributeError:
358
        bit_code = method_code
359
360
    if not isinstance(bit_code, int):
361
        raise ValueError("unknown method code {:}".format(method_code))
362
363
    # Test height
364
    if not test_height(np.nanmax(height), bit_code):
365
        return lat_out, lon_out, r_out
366
367
    # Test latitude range
368
    if np.abs(in_lat).max() > 90.0:
369
        if np.abs(in_lat).max() > 90.1:
370
            raise ValueError('unrealistic latitude')
371
        in_lat = np.clip(in_lat, -90.0, 90.0)
372
373
    # Constrain longitudes between -180 and 180
374
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
375
376
    
377
    # Set current date and time
378
    try:
379
        c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
380
                               dtime.minute, dtime.second)
381
    except TypeError as terr:
382
        raise TypeError("unable to set time for {:}: {:}".format(dtime, terr))
383
    except RuntimeError as rerr:
384
        raise RuntimeError("unable to set time for {:}: {:}".format(dtime,
385
                                                                    rerr))
386
387
    # Vectorise the AACGM C routine
388
    convert_vectorised = np.vectorize(c_aacgmv2.convert)
389
390
    # convert
391
    try:
392
        lat_out, lon_out, r_out = convert_vectorised(in_lat, in_lon, height,
393
                                                     bit_code)
394
395
    except:
396
        err = sys.exc_info()[0]
397
        estr = "unable to perform vector conversion at {:} using ".format(dtime)
398
        estr = "{:s}method {:}: {:}".format(estr, bit_code, err)
399
        aacgmv2.logger.warning(estr)
400
        pass
401
402
    return lat_out, lon_out, r_out
403
404
def get_aacgm_coord(glat, glon, height, dtime, method="TRACE"):
405
    """Get AACGM latitude, longitude, and magnetic local time
406
407
    Parameters
408
    ------------
409
    glat : (float)
410
        Geodetic latitude in degrees N
411
    glon : (float)
412
        Geodetic longitude in degrees E
413
    height : (float)
414
        Altitude above the surface of the earth in km
415
    dtime : (datetime)
416
        Date and time to calculate magnetic location
417
    method : (str)
418
        String denoting which type(s) of conversion to perform
419
        TRACE      - use field-line tracing, not coefficients
420
        ALLOWTRACE - use trace only above 2000 km
421
        BADIDEA    - use coefficients above 2000 km
422
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
423
        (default = "TRACE")
424
425
    Returns
426
    -------
427
    mlat : (float)
428
        magnetic latitude in degrees N
429
    mlon : (float)
430
        magnetic longitude in degrees E
431
    mlt : (float)
432
        magnetic local time in hours
433
434
    """
435
    # Initialize method code
436
    method_code = "G2A|{:s}".format(method)
437
438
    # Get magnetic lat and lon.
439
    mlat, mlon, _ = convert_latlon(glat, glon, height, dtime,
440
                                   method_code=method_code)
441
442
    # Get magnetic local time
443
    mlt = np.nan if np.isnan(mlon) else convert_mlt(mlon, dtime, m2a=False)
444
445
    return mlat, mlon, mlt
446
447
448
def get_aacgm_coord_arr(glat, glon, height, dtime, method="TRACE"):
449
    """Get AACGM latitude, longitude, and magnetic local time
450
451
    Parameters
452
    ------------
453
    glat : (np.array or list)
454
        Geodetic latitude in degrees N
455
    glon : (np.array or list)
456
        Geodetic longitude in degrees E
457
    height : (np.array or list)
458
        Altitude above the surface of the earth in km
459
    dtime : (datetime)
460
        Date and time to calculate magnetic location
461
    method : (str)
462
        String denoting which type(s) of conversion to perform
463
        TRACE      - use field-line tracing, not coefficients
464
        ALLOWTRACE - use trace only above 2000 km
465
        BADIDEA    - use coefficients above 2000 km
466
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
467
        (default = "TRACE")
468
        (default = "TRACE")
469
470
    Returns
471
    -------
472
    mlat : (float)
473
        magnetic latitude in degrees N
474
    mlon : (float)
475
        magnetic longitude in degrees E
476
    mlt : (float)
477
        magnetic local time in hours
478
479
    """
480
    # Initialize method code
481
    method_code = "G2A|{:s}".format(method)
482
483
    # Get magnetic lat and lon.
484
    mlat, mlon, _ = convert_latlon_arr(glat, glon, height, dtime,
485
                                       method_code=method_code)
486
487
    if np.all(np.isnan(mlon)):
488
        mlt = np.full(shape=mlat.shape, fill_value=np.nan)
489
    else:
490
        # Get magnetic local time
491
        mlt = convert_mlt(mlon, dtime, m2a=False)
492
493
    return mlat, mlon, mlt
494
495
def convert_str_to_bit(method_code):
496
    """convert string code specification to bit code specification
497
498
    Parameters
499
    ------------
500
    method_code : (str)
501
        Bitwise code for passing options into converter (default=0)
502
        G2A        - geographic (geodetic) to AACGM-v2
503
        A2G        - AACGM-v2 to geographic (geodetic)
504
        TRACE      - use field-line tracing, not coefficients
505
        ALLOWTRACE - use trace only above 2000 km
506
        BADIDEA    - use coefficients above 2000 km
507
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
508
509
    Returns
510
    --------
511
    bit_code : (int)
512
        Method code specification in bits
513
514
    Notes
515
    --------
516
    Multiple codes should be seperated by pipes '|'.  Invalid parts of the code
517
    are ignored and no code defaults to 'G2A'.
518
519
    """
520
521
    convert_code = {"G2A": c_aacgmv2.G2A, "A2G": c_aacgmv2.A2G,
522
                    "TRACE": c_aacgmv2.TRACE, "BADIDEA": c_aacgmv2.BADIDEA,
523
                    "GEOCENTRIC": c_aacgmv2.GEOCENTRIC,
524
                    "ALLOWTRACE": c_aacgmv2.ALLOWTRACE}
525
526
    # Force upper case, remove any spaces, and split along pipes
527
    method_codes = method_code.upper().replace(" ", "").split("|")
528
529
    # Add the valid parts of the code, invalid elements are ignored
530
    bit_code = sum([convert_code[k] for k in method_codes
531
                    if k in convert_code.keys()])
532
533
    return bit_code
534
535
def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False,
536
                        badidea=False, geocentric=False):
537
    """convert boolian flags to bit code specification
538
539
    Parameters
540
    ----------
541
    a2g : (bool)
542
        True for AACGM-v2 to geographic (geodetic), False otherwise
543
        (default=False)
544
    trace : (bool)
545
        If True, use field-line tracing, not coefficients (default=False)
546
    allowtrace : (bool)
547
        If True, use trace only above 2000 km (default=False)
548
    badidea : (bool)
549
        If True, use coefficients above 2000 km (default=False)
550
    geocentric : (bool)
551
        True for geodetic, False for geocentric w/RE=6371.2 (default=False)
552
553
    Returns
554
    --------
555
    bit_code : (int)
556
        code specification in bits
557
558
    """
559
560
    bit_code = c_aacgmv2.A2G if a2g else c_aacgmv2.G2A
561
562
    if trace:
563
        bit_code += c_aacgmv2.TRACE
564
    if allowtrace:
565
        bit_code += c_aacgmv2.ALLOWTRACE
566
    if badidea:
567
        bit_code += c_aacgmv2.BADIDEA
568
    if geocentric:
569
        bit_code += c_aacgmv2.GEOCENTRIC
570
571
    return bit_code
572
573
def convert_mlt(arr, dtime, m2a=False):
574
    """Converts between magnetic local time (MLT) and AACGM-v2 longitude
575
576
    Parameters
577
    ------------
578
    arr : (array_line or float)
579
        Magnetic longitudes (degrees E) or MLTs (hours) to convert
580
    dtime : (datetime.datetime)
581
        Date and time for MLT conversion in Universal Time (UT).
582
    m2a : (bool)
583
        Convert MLT to AACGM-v2 longitude (True) or magnetic longitude to MLT
584
        (False).  (default=False)
585
586
    Returns
587
    --------
588
    out : (np.ndarray)
589
        Converted coordinates/MLT in degrees E or hours (as appropriate)
590
591
    Notes
592
    -------
593
    This routine previously based on Laundal et al. 2016, but now uses the
594
    improved calculation available in AACGM-V2.4.
595
596
    """
597
598
    # Test time
599
    dtime = test_time(dtime)
600
    
601
    # Calculate desired location, C routines set date and time
602
    if m2a:
603
        # Get the magnetic longitude
604
        inv_vectorised = np.vectorize(c_aacgmv2.inv_mlt_convert)
605
        out = inv_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
606
                             dtime.minute, dtime.second, arr)
607
    else:
608
        # Get magnetic local time
609
        mlt_vectorised = np.vectorize(c_aacgmv2.mlt_convert)
610
        out = mlt_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
611
                             dtime.minute, dtime.second, arr)
612
613
    if hasattr(out, "shape") and out.shape == ():
614
        out = float(out)
615
616
    return out
617