Passed
Pull Request — develop (#79)
by Angeline
01:20
created

TestConvertLatLon.test_convert_latlon_high_lat()   A

Complexity

Conditions 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
rs 10
c 0
b 0
f 0
cc 1
nop 3
1
import datetime as dt
2
import logging
3
import numpy as np
4
import os
5
import pytest
6
import warnings
7
8
import aacgmv2
9
10
11
class TestConvertArray(object):
12
    """Unit tests for array conversion."""
13
    def setup_method(self):
14
        """Create a clean test environment."""
15
        self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0)
16
        self.ddate = dt.date(2015, 1, 1)
17
        self.lat_in = [60.0, 61.0]
18
        self.lon_in = [0.0, 0.0]
19
        self.alt_in = [300.0, 300.0]
20
        self.method = 'TRACE'
21
        self.out = None
22
        self.ref = [[58.22676, 59.31847], [81.16135, 81.60797],
23
                    [0.18880, 0.21857]]
24
        self.rtol = 1.0e-4
25
26
    def teardown_method(self):
27
        """Clean up the test envrionment."""
28
        del self.out, self.ref, self.lat_in, self.dtime, self.ddate
29
        del self.lon_in, self.alt_in, self.method, self.rtol
30
31
    def evaluate_output(self, ind=None):
32
        """Evaluate convert_latlon_arr output.
33
34
        Parameters
35
        ----------
36
        ind : NoneType or int
37
           Index or NoneType if no index
38
39
        """
40
        if self.out is not None:
41
            if ind is not None:
42
                self.ref = [[rr[ind]] for rr in self.ref]
43
44
            np.testing.assert_equal(len(self.out), len(self.ref))
45
            for i, oo in enumerate(self.out):
46
                if not isinstance(oo, np.ndarray):
47
                    raise TypeError("output value is not a numpy array")
48
49
                np.testing.assert_equal(len(oo), len(self.ref[i]))
50
                np.testing.assert_allclose(oo, self.ref[i], rtol=self.rtol)
51
52
53
class TestConvertLatLon(object):
54
    """Unit tests for single value conversion."""
55
56
    def setup_method(self):
57
        """Create a clean test environment."""
58
        self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0)
59
        self.ddate = dt.date(2015, 1, 1)
60
        self.in_args = [60, 0]
61
        self.out = None
62
        self.rtol = 1.0e-4
63
64
    def teardown_method(self):
65
        """Clean up the test envrionment."""
66
        del self.out, self.in_args, self.rtol, self.dtime, self.ddate
67
68
    @pytest.mark.parametrize('alt,method_code,ref',
69
                             [(300, 'TRACE', [58.2268, 81.1613, 1.0457]),
70
                              (3000.0, "G2A|BADIDEA", [64.3578, 83.2895,
71
                                                       1.4694]),
72
                              (7000.0, "G2A|TRACE|BADIDEA",
73
                               [69.3187, 85.0845, 2.0973])])
74
    def test_convert_latlon(self, alt, method_code, ref):
75
        """Test single value latlon conversion.
76
77
        Parameters
78
        ----------
79
        alt : int or float
80
            Altitude in km
81
        method_code : str
82
            Tracing/calculation method code
83
        ref : list
84
            Expected output values
85
86
        """
87
        self.in_args.extend([alt, self.dtime, method_code])
88
        self.out = aacgmv2.convert_latlon(*self.in_args)
89
        np.testing.assert_allclose(self.out, ref, rtol=self.rtol)
90
91
    @pytest.mark.parametrize('lat,ref',
92
                             [(90.01, [83.927161, 170.1471396, 1.04481923]),
93
                              (-90.01, [-74.9814852, 17.990332, 1.044819236])])
94
    def test_convert_latlon_high_lat(self, lat, ref):
95
        """Test single latlon conversion with latitude just out of bounds.
96
97
        Parameters
98
        ----------
99
        lat : float
100
            Latitude in degrees N
101
        ref : list
102
            Expected output values
103
104
        """
105
        self.in_args[0] = lat
106
        self.in_args.extend([300, self.dtime, 'G2A'])
107
        self.out = aacgmv2.convert_latlon(*self.in_args)
108
        np.testing.assert_allclose(self.out, ref, rtol=self.rtol)
109
110
    def test_convert_latlon_datetime_date(self):
111
        """Test single latlon conversion with date and datetime input."""
112
        self.in_args.extend([300, self.ddate, 'TRACE'])
113
        self.out = aacgmv2.convert_latlon(*self.in_args)
114
        np.testing.assert_allclose(self.out, [58.2268, 81.1613, 1.0457],
115
                                   rtol=self.rtol)
116
117
    def test_convert_latlon_location_failure(self):
118
        """Test single value latlon conversion with a bad location."""
119
        self.out = aacgmv2.convert_latlon(0, 0, 0, self.dtime, self.in_args[-1])
120
        assert np.all(np.isnan(np.array(self.out)))
121
122
    def test_convert_latlon_maxalt_failure(self):
123
        """test convert_latlon failure for an altitude too high for coeffs."""
124
        self.in_args.extend([2001, self.dtime, ""])
125
        self.out = aacgmv2.convert_latlon(*self.in_args)
