Passed
Pull Request — develop (#36)
by Angeline
01:13
created

aacgmv2.wrapper.convert_latlon()   F

Complexity

Conditions 16

Size

Total Lines 107
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 51
dl 0
loc 107
rs 2.4
c 0
b 0
f 0
cc 16
nop 5

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like aacgmv2.wrapper.convert_latlon() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
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
18
from __future__ import division, absolute_import, unicode_literals
19
import datetime as dt
20
import numpy as np
21
22
def set_coeff_path(igrf_file=False, coeff_prefix=False):
23
    """Sets the IGRF_COEFF and AACGMV_V2_DAT_PREFIX environment variables.
24
25
    Parameters
26
    -----------
27
    igrf_file : (str or bool)
28
        Full filename of IGRF coefficient file, True to use
29
        aacgmv2.IGRF_COEFFS, or False to leave as is. (default=False)
30
    coeff_prefix : (str or bool)
31
        Location and file prefix for aacgm coefficient files, True to use
32
        aacgmv2.AACGM_V2_DAT_PREFIX, or False to leave as is. (default=False)
33
34
    Returns
35
    ---------
36
    Void
37
    """
38
    import aacgmv2
39
    import os
40
41
    # Define coefficient file prefix if requested
42
    if coeff_prefix is not False:
43
        # Use the default value, if one was not supplied (allow None to
44
        # comply with depricated behaviour)
45
        if coeff_prefix is True or coeff_prefix is None:
46
            coeff_prefix = aacgmv2.AACGM_v2_DAT_PREFIX
47
48
        if hasattr(os, "unsetenv"):
49
            os.unsetenv('AACGM_v2_DAT_PREFIX')
50
        else:
51
            del os.environ['AACGM_v2_DAT_PREFIX']
52
        os.environ['AACGM_v2_DAT_PREFIX'] = coeff_prefix
53
54
    # Define IGRF file if requested
55
    if igrf_file is not False:
56
        # Use the default value, if one was not supplied (allow None to
57
        # comply with depricated behaviour)
58
        if igrf_file is True or igrf_file is None:
59
            igrf_file = aacgmv2.IGRF_COEFFS
60
61
        if hasattr(os, "unsetenv"):
62
            os.unsetenv('IGRF_COEFFS')
63
        else:
64
            del os.environ['IGRF_COEFFS']
65
        os.environ['IGRF_COEFFS'] = igrf_file
66
67
    return
68
69
def convert_latlon(in_lat, in_lon, height, dtime, code="G2A"):
70
    """Converts between geomagnetic coordinates and AACGM coordinates
71
72
    Parameters
73
    ------------
74
    in_lat : (float)
75
        Input latitude in degrees N (code specifies type of latitude)
76
    in_lon : (float)
77
        Input longitude in degrees E (code specifies type of longitude)
78
    height : (float)
79
        Altitude above the surface of the earth in km
80
    dtime : (datetime)
81
        Datetime for magnetic field
82
    code : (str or int)
83
        Bit code or string denoting which type(s) of conversion to perform
84
        G2A        - geographic (geodetic) to AACGM-v2
85
        A2G        - AACGM-v2 to geographic (geodetic)
86
        TRACE      - use field-line tracing, not coefficients
87
        ALLOWTRACE - use trace only above 2000 km
88
        BADIDEA    - use coefficients above 2000 km
89
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
90
        (default is "G2A")
91
92
    Returns
93
    -------
94
    out_lat : (float)
95
        Output latitude in degrees N
96
    out_lon : (float)
97
        Output longitude in degrees E
98
    out_r : (float)
99
        Geocentric radial distance (R_Earth) or altitude above the surface of
100
        the Earth (km)
101
    """
102
    import aacgmv2._aacgmv2 as c_aacgmv2
103
    import aacgmv2
104
105
    # Test time
106
    if isinstance(dtime, dt.date):
107
        dtime = dt.datetime.combine(dtime, dt.time(0))
108
109
    if not isinstance(dtime, dt.datetime):
110
        raise ValueError('time must be specified as datetime object')
111
112
    # Test height
113
    if height < 0:
114
        aacgmv2.logger.warning('conversion not intended for altitudes < 0 km')
115
116
    # Initialise output
117
    lat_out = np.nan
118
    lon_out = np.nan
119
    r_out = np.nan
120
121
    # Test code
122
    try:
123
        code = code.upper()
124
125
        if(height > aacgmv2.high_alt_coeff and code.find("TRACE") < 0 and
126
           code.find("ALLOWTRACE") < 0 and code.find("BADIDEA") < 0):
127
            estr = ''.join(['coefficients are not valid for altitudes above ',
128
                            '{:.0f} km. You '.format(aacgmv2.high_alt_coeff),
129
                            'must either use field-line tracing (trace=True or',
130
                            ' allowtrace=True) or indicate you know this is a',
131
                            ' bad idea'])
132
            aacgmv2.logger.error(estr)
133
            return lat_out, lon_out, r_out
134
135
        if height > aacgmv2.high_alt_trace and code.find("BADIDEA") < 0:
136
            estr = ''.join(['these coordinates are not intended for the ',
137
                            'magnetosphere! You must indicate that you know ',
138
                            'this is a bad idea.  If you continue, it is ',
139
                            'possible that the code will hang.'])
140
141
            aacgmv2.logger.error(estr)
142
            return lat_out, lon_out, r_out
143
144
        # make flag
145
        bit_code = convert_str_to_bit(code)
146
    except AttributeError:
147
        bit_code = code
148
149
    if not isinstance(bit_code, int):
150
        raise ValueError("unknown code {:}".format(bit_code))
151
152
    # Test latitude range
153
    if abs(in_lat) > 90.0:
154
        if abs(in_lat) > 90.1:
155
            raise ValueError('unrealistic latitude')
156
        in_lat = np.sign(in_lat) * 90.0
157
158
    # Constrain longitudes between -180 and 180
159
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
160
161
    # Set current date and time
162
    try:
163
        c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
164
                               dtime.minute, dtime.second)
