TestConvertCode.setup_method()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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