126
        assert np.all(np.isnan(np.array(self.out)))
127
128
    @pytest.mark.parametrize('in_rep,in_irep,msg',
129
                             [(None, 3, "must be a datetime object"),
130
                              (91, 0, "unrealistic latitude"),
131
                              (-91, 0, "unrealistic latitude"),
132
                              (None, 4, "unknown method code")])
133
    def test_convert_latlon_failure(self, in_rep, in_irep, msg):
134
        """Test ValueError raised with bad inputs.
135
136
        Parameters
137
        ----------
138
        in_ref : int or NoneType
139
            Input value
140
        in_irep : int
141
            Input index
142
        msg : str
143
            Expected error message
144
145
        """
146
        self.in_args.extend([300, self.dtime, "G2A"])
147
        self.in_args[in_irep] = in_rep
148
        with pytest.raises(ValueError, match=msg):
149
            aacgmv2.convert_latlon(*self.in_args)
150
151
152
class TestConvertLatLonArr(TestConvertArray):
153
    """Unit tests for Lat/Lon array conversion."""
154
155
    def test_convert_latlon_arr_large(self):
156
        """Test array latlon conversion for a large array."""
157
        # Update input
158
        self.lat_in = np.full(shape=(6000,), fill_value=self.lat_in[0])
159
        self.lon_in = np.full(shape=(6000,), fill_value=self.lon_in[0])
160
161
        # Update expected output
162
        self.ref[0] = np.full(shape=(6000,), fill_value=self.ref[0][0])
163
        self.ref[1] = np.full(shape=(6000,), fill_value=self.ref[1][0])
164
        self.ref[2] = np.full(shape=(6000,), fill_value=1.0457)
165
166
        # Run the conversion and evaluate
167
        self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in,
168
                                              self.alt_in[0], self.dtime,
169
                                              self.method)
170
        self.evaluate_output()
171
172
    def test_convert_latlon_arr_single_val(self):
173
        """Test array latlon conversion for a single value."""
174
        self.out = aacgmv2.convert_latlon_arr(self.lat_in[0], self.lon_in[0],
175
                                              self.alt_in[0], self.dtime,
176
                                              self.method)
177
178
        self.ref[2][0] = 1.0457
179
        self.evaluate_output(ind=0)
180
181
    def test_convert_latlon_arr_arr_single(self):
182
        """Test array latlon conversion for array input of shape (1,)."""
183
        self.out = aacgmv2.convert_latlon_arr(np.array([self.lat_in[0]]),
184
                                              np.array([self.lon_in[0]]),
185
                                              np.array([self.alt_in[0]]),
186
                                              self.dtime, self.method)
187
188
        self.ref[2][0] = 1.0457
189
        self.evaluate_output(ind=0)
190
191
    def test_convert_latlon_arr_list_single(self):
192
        """Test array latlon conversion for list input of single values."""
193
        self.out = aacgmv2.convert_latlon_arr([self.lat_in[0]],
194
                                              [self.lon_in[0]],
195
                                              [self.alt_in[0]], self.dtime,
196
                                              self.method)
197
198
        self.ref[2][0] = 1.0457
199
        self.evaluate_output(ind=0)
200
201
    def test_convert_latlon_arr_list(self):
202
        """Test array latlon conversion for list input."""
203
        self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in,
204
                                              self.alt_in, self.dtime,
205
                                              self.method)
206
207
        self.ref[2] = [1.0457, 1.0456]
208
        self.evaluate_output()
209
210
    def test_convert_latlon_arr_arr(self):
211
        """Test array latlon conversion for array input."""
212
        self.out = aacgmv2.convert_latlon_arr(np.array(self.lat_in),
213
                                              np.array(self.lon_in),
214
                                              np.array(self.alt_in),
215
                                              self.dtime, self.method)
216
217
        self.ref[2] = [1.0457, 1.0456]
218
        self.evaluate_output()
219
220
    def test_convert_latlon_arr_list_mix(self):
221
        """Test array latlon conversion for mixed types with list."""
222
        self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in[0],
223
                                              self.alt_in[0], self.dtime,
224
                                              self.method)
225
226
        self.ref[2] = [1.0457, 1.0456]
227
        self.evaluate_output()
228
229
    def test_convert_latlon_arr_arr_mix(self):
230
        """Test array latlon conversion for mixed type with an array."""
231
        self.out = aacgmv2.convert_latlon_arr(np.array(self.lat_in),
232
                                              self.lon_in[0], self.alt_in[0],
233
                                              self.dtime, self.method)
234
235
        self.ref[2] = [1.0457, 1.0456]
236
        self.evaluate_output()
237
238
    def test_convert_latlon_arr_arr_mult_and_single_element(self):
239
        """Test latlon conversion for arrays with multiple and single vals."""
240
        self.out = aacgmv2.convert_latlon_arr(np.array(self.lat_in),
241
                                              np.array([self.lon_in[0]]),
242
                                              np.array(self.alt_in),
243
                                              self.dtime, self.method)
244
245
        self.ref[2] = [1.0457, 1.0456]
246
        self.evaluate_output()