165
    except:
166
        raise ValueError("unable to set time for {:}".format(dtime))
167
168
    # convert location
169
    try:
170
        lat_out, lon_out, r_out = c_aacgmv2.convert(in_lat, in_lon, height,
171
                                                    bit_code)
172
    except:
173
        pass
174
175
    return lat_out, lon_out, r_out
176
177
def convert_latlon_arr(in_lat, in_lon, height, dtime, code="G2A"):
178
    """Converts between geomagnetic coordinates and AACGM coordinates.
179
180
    Parameters
181
    ------------
182
    in_lat : (np.ndarray or list or float)
183
        Input latitude in degrees N (code specifies type of latitude)
184
    in_lon : (np.ndarray or list or float)
185
        Input longitude in degrees E (code specifies type of longitude)
186
    height : (np.ndarray or list or float)
187
        Altitude above the surface of the earth in km
188
    dtime : (datetime)
189
        Single datetime object for magnetic field
190
    code : (int or str)
191
        Bit code or string denoting which type(s) of conversion to perform
192
        G2A        - geographic (geodetic) to AACGM-v2
193
        A2G        - AACGM-v2 to geographic (geodetic)
194
        TRACE      - use field-line tracing, not coefficients
195
        ALLOWTRACE - use trace only above 2000 km
196
        BADIDEA    - use coefficients above 2000 km
197
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
198
        (default = "G2A")
199
200
    Returns
201
    -------
202
    out_lat : (np.ndarray)
203
        Output latitudes in degrees N
204
    out_lon : (np.ndarray)
205
        Output longitudes in degrees E
206
    out_r : (np.ndarray)
207
        Geocentric radial distance (R_Earth) or altitude above the surface of
208
        the Earth (km)
209
210
    Notes
211
    -------
212
    At least one of in_lat, in_lon, and height must be a list or array.
213
    """
214
    import aacgmv2._aacgmv2 as c_aacgmv2
215
    import aacgmv2
216
217
    # Recast the data as numpy arrays
218
    in_lat = np.array(in_lat)
219
    in_lon = np.array(in_lon)
220
    height = np.array(height)
221
222
    # If one or two of these elements is a float or int, create an array
223
    test_array = np.array([len(in_lat.shape), len(in_lon.shape),
224
                           len(height.shape)])
