Completed
Pull Request — develop (#22)
by Angeline
05:29
created

aacgmv2.wrapper   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 464
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 164
dl 0
loc 464
rs 4.5599
c 0
b 0
f 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
A get_aacgm_coord() 0 43 2
D convert_latlon() 0 92 13
B convert_mlt() 0 48 6
A convert_str_to_bit() 0 38 3
A get_aacgm_coord_arr() 0 43 2
F convert_latlon_arr() 0 135 26
B convert_bool_to_bit() 0 37 6

How to fix   Complexity   

Complexity

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