247
248
    @pytest.mark.parametrize('method_code,alt,local_ref',
249
                             [("BADIDEA", 3000.0,
250
                               [[64.3580], [83.2895], [1.4694]]),
251
                              ("BADIDEA|TRACE", 7000.0,
252
                               [[69.3187], [85.0845], [2.0973]])])
253
    def test_convert_latlon_arr_badidea(self, method_code, alt, local_ref):
254
        """Test array latlon conversion for BADIDEA.
255
256
        Parameters
257
        ----------
258
        method_code : str
259
            Input method code
260
        alt : float
261
            Altitude in km
262
        local_ref : list
263
            Expected output values
264
265
        """
266
        self.out = aacgmv2.convert_latlon_arr(self.lat_in[0], self.lon_in[0],
267
                                              [alt], self.dtime, method_code)
268
        self.ref = local_ref
269
        self.evaluate_output()
270
271
    def test_convert_latlon_arr_location_failure(self):
272
        """Test array latlon conversion with a bad location."""
273
274
        with warnings.catch_warnings():
275
            # Causes all warnings to be surpressed
276
            warnings.simplefilter("ignore")
277
278
            # Trigger a warning
279
            self.out = aacgmv2.convert_latlon_arr([0], [0], [0], self.dtime, "")
280
281
            # Test the output
282
            np.testing.assert_equal(len(self.out), len(self.ref))
283
            assert np.any(~np.isfinite(np.array(self.out)))
284
285
    def test_convert_latlon_arr_datetime_date(self):
286
        """Test array latlon conversion with date and datetime input."""
287
        self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in,
288
                                              self.alt_in, self.ddate,
289
                                              self.method)
290
291
        self.ref[2] = [1.0457, 1.0456]
292
        self.evaluate_output()
293
294
    def test_convert_latlon_arr_clip(self):
295
        """Test array latlon conversion with latitude clipping."""
296
        self.lat_in = [90.01, -90.01]
297
        self.ref = [[83.92352053, -74.98110552], [170.1381271, 17.98164313],
298
                    [1.04481924, 1.04481924]]
299
        self.out = aacgmv2.convert_latlon_arr(self.lat_in, self.lon_in,
300
                                              self.alt_in, self.ddate,
301
                                              self.method)
302
        self.evaluate_output()
303
304
    def test_convert_latlon_arr_maxalt_failure(self):
305
        """Test convert_latlon_arr failure for altitudes too high for coeffs."""
306
        self.method = ""
307
        self.out = aacgmv2.convert_latlon_arr(self.lat_in[0], self.lon_in[0],
308
                                              [2001], self.dtime, self.method)
309
        assert np.all(np.isnan(np.array(self.out)))
310
311
    @pytest.mark.parametrize('in_rep,in_irep,msg',
312
                             [(None, 3, "must be a datetime object"),
313
                              ([np.full(shape=(3, 2), fill_value=50.0), 0],
314
                               [0, 1], "unable to process multi-dimensional"),
315
                              ([50, 60, 70], 0, "arrays are mismatched"),
316
                              ([[91, 60, -91], 0, 300], [0, 1, 2],
317
                               "unrealistic latitude"),
318
                              (None, 4, "unknown method code")])
319
    def test_convert_latlon_arr_failure(self, in_rep, in_irep, msg):
320
        in_args = np.array([self.lat_in, self.lon_in, self.alt_in, self.dtime,
321
                            "G2A"], dtype=object)
322
        in_args[in_irep] = in_rep
323
        with pytest.raises(ValueError, match=msg):
324
            aacgmv2.convert_latlon_arr(*in_args)
325
326
327
class TestGetAACGMCoord(object):
328
    """Unit tests for AACGM coordinate conversion."""
329
    def setup_method(self):
330
        """Create a clean test environment."""
331
        self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0)
332
        self.ddate = dt.date(2015, 1, 1)
333
        self.in_args = [60, 0]
334
        self.out = None
335
        self.rtol = 1.0e-4
336
337
    def teardown_method(self):
338
        """Clean up the test envrionment."""
339
        del self.out, self.in_args, self.rtol, self.dtime, self.ddate
340
341
    @pytest.mark.parametrize('alt,method_code,ref',
342
                             [(300, 'TRACE', [58.2268, 81.1613, 0.1888]),
343
                              (3000.0, "G2A|BADIDEA", [64.3578, 83.2895,
344
                                                       0.3307]),
345
                              (7000.0, "G2A|TRACE|BADIDEA",
346
                               [69.3187, 85.0845, 0.4503])])
347
    def test_get_aacgm_coord(self, alt, method_code, ref):
348
        """Test single value AACGMV2 calculation, defaults to TRACE.
349
350
        Parameters
351
        ----------
352
        alt : float
353
            Input alitude in km
354
        method_code : str
355
            Calc/tracing method code
356
        ref : list
357
            Expected output values
358
359
        """
360
        self.in_args.extend([alt, self.dtime, method_code])
361
        self.out = aacgmv2.get_aacgm_coord(*self.in_args)
362
        np.testing.assert_allclose(self.out, ref, rtol=self.rtol)
363
364
    def test_get_aacgm_coord_datetime_date(self):
365
        """Test single AACGMV2 calculation with date and datetime input."""
366
        self.in_args.extend([300.0, self.ddate, 'TRACE'])