225
    if test_array.min() == 0:
226
        if test_array.max() == 0:
227
            aacgmv2.logger.warning("for a single location, consider using " \
228
                                   "convert_latlon or get_aacgm_coord")
229
            in_lat = np.array([in_lat])
230
            in_lon = np.array([in_lon])
231
            height = np.array([height])
232
        else:
233
            imax = test_array.argmax()
234
            max_shape = in_lat.shape if imax == 0 else (in_lon.shape \
235
                                            if imax == 1 else height.shape) 
236
            if test_array[0] == 0:
237
                in_lat = np.full(shape=max_shape, fill_value=in_lat)
238
            if not test_array[1]:
239
                in_lon = np.full(shape=max_shape, fill_value=in_lon)
240
            if not test_array[2]:
241
                height = np.full(shape=max_shape, fill_value=height)
242
243
    # Ensure that lat, lon, and height are the same length or if the lengths
244
    # differ that the different ones contain only a single value
245
    if not (in_lat.shape == in_lon.shape and in_lat.shape == height.shape):
246
        ulen = np.unique([in_lat.shape, in_lon.shape, height.shape])
247
        if ulen.min() != (1,):
248
            raise ValueError('mismatched input arrays')
249
250
    # Test time
251
    if isinstance(dtime, dt.date):
252
        dtime = dt.datetime.combine(dtime, dt.time(0))
253
254
    if not isinstance(dtime, dt.datetime):
255
        raise ValueError('time must be specified as datetime object')
256
257
    # Test height
258
    if np.min(height) < 0:
259
        aacgmv2.logger.warning('conversion not intended for altitudes < 0 km')
260
261
    # Initialise output
262
    lat_out = np.full(shape=in_lat.shape, fill_value=np.nan)
263
    lon_out = np.full(shape=in_lon.shape, fill_value=np.nan)
264
    r_out = np.full(shape=height.shape, fill_value=np.nan)
265
266
    # Test code
267
    try:
268
        code = code.upper()
269
270
        if(np.nanmax(height) > aacgmv2.high_alt_coeff and code.find("TRACE") < 0
271
           and code.find("ALLOWTRACE") < 0 and code.find("BADIDEA") < 0):
272
            estr = ''.join(['coefficients are not valid for altitudes above ',
273
                            '{:.0f} km. You '.format(aacgmv2.high_alt_coeff),
274
                            'must either use field-line tracing (trace=True or',
275
                            ' allowtrace=True) or indicate you know this is a ',
276
                            'bad idea'])
277
            aacgmv2.logger.error(estr)
278
            return lat_out, lon_out, r_out
279
        
280
        if(np.nanmax(height) > aacgmv2.high_alt_trace and
281
           code.find("BADIDEA") < 0):
282
            estr = ''.join(['these coordinates are not intended for the ',
283
                            'magnetosphere! You must indicate that you know ',
284
                            'this is a bad idea.  If you continue, it is ',
285
                            'possible that the code will hang.'])
286
            aacgmv2.logger.error(estr)
287
            return lat_out, lon_out, r_out
288
289
        # make flag
290
        bit_code = convert_str_to_bit(code)
291
    except AttributeError:
292
        bit_code = code
293
294
    if not isinstance(bit_code, int):
295
        raise ValueError("unknown code {:}".format(bit_code))
296
297
    # Test latitude range
298
    if np.abs(in_lat).max() > 90.0:
299
        if np.abs(in_lat).max() > 90.1:
300
            raise ValueError('unrealistic latitude')
301
        in_lat = np.clip(in_lat, -90.0, 90.0)
302
303
    # Constrain longitudes between -180 and 180
304
    in_lon = ((in_lon + 180.0) % 360.0) - 180.0
305
306
    
307
    # Set current date and time
308
    try:
309
        c_aacgmv2.set_datetime(dtime.year, dtime.month, dtime.day, dtime.hour,
310
                               dtime.minute, dtime.second)
311
    except:
312
        raise RuntimeError("unable to set time for {:}".format(dtime))
313
314
    # Vectorise the AACGM code
315
    convert_vectorised = np.vectorize(c_aacgmv2.convert)
316
317
    # convert