367
        self.out = aacgmv2.get_aacgm_coord(*self.in_args)
368
        np.testing.assert_allclose(self.out, [58.2268, 81.1613, 0.1888],
369
                                   rtol=self.rtol)
370
371
    def test_get_aacgm_coord_location_failure(self):
372
        """Test single value AACGMV2 calculation with a bad location."""
373
        self.in_args.extend([0.0, self.dtime, 'TRACE'])
374
        self.in_args[0] = 0.0
375
376
        self.out = aacgmv2.get_aacgm_coord(*self.in_args)
377
        np.all(np.isnan(np.array(self.out)))
378
379
    def test_get_aacgm_coord_maxalt_failure(self):
380
        """Test get_aacgm_coord failure for an altitude too high for coeffs."""
381
        self.in_args.extend([2001, self.dtime, ""])
382
        self.out = aacgmv2.get_aacgm_coord(*self.in_args)
383
        assert np.all(np.isnan(np.array(self.out)))
384
385
    @pytest.mark.parametrize('in_index,value',
386
                             [(3, None), (0, 91.0), (0, -91.0)])
387
    def test_get_aacgm_coord_raise_value_error(self, in_index, value):
388
        """Test different ways to raise a ValueError.
389
390
        Parameters
391
        ----------
392
        in_index : int
393
            Input index to replace
394
        value : NoneType or float
395
            Bad value to use as input
396
397
        """
398
        self.in_args.extend([300.0, self.dtime])
399
        self.in_args[in_index] = value
400
        with pytest.raises(ValueError):
401
            self.out = aacgmv2.get_aacgm_coord(*self.in_args)
402
403
404
class TestGetAACGMCoordArr(TestConvertArray):
405
    """Unit tests for AACGM coordinate array conversion."""
406
407
    def test_get_aacgm_coord_arr_large(self):
408
        """Test array conversion for a large array."""
409
        # Update input
410
        self.lat_in = np.full(shape=(6000,), fill_value=self.lat_in[0])
411
        self.lon_in = np.full(shape=(6000,), fill_value=self.lon_in[0])
412
        self.alt_in = np.full(shape=(6000,), fill_value=self.alt_in[0])
413
414
        # Update expected output
415
        self.ref[0] = np.full(shape=(6000,), fill_value=self.ref[0][0])
416
        self.ref[1] = np.full(shape=(6000,), fill_value=self.ref[1][0])
417
        self.ref[2] = np.full(shape=(6000,), fill_value=self.ref[2][0])
418
419
        # Run the conversion and evaluate
420
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in,
421
                                               self.alt_in, self.dtime,
422
                                               self.method)
423
        self.evaluate_output()
424
425
    def test_get_aacgm_coord_arr_single_val(self):
426
        """Test array AACGMV2 calculation for a single value."""
427
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in[0], self.lon_in[0],
428
                                               self.alt_in[0], self.dtime,
429
                                               self.method)
430
        self.evaluate_output(ind=0)
431
432
    def test_get_aacgm_coord_arr_list_single(self):
433
        """Test array AACGMV2 calculation for list input of single values."""
434
        self.out = aacgmv2.get_aacgm_coord_arr([self.lat_in[0]],
435
                                               [self.lon_in[0]],
436
                                               [self.alt_in[0]], self.dtime,
437
                                               self.method)
438
        self.evaluate_output(ind=0)
439
440
    def test_get_aacgm_coord_arr_arr_single(self):
441
        """Test array AACGMV2 calculation for array with a single value."""
442
        self.out = aacgmv2.get_aacgm_coord_arr(np.array([self.lat_in[0]]),
443
                                               np.array([self.lon_in[0]]),
444
                                               np.array([self.alt_in[0]]),
445
                                               self.dtime, self.method)
446
        self.evaluate_output(ind=0)
447
448
    def test_get_aacgm_coord_arr_list(self):
449
        """Test array AACGMV2 calculation for list input."""
450
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in,
451
                                               self.alt_in, self.dtime,
452
                                               self.method)
453
        self.evaluate_output()
454
455
    def test_get_aacgm_coord_arr_arr(self):
456
        """Test array AACGMV2 calculation for an array."""
457
        self.out = aacgmv2.get_aacgm_coord_arr(np.array(self.lat_in),
458
                                               np.array(self.lon_in),
459
                                               np.array(self.alt_in),
460
                                               self.dtime, self.method)
461
        self.evaluate_output()
462
463
    def test_get_aacgm_coord_arr_list_mix(self):
464
        """Test array AACGMV2 calculation for a list and floats."""
465
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in[0],
466
                                               self.alt_in[0], self.dtime,
467
                                               self.method)
468
        self.evaluate_output()
469
470
    def test_get_aacgm_coord_arr_arr_mix(self):
471
        """Test array AACGMV2 calculation for an array and floats."""
472
        self.out = aacgmv2.get_aacgm_coord_arr(np.array(self.lat_in),
473
                                               self.lon_in[0], self.alt_in[0],
474
                                               self.dtime, self.method)
475
        self.evaluate_output()
476
477
    def test_get_aacgm_coord_arr_badidea(self):
478
        """Test array AACGMV2 calculation for BADIDEA."""
479
        self.method = "|".join([self.method, "BADIDEA"])
480
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in[0], self.lon_in[0],
481
                                               [3000.0], self.dtime,
482
                                               self.method)
483
        self.ref = [[64.3481], [83.2885], [0.3306]]
484
        self.evaluate_output()
485
486
    def test_get_aacgm_coord_arr_location_failure(self):
487
        """Test array AACGMV2 calculation with a bad location."""
488
        self.out = aacgmv2.get_aacgm_coord_arr([0], [0], [0], self.dtime,
489
                                               self.method)
490
491
        np.testing.assert_equal(len(self.out), len(self.ref))
492
        assert [isinstance(oo, np.ndarray) and len(oo) == 1 for oo in self.out]
493
        assert np.any([np.isnan(oo) for oo in self.out])
494
495
    def test_get_aacgm_coord_arr_mult_failure(self):
496
        """Test aacgm_coord_arr failure with multi-dim array input."""
497
498
        with pytest.raises(ValueError):
499
            (self.mlat_out, self.mlon_out,
500
             self.mlt_out) = aacgmv2.get_aacgm_coord_arr(
501
                 np.array([[60, 61, 62], [63, 64, 65]]), 0, 300, self.dtime)
502
503
    def test_get_aacgm_coord_arr_time_failure(self):
504
        """Test array AACGMV2 calculation with a bad time."""
505
        with pytest.raises(ValueError):
506
            aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in, self.alt_in,
507
                                        None, self.method)
508
509
    def test_get_aacgm_coord_arr_mlat_failure(self):
510
        """Test error return for co-latitudes above 90 for an array."""
511
512
        self.lat_in = [91, 60, -91]
513
        with pytest.raises(ValueError):
514
            self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in[0],
515
                                                   self.alt_in[0], self.dtime,
516
                                                   self.method)
517
518
    def test_get_aacgm_coord_arr_datetime_date(self):
519
        """Test array AACGMV2 calculation with date and datetime input."""
520
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in,
521
                                               self.alt_in, self.ddate,
522
                                               self.method)
523
        self.ref = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in,
524
                                               self.alt_in, self.dtime,
525
                                               self.method)
526
        self.evaluate_output()
527
528
    def test_get_aacgm_coord_arr_maxalt_failure(self):
529
        """Test aacgm_coord_arr failure for an altitude too high for coeff."""
530
        self.method = ""
531
        self.alt_in = [2001 for ll in self.lat_in]
532
        self.out = aacgmv2.get_aacgm_coord_arr(self.lat_in, self.lon_in,
533
                                               self.alt_in, self.dtime,
534
                                               self.method)
535
536
        np.testing.assert_equal(len(self.out), len(self.ref))
537
        assert [isinstance(oo, np.ndarray) and len(oo) == len(self.lat_in)
538
                for oo in self.out]
539
        assert np.all(np.isnan(np.array(self.out)))
540
541
542
class TestConvertCode(object):
543
    """Unit tests for the conversion codes."""
544
    def setup_method(self):
545
        """Create a clean test environment."""
546
        self.c_method_code = None
547
        self.ref_code = None
548
        self.out = None
549
550
    def teardown_method(self):
551
        """Clean up the test envrionment."""
552
        del self.c_method_code, self.ref_code, self.out
553
554
    def set_c_code(self):
555
        """Set desired C method code."""
556
        if self.ref_code is not None:
557
            self.ref_code = self.ref_code.upper()
558
            self.c_method_code = getattr(aacgmv2._aacgmv2, self.ref_code)
559
560
    def set_bad_c_code(self):
561
        """Raise a failure through a bad code name."""
562
        self.ref_code = "not_a_valid_code"
563
        with pytest.raises(AttributeError):
564
            self.set_c_code()
565
566
    @pytest.mark.parametrize('method_code',
567
                             [('G2A'), ('A2G'), ('TRACE'), ('ALLOWTRACE'),
568
                              ('BADIDEA'), ('GEOCENTRIC'), ('g2a')])
569
    def test_standard_convert_str_to_bit(self, method_code):
570
        """Test conversion from string code to bit for standard cases.
571
572
        Parameters
573
        ----------
574
        method_code : str
575
            Calculcation and tracing method code
576
577
        """
578
        self.ref_code = method_code
579
        self.set_c_code()
580
        self.out = aacgmv2.convert_str_to_bit(method_code)
581
582
        np.testing.assert_equal(self.out, self.c_method_code)
583
584
    @pytest.mark.parametrize('str_code,bit_ref',
585
                             [("G2A | trace",
586
                               aacgmv2._aacgmv2.G2A + aacgmv2._aacgmv2.TRACE),
587
                              ("ggoogg|", aacgmv2._aacgmv2.G2A)])
588
    def test_non_standard_convert_str_to_bit(self, str_code, bit_ref):
589
        """Test conversion from string code to bit for non-standard cases.
590
591
        Parameters
592
        ----------
593
        str_code : str
594
            String value of code
595
        bit_ref : int
596
            Integer value of code provided by attributes
597
598
        """
599
        self.out = aacgmv2.convert_str_to_bit(str_code)
600
        np.testing.assert_equal(self.out, bit_ref)