318
    try:
319
        lat_out, lon_out, r_out = convert_vectorised(in_lat, in_lon, height,
320
                                                     bit_code)
321
322
    except:
323
        pass
324
325
    return lat_out, lon_out, r_out
326
327
def get_aacgm_coord(glat, glon, height, dtime, method="TRACE"):
328
    """Get AACGM latitude, longitude, and magnetic local time
329
330
    Parameters
331
    ------------
332
    glat : (float)
333
        Geodetic latitude in degrees N
334
    glon : (float)
335
        Geodetic longitude in degrees E
336
    height : (float)
337
        Altitude above the surface of the earth in km
338
    dtime : (datetime)
339
        Date and time to calculate magnetic location
340
    method : (str)
341
        String denoting which type(s) of conversion to perform
342
        TRACE      - use field-line tracing, not coefficients
343
        ALLOWTRACE - use trace only above 2000 km
344
        BADIDEA    - use coefficients above 2000 km
345
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
346
        (default = "TRACE")
347
348
    Returns
349
    -------
350
    mlat : (float)
351
        magnetic latitude in degrees N
352
    mlon : (float)
353
        magnetic longitude in degrees E
354
    mlt : (float)
355
        magnetic local time in hours
356
    """
357
    # Initialize code
358
    code = "G2A|{:s}".format(method)
359
360
    # Get magnetic lat and lon.
361
    mlat, mlon, _ = convert_latlon(glat, glon, height, dtime, code=code)
362
363
    # Get magnetic local time
364
    if np.isnan(mlon):
365
        mlt = np.nan
366
    else:
367
        mlt = convert_mlt(mlon, dtime, m2a=False)
368
369
    return mlat, mlon, mlt
370
371
372
def get_aacgm_coord_arr(glat, glon, height, dtime, method="TRACE"):
373
    """Get AACGM latitude, longitude, and magnetic local time
374
375
    Parameters
376
    ------------
377
    glat : (np.array or list)
378
        Geodetic latitude in degrees N
379
    glon : (np.array or list)
380
        Geodetic longitude in degrees E
381
    height : (np.array or list)
382
        Altitude above the surface of the earth in km
383
    dtime : (datetime)
384
        Date and time to calculate magnetic location
385
    method : (str)
386
        String denoting which type(s) of conversion to perform
387
        TRACE      - use field-line tracing, not coefficients
388
        ALLOWTRACE - use trace only above 2000 km
389
        BADIDEA    - use coefficients above 2000 km
390
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
391
        (default = "TRACE")
392
        (default = "TRACE")
393
394
    Returns
395
    -------
396
    mlat : (float)
397
        magnetic latitude in degrees N
398
    mlon : (float)
399
        magnetic longitude in degrees E
400
    mlt : (float)
401
        magnetic local time in hours
402
    """
403
    # Initialize code
404
    code = "G2A|{:s}".format(method)
405
406
    # Get magnetic lat and lon.
407
    mlat, mlon, _ = convert_latlon_arr(glat, glon, height, dtime, code=code)
408
409
    if np.all(np.isnan(mlon)):
410
        mlt = np.full(shape=mlat.shape, fill_value=np.nan)
411
    else:
412
        # Get magnetic local time
413
        mlt = convert_mlt(mlon, dtime, m2a=False)
414
415
    return mlat, mlon, mlt
416
417
def convert_str_to_bit(code):
418
    """convert string code specification to bit code specification
419
420
    Parameters
421
    ------------
422
    code : (str)
423
        Bitwise code for passing options into converter (default=0)
424
        G2A        - geographic (geodetic) to AACGM-v2
425
        A2G        - AACGM-v2 to geographic (geodetic)
426
        TRACE      - use field-line tracing, not coefficients
427
        ALLOWTRACE - use trace only above 2000 km
428
        BADIDEA    - use coefficients above 2000 km
429
        GEOCENTRIC - assume inputs are geocentric w/ RE=6371.2
430
431
    Returns
432
    --------
433
    bit_code : (int)
434
        code specification in bits
435
436
    Notes
437
    --------
438
    Multiple codes should be seperated by pipes '|'.  Invalid parts of the code
439
    are ignored and no code defaults to 'G2A'.
440
    """