601
602
    @pytest.mark.parametrize('bool_dict,method_code',
603
                             [({}, 'G2A'), ({'a2g': True}, 'A2G'),
604
                              ({'trace': True}, 'TRACE'),
605
                              ({'allowtrace': True}, 'ALLOWTRACE'),
606
                              ({'badidea': True}, 'BADIDEA'),
607
                              ({'geocentric': True}, 'GEOCENTRIC')])
608
    def test_convert_bool_to_bit(self, bool_dict, method_code):
609
        """Test conversion from Boolean code to bit.
610
611
        Parameters
612
        ----------
613
        bool_dict : dict
614
            Dict with boolean flags
615
        method_code : str
616
            Expected output code
617
618
        """
619
        self.ref_code = method_code
620
        self.set_c_code()
621
        self.out = aacgmv2.convert_bool_to_bit(**bool_dict)
622
623
        np.testing.assert_equal(self.out, self.c_method_code)
624
625
626
class TestMLTConvert(object):
627
    """Unit tests for MLT conversion."""
628
629
    def setup_method(self):
630
        """Create a clean test environment."""
631
        self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0)
632
        self.dtime2 = dt.datetime(2015, 1, 1, 10, 0, 0)
633
        self.ddate = dt.date(2015, 1, 1)
634
        self.mlon_out = None
635
        self.mlt_out = None
636
        self.mlt_diff = None
637
        self.mlon_list = [270.0, 80.0, -95.0]
638
        self.mlt_list = [12.0, 25.0, -1.0]
639
        self.mlon_comp = [-101.670617955439, 93.329382044561, 63.329382044561]
640
        self.mlt_comp = [12.7780412, 0.11137453, 12.44470786]
641
        self.diff_comp = np.ones(shape=(3,)) * -10.52411552
642
643
    def teardown_method(self):
644
        """Clean up the test envrionment."""
645
        del self.mlon_out, self.mlt_out, self.mlt_list, self.mlon_list
646
        del self.mlon_comp, self.mlt_comp, self.mlt_diff, self.diff_comp
647
648
    def test_date_input(self):
649
        """Test to see that the date input works."""
650
        self.mlt_out = aacgmv2.convert_mlt(self.mlon_list, self.ddate,
651
                                           m2a=False)
652
        np.testing.assert_allclose(self.mlt_out, self.mlt_comp, rtol=1.0e-4)
653
654
    def test_datetime_exception(self):
655
        """Test to see that a value error is raised with bad time input."""
656
        with pytest.raises(ValueError):
657
            self.mlt_out = aacgmv2.wrapper.convert_mlt(self.mlon_list, 1997)
658
659
    def test_inv_convert_mlt_single(self):
660
        """Test MLT inversion for a single value."""
661
        for i, mlt in enumerate(self.mlt_list):
662
            self.mlon_out = aacgmv2.convert_mlt(mlt, self.dtime, m2a=True)
663
            np.testing.assert_almost_equal(self.mlon_out, self.mlon_comp[i],
664
                                           decimal=4)
665
666
    def test_inv_convert_mlt_list(self):
667
        """Test MLT inversion for a list."""
668
        self.mlon_out = aacgmv2.convert_mlt(self.mlt_list, self.dtime, m2a=True)
669
        np.testing.assert_allclose(self.mlon_out, self.mlon_comp, rtol=1.0e-4)
670
671
    def test_inv_convert_mlt_arr(self):
672
        """Test MLT inversion for an array."""
673
        self.mlon_out = aacgmv2.convert_mlt(np.array(self.mlt_list), self.dtime,
674
                                            m2a=True)
675
676
        np.testing.assert_allclose(self.mlon_out, self.mlon_comp, rtol=1.0e-4)
677
678
    def test_inv_convert_mlt_wrapping(self):
679
        """Test MLT wrapping."""
680
        self.mlon_out = aacgmv2.convert_mlt(np.array([1, 25, -1, 23]),
681
                                            self.dtime, m2a=True)
682
683
        np.testing.assert_almost_equal(self.mlon_out[0], self.mlon_out[1],
684
                                       decimal=6)
685
        np.testing.assert_almost_equal(self.mlon_out[2], self.mlon_out[3],
686
                                       decimal=6)
687
688
    def test_mlt_convert_mlon_wrapping(self):
689
        """Test magnetic longitude wrapping."""
690
        self.mlt_out = aacgmv2.convert_mlt(np.array([270, -90, 1, 361]),
691
                                           self.dtime, m2a=False)
692
693
        np.testing.assert_almost_equal(self.mlt_out[0], self.mlt_out[1],
694
                                       decimal=6)
695
        np.testing.assert_almost_equal(self.mlt_out[2], self.mlt_out[3],
696
                                       decimal=6)
697
698
    def test_mlt_convert_single(self):
699
        """Test MLT calculation for a single value."""
700
        for i, mlon in enumerate(self.mlon_list):
701
            self.mlt_out = aacgmv2.convert_mlt(mlon, self.dtime, m2a=False)
702
            np.testing.assert_almost_equal(self.mlt_out, self.mlt_comp[i],
703
                                           decimal=4)
704
705
    def test_mlt_convert_list(self):
706
        """Test MLT calculation for a list."""