441
    import aacgmv2._aacgmv2 as c_aacgmv2
442
443
    convert_code = {"G2A": c_aacgmv2.G2A, "A2G": c_aacgmv2.A2G,
444
                    "TRACE": c_aacgmv2.TRACE, "BADIDEA": c_aacgmv2.BADIDEA,
445
                    "GEOCENTRIC": c_aacgmv2.GEOCENTRIC,
446
                    "ALLOWTRACE": c_aacgmv2.ALLOWTRACE}
447
448
    # Force upper case, remove any spaces, and split along pipes
449
    codes = code.upper().replace(" ", "").split("|")
450
451
    # Add the valid parts of the code, invalid elements are ignored
452
    bit_code = sum([convert_code[k] for k in codes if k in convert_code.keys()])
453
454
    return bit_code
455
456
def convert_bool_to_bit(a2g=False, trace=False, allowtrace=False,
457
                        badidea=False, geocentric=False):
458
    """convert boolian flags to bit code specification
459
460
    Parameters
461
    ----------
462
    a2g : (bool)
463
        True for AACGM-v2 to geographic (geodetic), False otherwise
464
        (default=False)
465
    trace : (bool)
466
        If True, use field-line tracing, not coefficients (default=False)
467
    allowtrace : (bool)
468
        If True, use trace only above 2000 km (default=False)
469
    badidea : (bool)
470
        If True, use coefficients above 2000 km (default=False)
471
    geocentric : (bool)
472
        True for geodetic, False for geocentric w/RE=6371.2 (default=False)
473
474
    Returns
475
    --------
476
    bit_code : (int)
477
        code specification in bits
478
    """
479
    import aacgmv2._aacgmv2 as c_aacgmv2
480
481
    bit_code = c_aacgmv2.A2G if a2g else c_aacgmv2.G2A
482
483
    if trace:
484
        bit_code += c_aacgmv2.TRACE
485
    if allowtrace:
486
        bit_code += c_aacgmv2.ALLOWTRACE
487
    if badidea:
488
        bit_code += c_aacgmv2.BADIDEA
489
    if geocentric:
490
        bit_code += c_aacgmv2.GEOCENTRIC
491
492
    return bit_code
493
494
def convert_mlt(arr, dtime, m2a=False):
495
    """Converts between magnetic local time (MLT) and AACGM-v2 longitude
496
497
    Parameters
498
    ------------
499
    arr : (array_line or float)
500
        Magnetic longitudes (degrees E) or MLTs (hours) to convert
501
    dtime : (datetime.datetime)
502
        Date and time for MLT conversion in Universal Time (UT).
503
    m2a : (bool)
504
        Convert MLT to AACGM-v2 longitude (True) or magnetic longitude to MLT
505
        (False).  (default=False)
506
507
    Returns
508
    --------
509
    out : (np.ndarray)
510
        Converted coordinates/MLT in degrees E or hours (as appropriate)
511
512
    Notes
513
    -------
514
    This routine previously based on Laundal et al. 2016, but now uses the
515
    improved calculation available in AACGM-V2.4.
516
    """
517
    import aacgmv2._aacgmv2 as c_aacgmv2
518
519
    # Test time
520
    if isinstance(dtime, dt.date):
521
        if not isinstance(dtime, dt.datetime):
522
            dtime = dt.datetime.combine(dtime, dt.time(0))
523
    else:
524
        raise ValueError('time must be specified as datetime object')
525
526
    # Calculate desired location, C routines set date and time
527
    if m2a:
528
        # Get the magnetic longitude
529
        inv_vectorised = np.vectorize(c_aacgmv2.inv_mlt_convert)
530
        out = inv_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
531
                             dtime.minute, dtime.second, arr)
532
    else:
533
        # Get magnetic local time
534
        mlt_vectorised = np.vectorize(c_aacgmv2.mlt_convert)
535
        out = mlt_vectorised(dtime.year, dtime.month, dtime.day, dtime.hour,
536
                             dtime.minute, dtime.second, arr)
537
538
    if hasattr(out, "shape") and out.shape == ():
539
        out = float(out)
540
541
    return out
542