707
        self.mlt_out = aacgmv2.convert_mlt(self.mlon_list, self.dtime,
708
                                           m2a=False)
709
        np.testing.assert_allclose(self.mlt_out, self.mlt_comp, rtol=1.0e-4)
710
711
    def test_mlt_convert_arr(self):
712
        """Test MLT calculation for an array."""
713
        self.mlt_out = aacgmv2.convert_mlt(np.array(self.mlon_list),
714
                                           self.dtime, m2a=False)
715
        np.testing.assert_allclose(self.mlt_out, self.mlt_comp, rtol=1.0e-4)
716
717
    def test_mlt_convert_list_w_times(self):
718
        """Test MLT calculation for data and time arrays."""
719
        self.dtime = [self.dtime for dd in self.mlon_list]
720
        self.mlt_out = aacgmv2.convert_mlt(self.mlon_list, self.dtime,
721
                                           m2a=False)
722
        np.testing.assert_allclose(self.mlt_out, self.mlt_comp, rtol=1.0e-4)
723
724
    def test_mlt_convert_change(self):
725
        """Test that MLT changes with UT."""
726
        self.mlt_out = aacgmv2.convert_mlt(self.mlon_list, self.dtime)
727
        self.mlt_diff = np.array(self.mlt_out) \
728
            - np.array(aacgmv2.convert_mlt(self.mlon_list, self.dtime2))
729
730
        np.testing.assert_allclose(self.mlt_diff, self.diff_comp, rtol=1.0e-4)
731
732
    def test_mlt_convert_multidim_failure(self):
733
        """Test MLT calculation failure for multi-dimensional arrays."""
734
        self.mlon_list = np.full(shape=(3, 2), fill_value=50.0)
735
        with pytest.raises(ValueError):
736
            aacgmv2.convert_mlt(self.mlon_list, self.dtime, m2a=False)
737
738
    def test_mlt_convert_mismatch_failure(self):
739
        """Test MLT calculation failure for mismatched array input."""
740
        with pytest.raises(ValueError):
741
            aacgmv2.convert_mlt(self.mlon_list, [self.dtime, self.dtime2],
742
                                m2a=False)
743
744
745
class TestCoeffPath(object):
746
    """Unit tests for the coefficient path."""
747
748
    def setup_method(self):
749
        """Create a clean test environment."""
750
        os.environ['IGRF_COEFFS'] = "default_igrf"
751
        os.environ['AACGM_v2_DAT_PREFIX'] = "default_coeff"
752
        self.ref = {"igrf_file": os.environ['IGRF_COEFFS'],
753
                    "coeff_prefix": os.environ['AACGM_v2_DAT_PREFIX']}
754
755
    def teardown_method(self):
756
        """Clean up the test envrionment."""
757
        del self.ref
758
759
    @pytest.mark.parametrize("in_coeff",
760
                             [({}),
761
                              ({"igrf_file": "hi", "coeff_prefix": "bye"}),
762
                              ({"igrf_file": True, "coeff_prefix": True}),
763
                              ({"coeff_prefix": "hi"}),
764
                              ({"igrf_file": "hi"}),
765
                              ({"igrf_file": None, "coeff_prefix": None})])
766
    def test_set_coeff_path(self, in_coeff):
767
        """Test the coefficient path setting using default values.
768
769
        Parameters
770
        ----------
771
        in_coeff : dict
772
           Input coefficient dictionary
773
774
        """
775
        # Update the reference key, if needed
776
        for ref_key in in_coeff.keys():
777
            if in_coeff[ref_key] is True or in_coeff[ref_key] is None:
778
                if ref_key == "igrf_file":
779
                    self.ref[ref_key] = aacgmv2.IGRF_COEFFS
780
                elif ref_key == "coeff_prefix":
781
                    self.ref[ref_key] = aacgmv2.AACGM_v2_DAT_PREFIX
782
            else:
783
                self.ref[ref_key] = in_coeff[ref_key]
784
785
        # Run the routine
786
        aacgmv2.wrapper.set_coeff_path(**in_coeff)
787
788
        # Ensure the environment variables were set correctly
789
        if os.environ['IGRF_COEFFS'] != self.ref['igrf_file']:
790
            raise AssertionError("{:} != {:}".format(os.environ['IGRF_COEFFS'],
791
                                                     self.ref['igrf_file']))
792
        if os.environ['AACGM_v2_DAT_PREFIX'] != self.ref['coeff_prefix']:
793
            raise AssertionError("{:} != {:}".format(
794
                os.environ['AACGM_v2_DAT_PREFIX'], self.ref['coeff_prefix']))
795
796
797
class TestHeightReturns(object):
798
    """Unit tests for heights."""
799
800
    def setup_method(self):
801
        """Create a clean test environment."""
802
        self.code = aacgmv2._aacgmv2.A2G
803
        self.bad_code = aacgmv2._aacgmv2.BADIDEA
804
        self.trace_code = aacgmv2._aacgmv2.TRACE
805
806
    def teardown_method(self):
807
        """Clean up the test envrionment."""
808
        del self.code, self.bad_code
809
810
    def test_low_height_good(self):
811
        """Test to see that a very low height is still accepted."""
812
813
        assert aacgmv2.wrapper.test_height(-1, self.code)
814
815
    def test_high_coeff_bad(self):
816
        """Test to see that a high altitude for coefficent use fails."""
817
818
        assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0,
819
                                               self.code)
820
821
    def test_high_coeff_good(self):
822
        """Test a high altitude for coefficent use with badidea."""
823
824
        assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0,
825
                                           self.bad_code)
826
827
    def test_low_coeff_good(self):
828
        """Test that a normal height succeeds."""
829
        assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff * 0.5,
830
                                           self.code)
831
832
    def test_high_trace_bad(self):
833
        """Test that a high trace height fails."""
834
        assert not aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace + 10.0,
835
                                               self.code)
836
837
    def test_low_trace_good(self):
838
        """Test that a high coefficient height succeeds with trace."""
839
        assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_coeff + 10.0,
840
                                           self.trace_code)
841
842
    def test_high_trace_good(self):
843
        """Test that a high trace height succeeds with badidea."""
844
        assert aacgmv2.wrapper.test_height(aacgmv2.high_alt_trace + 10.0,
845
                                           self.bad_code)
846
847
848
class TestPyLogging(object):
849
    """Unit tests for logging output."""
850
851
    def setup_method(self):
852
        """Create a clean test environment."""
853
        self.lwarn = ""
854
        self.lout = ""
855
        self.log_name = "aacgmv2_logger"
856
857
    def teardown_method(self):
858
        """Clean up the test envrionment."""
859
        del self.lwarn, self.lout, self.log_name
860
861
    def eval_logger_message(self):
862
        """Evaluate the logger message."""
863
        assert self.lout.find(self.lwarn) >= 0, "unknown logger message"
864
865
    def test_warning_below_ground(self, caplog):
866
        """Test that a warning is issued if height < 0 for height test."""
867
        self.lwarn = "conversion not intended for altitudes < 0 km"
868
869
        with caplog.at_level(logging.WARNING, logger=self.log_name):
870
            aacgmv2.wrapper.test_height(-1, 0)
871
872
        self.lout = caplog.text
873
        self.eval_logger_message()
874
875
    def test_warning_magnetosphere(self, caplog):
876
        """Test that a warning is issued if altitude is very high."""
877
        self.lwarn = "coordinates are not intended for the magnetosphere"
878
879
        with caplog.at_level(logging.ERROR, logger=self.log_name):
880
            aacgmv2.wrapper.test_height(70000, aacgmv2._aacgmv2.TRACE)
881
882
        self.lout = caplog.text
883
        self.eval_logger_message()
884
885
    def test_warning_high_coeff(self, caplog):
886
        """Test that a warning is issued if altitude is very high."""
887
        self.lwarn = "must either use field-line tracing (trace=True"
888
889
        with caplog.at_level(logging.ERROR, logger=self.log_name):
890
            aacgmv2.wrapper.test_height(3000, 0)
891
892
        self.lout = caplog.text
893
        self.eval_logger_message()
894
895
    def test_warning_single_loc_in_arr(self, caplog):
896
        """Test that user is warned they should be using simpler routine."""
897
        self.lwarn = "for a single location, consider using"
898
899
        with caplog.at_level(logging.INFO, logger=self.log_name):
900
            aacgmv2.convert_latlon_arr(60, 0, 300,
901
                                       dt.datetime(2015, 1, 1, 0, 0, 0))
902
903
        self.lout = caplog.text
904
        self.eval_logger_message()
905
906
    def test_warning_equator(self, caplog):
907
        """Test that user is warned about undefined coordinates."""
908
        self.lwarn = "unable to perform conversion at"
909
910
        with caplog.at_level(logging.WARNING, logger=self.log_name):
911
            aacgmv2.convert_latlon(10.0, 10.0, 300,
912
                                   dt.datetime(2015, 1, 1, 0, 0, 0))
913
914
        self.lout = caplog.text
915
        self.eval_logger_message()
916
917
918
class TestTimeReturns(object):
919
    """Unit tests for time functions."""
920
921
    def setup_method(self):
922
        """Create a clean test environment."""
923
        self.dtime = dt.datetime(2015, 1, 1, 0, 0, 0)
924
        self.dtime2 = dt.datetime(2015, 1, 1, 10, 10, 10)
925
        self.ddate = dt.date(2015, 1, 1)
926
927
    def teardown_method(self):
928
        """Clean up the test envrionment."""
929
        del self.dtime, self.ddate, self.dtime2
930
931
    def test_good_time(self):
932
        """Test to see that a good datetime is accepted."""
933
        assert self.dtime == aacgmv2.wrapper.test_time(self.dtime)
934
935
    def test_good_time_with_nonzero_time(self):
936
        """Test to see that a good datetime with h/m/s is accepted."""
937
        assert self.dtime2 == aacgmv2.wrapper.test_time(self.dtime2)
938
939
    def test_good_date(self):
940
        """Test to see that a good date has a good datetime output."""
941
        assert self.dtime == aacgmv2.wrapper.test_time(self.dtime)
942
943
    def test_bad_time(self):
944
        """Test to see that a warning is raised with a bad time input."""
945
        with pytest.raises(ValueError):
946
            aacgmv2.wrapper.test_time(2015)
947