TestApexMethod.test_fortran_longitude_rollover()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 47
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
nop 7
dl 0
loc 47
rs 9.376
c 0
b 0
f 0
1
# -*- coding: utf-8 -*-
2
"""Test the apexpy.Apex class
3
4
Notes
5
-----
6
Whenever function outputs are tested against hard-coded numbers, the test
7
results (numbers) were obtained by running the code that is tested.  Therefore,
8
these tests below only check that nothing changes when refactoring, etc., and
9
not if the results are actually correct.
10
11
These results are expected to change when IGRF is updated.
12
13
"""
14
15
import datetime as dt
16
import numpy as np
17
import os
18
import pytest
19
import shutil
20
import warnings
21
22
import apexpy
23
24
25
class TestApexInit(object):
26
    """Test class for the Apex class object."""
27
28
    def setup_method(self):
29
        """Initialize all tests."""
30
        self.apex_out = None
31
        self.test_date = dt.datetime.now(tz=dt.timezone.utc)
32
        self.test_refh = 0
33
        self.bad_file = 'foo/path/to/datafile.blah'
34
        self.temp_file = None
35
36
    def teardown_method(self):
37
        """Clean up after each test."""
38
        del self.apex_out, self.test_date, self.test_refh
39
        del self.bad_file, self.temp_file
40
41
    def eval_date(self):
42
        """Evaluate the times in self.test_date and self.apex_out."""
43
        if isinstance(self.test_date, dt.datetime) \
44
           or isinstance(self.test_date, dt.date):
45
            self.test_date = apexpy.helpers.toYearFraction(self.test_date)
46
47
        # Assert the times are the same on the order of tens of seconds.
48
        # Necessary to evaluate the current UTC
49
        np.testing.assert_almost_equal(self.test_date, self.apex_out.year, 6)
50
        return
51
52
    def eval_refh(self):
53
        """Evaluate the reference height in self.refh and self.apex_out."""
54
        eval_str = "".join(["expected reference height [",
55
                            "{:}] not equal to Apex ".format(self.test_refh),
56
                            "reference height ",
57
                            "[{:}]".format(self.apex_out.refh)])
58
        assert self.test_refh == self.apex_out.refh, eval_str
59
        return
60
61
    def test_init_defaults(self):
62
        """Test Apex class default initialization."""
63
        self.apex_out = apexpy.Apex()
64
        self.eval_date()
65
        self.eval_refh()
66
        return
67
68
    def test_init_today(self):
69
        """Test Apex class initialization with today's date."""
70
        self.apex_out = apexpy.Apex(date=self.test_date)
71
        self.eval_date()
72
        self.eval_refh()
73
        return
74
75
    @pytest.mark.parametrize("in_date",
76
                             [2015, 2015.5, dt.date(2015, 1, 1),
77
                              dt.datetime(2015, 6, 1, 18, 23, 45)])
78
    def test_init_date(self, in_date):
79
        """Test Apex class with date initialization.
80
81
        Parameters
82
        ----------
83
        in_date : int, float, dt.date, or dt.datetime
84
            Input date in a variety of formats
85
86
        """
87
        self.test_date = in_date
88
        self.apex_out = apexpy.Apex(date=self.test_date)
89
        self.eval_date()
90
        self.eval_refh()
91
        return
92
93
    @pytest.mark.parametrize("new_date", [2015, 2015.5])
94
    def test_set_epoch(self, new_date):
95
        """Test successful setting of Apex epoch after initialization.
96
97
        Parameters
98
        ----------
99
        new_date : int or float
100
            New date for the Apex class
101
102
        """
103
        # Evaluate the default initialization
104
        self.apex_out = apexpy.Apex()
105
        self.eval_date()
106
        self.eval_refh()
107
108
        # Update the epoch
109
        ref_apex = eval(self.apex_out.__repr__())
110
        self.apex_out.set_epoch(new_date)
111
        assert ref_apex != self.apex_out
112
        self.test_date = new_date
113
        self.eval_date()
114
        return
115
116
    @pytest.mark.parametrize("in_refh", [0.0, 300.0, 30000.0, -1.0])
117
    def test_init_refh(self, in_refh):
118
        """Test Apex class with reference height initialization.
119
120
        Parameters
121
        ----------
122
        in_refh : float
123
            Input reference height in km
124
125
        """
126
        self.test_refh = in_refh
127
        self.apex_out = apexpy.Apex(refh=self.test_refh)
128
        self.eval_date()
129
        self.eval_refh()
130
        return
131
132
    @pytest.mark.parametrize("new_refh", [0.0, 300.0, 30000.0, -1.0])
133
    def test_set_refh(self, new_refh):
134
        """Test the method used to set the reference height after the init.
135
136
        Parameters
137
        ----------
138
        new_refh : float
139
            Reference height in km
140
141
        """
142
        # Verify the defaults are set
143
        self.apex_out = apexpy.Apex(date=self.test_date)
144
        self.eval_date()
145
        self.eval_refh()
146
147
        # Update to a new reference height and test
148
        ref_apex = eval(self.apex_out.__repr__())
149
        self.apex_out.set_refh(new_refh)
150
151
        if self.test_refh == new_refh:
152
            assert ref_apex == self.apex_out
153
        else:
154
            assert ref_apex != self.apex_out
155
            self.test_refh = new_refh
156
        self.eval_refh()
157
        return
158
159
    def copy_file(self, original, max_attempts=100):
160
        """Make a temporary copy of input file."""
161
        _, ext = os.path.splitext(original)
162
        self.temp_file = 'temp' + ext
163
164
        for _ in range(max_attempts):
165
            try:
166
                shutil.copy(original, self.temp_file)
167
                break
168
            except Exception:
169
                pass
170
171
        return self.temp_file
172
173
    def test_default_datafile(self):
174
        """Test that the class initializes with the default datafile."""
175
        self.apex_out = apexpy.Apex()
176
        assert os.path.isfile(self.apex_out.datafile)
177
        return
178
179
    def test_custom_datafile(self):
180
        """Test that the class initializes with a good datafile input."""
181
182
        # Get the original datafile name
183
        apex_out_orig = apexpy.Apex()
184
        original_file = apex_out_orig.datafile
185
        del apex_out_orig
186
187
        # Create copy of datafile
188
        custom_file = self.copy_file(original_file)
189
190
        apex_out = apexpy.Apex(datafile=custom_file)
191
        assert apex_out.datafile == custom_file
192
193
        os.remove(custom_file)
194
        return
195
196
    def test_init_with_bad_datafile(self):
197
        """Test raises IOError with non-existent datafile input."""
198
        with pytest.raises(IOError) as oerr:
199
            apexpy.Apex(datafile=self.bad_file)
200
        assert str(oerr.value).startswith('Data file does not exist')
201
        return
202
203
    def test_default_fortranlib(self):
204
        """Test that the class initializes with the default fortranlib."""
205
        apex_out = apexpy.Apex()
206
        assert os.path.isfile(apex_out.fortranlib)
207
        return
208
209
    def test_custom_fortranlib(self):
210
        """Test that the class initializes with a good fortranlib input."""
211
212
        # Get the original fortranlib name
213
        apex_out_orig = apexpy.Apex()
214
        original_lib = apex_out_orig.fortranlib
215
        del apex_out_orig
216
217
        # Create copy of datafile
218
        custom_lib = self.copy_file(original_lib)
219
220
        apex_out = apexpy.Apex(fortranlib=custom_lib)
221
        assert apex_out.fortranlib == custom_lib
222
223
        os.remove(custom_lib)
224
        return
225
226
    def test_init_with_bad_fortranlib(self):
227
        """Test raises IOError with non-existent fortranlib input."""
228
        with pytest.raises(IOError) as oerr:
229
            apexpy.Apex(fortranlib=self.bad_file)
230
        assert str(oerr.value).startswith('Fortran library does not exist')
231
        return
232
233
    def test_igrf_fn(self):
234
        """Test the default igrf_fn."""
235
        apex_out = apexpy.Apex()
236
        assert os.path.isfile(apex_out.igrf_fn)
237
        return
238
239
    def test_repr_eval(self):
240
        """Test the Apex.__repr__ results."""
241
        # Initialize the apex object
242
        self.apex_out = apexpy.Apex()
243
        self.eval_date()
244
        self.eval_refh()
245
246
        # Get and test the repr string
247
        out_str = self.apex_out.__repr__()
248
        assert out_str.find("apexpy.Apex(") == 0
249
250
        # Test the ability to re-create the apex object from the repr string
251
        new_apex = eval(out_str)
252
        assert new_apex == self.apex_out
253
        return
254
255
    def test_ne_other_class(self):
256
        """Test Apex class inequality to a different class."""
257
        self.apex_out = apexpy.Apex()
258
        self.eval_date()
259
        self.eval_refh()
260
261
        assert self.apex_out != self.test_date
262
        return
263
264
    def test_ne_missing_attr(self):
265
        """Test Apex class inequality when attributes are missing from one."""
266
        self.apex_out = apexpy.Apex()
267
        self.eval_date()
268
        self.eval_refh()
269
        ref_apex = eval(self.apex_out.__repr__())
270
        del ref_apex.RE
271
272
        assert ref_apex != self.apex_out
273
        assert self.apex_out != ref_apex
274
        return
275
276
    def test_eq_missing_attr(self):
277
        """Test Apex class equality when attributes are missing from both."""
278
        self.apex_out = apexpy.Apex()
279
        self.eval_date()
280
        self.eval_refh()
281
        ref_apex = eval(self.apex_out.__repr__())
282
        del ref_apex.RE, self.apex_out.RE
283
284
        assert ref_apex == self.apex_out
285
        return
286
287
    def test_str_eval(self):
288
        """Test the Apex.__str__ results."""
289
        # Initialize the apex object
290
        self.apex_out = apexpy.Apex()
291
        self.eval_date()
292
        self.eval_refh()
293
294
        # Get and test the printed string
295
        out_str = self.apex_out.__str__()
296
        assert out_str.find("Decimal year") > 0
297
        return
298
299
300
class TestApexMethod(object):
301
    """Test the Apex methods."""
302
    def setup_method(self):
303
        """Initialize all tests."""
304
        self.apex_out = apexpy.Apex(date=2000, refh=300)
305
        self.in_lat = 60
306
        self.in_lon = 15
307
        self.in_alt = 100
308
309
    def teardown_method(self):
310
        """Clean up after each test."""
311
        del self.apex_out, self.in_lat, self.in_lon, self.in_alt
312
313
    def get_input_args(self, method_name, precision=0.0):
314
        """Set the input arguments for the different Apex methods.
315
316
        Parameters
317
        ----------
318
        method_name : str
319
            Name of the Apex class method
320
        precision : float
321
            Value for the precision (default=0.0)
322
323
        Returns
324
        -------
325
        in_args : list
326
            List of the appropriate input arguments
327
328
        """
329
        in_args = [self.in_lat, self.in_lon, self.in_alt]
330
331
        # Add precision, if needed
332
        if method_name in ["_qd2geo", "apxq2g", "apex2geo", "qd2geo",
333
                           "_apex2geo"]:
334
            in_args.append(precision)
335
336
        # Add a reference height, if needed
337
        if method_name in ["apxg2all"]:
338
            in_args.append(300)
339
340
        # Add a vector flag, if needed
341
        if method_name in ["apxg2all", "apxg2q"]:
342
            in_args.append(1)
343
344
        return in_args
345
346
    def test_apex_conversion_today(self):
347
        """Test Apex class conversion with today's date."""
348
        self.apex_out = apexpy.Apex(date=dt.datetime.now(tz=dt.timezone.utc),
349
                                    refh=300)
350
        assert not np.isnan(self.apex_out.geo2apex(self.in_lat, self.in_lon,
351
                                                   self.in_alt)).any()
352
        return
353
354
    @pytest.mark.parametrize("apex_method,fortran_method,fslice",
355
                             [("_geo2qd", "apxg2q", slice(0, 2, 1)),
356
                              ("_geo2apex", "apxg2all", slice(2, 4, 1)),
357
                              ("_qd2geo", "apxq2g", slice(None)),
358
                              ("_basevec", "apxg2q", slice(2, 4, 1))])
359
    @pytest.mark.parametrize("lat", [0, 30, 60, 89])
360
    @pytest.mark.parametrize("lon", [-179, -90, 0, 90, 180])
361
    def test_fortran_scalar_input(self, apex_method, fortran_method, fslice,
362
                                  lat, lon):
363
        """Tests Apex/fortran interface consistency for scalars.
364
365
        Parameters
366
        ----------
367
        apex_method : str
368
            Name of the Apex class method to test
369
        fortran_method : str
370
            Name of the Fortran function to test
371
        fslice : slice
372
            Slice used select the appropriate Fortran outputs
373
        lat : int or float
374
            Latitude in degrees N
375
        lon : int or float
376
            Longitude in degrees E
377
378
        """
379
        # Set the input coordinates
380
        self.in_lat = lat
381
        self.in_lon = lon
382
383
        # Get the Apex class method and the fortran function call
384
        apex_func = getattr(self.apex_out, apex_method)
385
        fortran_func = getattr(apexpy.fortranapex, fortran_method)
386
387
        # Get the appropriate input arguments
388
        apex_args = self.get_input_args(apex_method)
389
        fortran_args = self.get_input_args(fortran_method)
390
391
        # Evaluate the equivalent function calls
392
        np.testing.assert_allclose(apex_func(*apex_args),
393
                                   fortran_func(*fortran_args)[fslice])
394
        return
395
396
    @pytest.mark.parametrize("apex_method,fortran_method,fslice",
397
                             [("_geo2qd", "apxg2q", slice(0, 2, 1)),
398
                              ("_geo2apex", "apxg2all", slice(2, 4, 1)),
399
                              ("_qd2geo", "apxq2g", slice(None)),
400
                              ("_basevec", "apxg2q", slice(2, 4, 1))])
401
    @pytest.mark.parametrize("lat", [0, 30, 60, 89])
402
    @pytest.mark.parametrize("lon1,lon2", [(180, 180), (-180, -180),
403
                                           (180, -180), (-180, 180),
404
                                           (-345, 15), (375, 15)])
405
    def test_fortran_longitude_rollover(self, apex_method, fortran_method,
406
                                        fslice, lat, lon1, lon2):
407
        """Tests Apex/fortran interface consistency for longitude rollover.
408
409
        Parameters
410
        ----------
411
        apex_method : str
412
            Name of the Apex class method to test
413
        fortran_method : str
414
            Name of the Fortran function to test
415
        fslice : slice
416
            Slice used select the appropriate Fortran outputs
417
        lat : int or float
418
            Latitude in degrees N
419
        lon1 : int or float
420
            Longitude in degrees E
421
        lon2 : int or float
422
            Equivalent longitude in degrees E
423
424
        """
425
        # Set the fixed input coordinate
426
        self.in_lat = lat
427
428
        # Get the Apex class method and the fortran function call
429
        apex_func = getattr(self.apex_out, apex_method)
430
        fortran_func = getattr(apexpy.fortranapex, fortran_method)
431
432
        # Get the appropriate input arguments
433
        self.in_lon = lon1
434
        apex_args = self.get_input_args(apex_method)
435
436
        self.in_lon = lon2
437
        fortran_args = self.get_input_args(fortran_method)
438
439
        # Evaluate the equivalent function calls
440
        np.testing.assert_allclose(apex_func(*apex_args),
441
                                   fortran_func(*fortran_args)[fslice])
442
        return
443
444
    @pytest.mark.parametrize("arr_shape", [(2, 2), (4,), (1, 4)])
445
    @pytest.mark.parametrize("apex_method,fortran_method,fslice",
446
                             [("_geo2qd", "apxg2q", slice(0, 2, 1)),
447
                              ("_geo2apex", "apxg2all", slice(2, 4, 1)),
448
                              ("_qd2geo", "apxq2g", slice(None)),
449
                              ("_basevec", "apxg2q", slice(2, 4, 1))])
450
    def test_fortran_array_input(self, arr_shape, apex_method, fortran_method,
451
                                 fslice):
452
        """Tests Apex/fortran interface consistency for array input.
453
454
        Parameters
455
        ----------
456
        arr_shape : tuple
457
            Expected output shape
458
        apex_method : str
459
            Name of the Apex class method to test
460
        fortran_method : str
461
            Name of the Fortran function to test
462
        fslice : slice
463
            Slice used select the appropriate Fortran outputs
464
465
        """
466
        # Get the Apex class method and the fortran function call
467
        apex_func = getattr(self.apex_out, apex_method)
468
        fortran_func = getattr(apexpy.fortranapex, fortran_method)
469
470
        # Set up the input arrays
471
        ref_lat = np.array([0, 30, 60, 90])
472
        ref_alt = np.array([100, 200, 300, 400])
473
        self.in_lat = ref_lat.reshape(arr_shape)
474
        self.in_alt = ref_alt.reshape(arr_shape)
475
        apex_args = self.get_input_args(apex_method)
476
477
        # Get the Apex class results
478
        aret = apex_func(*apex_args)
479
480
        # Get the fortran function results
481
        flats = list()
482
        flons = list()
483
484
        for i, lat in enumerate(ref_lat):
485
            self.in_lat = lat
486
            self.in_alt = ref_alt[i]
487
            fortran_args = self.get_input_args(fortran_method)
488
            fret = fortran_func(*fortran_args)[fslice]
489
            flats.append(fret[0])
490
            flons.append(fret[1])
491
492
        flats = np.array(flats)
493
        flons = np.array(flons)
494
495
        # Evaluate results
496
        try:
497
            # This returned value is array of floats
498
            np.testing.assert_allclose(aret[0].astype(float),
499
                                       flats.reshape(arr_shape).astype(float))
500
            np.testing.assert_allclose(aret[1].astype(float),
501
                                       flons.reshape(arr_shape).astype(float))
502
        except ValueError:
503
            # This returned value is array of arrays
504
            alats = aret[0].reshape((4,))
505
            alons = aret[1].reshape((4,))
506
            for i, flat in enumerate(flats):
507
                np.testing.assert_array_almost_equal(alats[i], flat, 2)
508
                np.testing.assert_array_almost_equal(alons[i], flons[i], 2)
509
510
        return
511
512
    @pytest.mark.parametrize("lat", [0, 30, 60, 89])
513
    @pytest.mark.parametrize("lon", [-179, -90, 0, 90, 180])
514
    def test_geo2apexall_scalar(self, lat, lon):
515
        """Test Apex/fortran geo2apexall interface consistency for scalars.
516
517
        Parameters
518
        ----------
519
        lat : int or float
520
            Latitude in degrees N
521
        long : int or float
522
            Longitude in degrees E
523
524
        """
525
        # Get the Apex and Fortran results
526
        aret = self.apex_out._geo2apexall(lat, lon, self.in_alt)
527
        fret = apexpy.fortranapex.apxg2all(lat, lon, self.in_alt, 300, 1)
528
529
        # Evaluate each element in the results
530
        for aval, fval in zip(aret, fret):
531
            np.testing.assert_allclose(aval, fval)
532
533
    @pytest.mark.parametrize("arr_shape", [(2, 2), (4,), (1, 4)])
534
    def test_geo2apexall_array(self, arr_shape):
535
        """Test Apex/fortran geo2apexall interface consistency for arrays.
536
537
        Parameters
538
        ----------
539
        arr_shape : tuple
540
            Expected output shape
541
542
        """
543
        # Set the input
544
        self.in_lat = np.array([0, 30, 60, 90])
545
        self.in_alt = np.array([100, 200, 300, 400])
546
547
        # Get the Apex class results
548
        aret = self.apex_out._geo2apexall(self.in_lat.reshape(arr_shape),
549
                                          self.in_lon,
550
                                          self.in_alt.reshape(arr_shape))
551
552
        # For each lat/alt pair, get the Fortran results
553
        fret = list()
554
        for i, lat in enumerate(self.in_lat):
555
            fret.append(apexpy.fortranapex.apxg2all(lat, self.in_lon,
556
                                                    self.in_alt[i], 300, 1))
557
558
        # Cycle through all returned values
559
        for i, ret in enumerate(aret):
560
            try:
561
                # This returned value is array of floats
562
                fret_test = np.array([fret[0][i], fret[1][i], fret[2][i],
563
                                      fret[3][i]]).reshape(arr_shape)
564
                np.testing.assert_allclose(ret.astype(float),
565
                                           fret_test.astype(float))
566
            except ValueError:
567
                # This returned value is array of arrays
568
                ret = ret.reshape((4,))
569
                for j, single_fret in enumerate(fret):
570
                    np.testing.assert_allclose(ret[j], single_fret[i])
571
        return
572
573
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
574
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
575
    def test_convert_consistency(self, in_coord, out_coord):
576
        """Test the self-consistency of the Apex convert method.
577
578
        Parameters
579
        ----------
580
        in_coord : str
581
            Input coordinate system
582
        out_coord : str
583
            Output coordinate system
584
585
        """
586
        if in_coord == out_coord:
587
            pytest.skip("Test not needed for same src and dest coordinates")
588
589
        # Define the method name
590
        method_name = "2".join([in_coord, out_coord])
591
592
        # Get the method and method inputs
593
        convert_kwargs = {'height': self.in_alt, 'precision': 0.0}
594
        apex_args = self.get_input_args(method_name)
595
        apex_method = getattr(self.apex_out, method_name)
596
597
        # Define the slice needed to get equivalent output from the named method
598
        mslice = slice(0, -1, 1) if out_coord == "geo" else slice(None)
599
600
        # Get output using convert and named method
601
        convert_out = self.apex_out.convert(self.in_lat, self.in_lon, in_coord,
602
                                            out_coord, **convert_kwargs)
603
        method_out = apex_method(*apex_args)[mslice]
604
605
        # Compare both outputs, should be identical
606
        np.testing.assert_allclose(convert_out, method_out)
607
        return
608
609
    @pytest.mark.parametrize("bound_lat", [90, -90])
610
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
611
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
612
    def test_convert_at_lat_boundary(self, bound_lat, in_coord, out_coord):
613
        """Test the conversion at the latitude boundary, with allowed excess.
614
615
        Parameters
616
        ----------
617
        bound_lat : int or float
618
            Boundary latitude in degrees N
619
        in_coord : str
620
            Input coordinate system
621
        out_coord : str
622
            Output coordinate system
623
624
        """
625
        excess_lat = np.sign(bound_lat) * (abs(bound_lat) + 1.0e-5)
626
627
        # Get the two outputs, slight tolerance outside of boundary allowed
628
        bound_out = self.apex_out.convert(bound_lat, 0, in_coord, out_coord)
629
        excess_out = self.apex_out.convert(excess_lat, 0, in_coord, out_coord)
630
631
        # Test the outputs
632
        np.testing.assert_allclose(excess_out, bound_out, rtol=0, atol=1e-8)
633
        return
634
635
    def test_convert_qd2apex_at_equator(self):
636
        """Test the quasi-dipole to apex conversion at the magnetic equator."""
637
        eq_out = self.apex_out.convert(lat=0.0, lon=0, source='qd', dest='apex',
638
                                       height=320.0)
639
        close_out = self.apex_out.convert(lat=0.001, lon=0, source='qd',
640
                                          dest='apex', height=320.0)
641
        np.testing.assert_allclose(eq_out, close_out, atol=1e-4)
642
        return
643
644
    @pytest.mark.parametrize("src", ["geo", "apex", "qd"])
645
    @pytest.mark.parametrize("dest", ["geo", "apex", "qd"])
646
    def test_convert_withnan(self, src, dest):
647
        """Test Apex.convert success with NaN input.
648
649
        Parameters
650
        ----------
651
        src : str
652
            Input coordinate system
653
        dest : str
654
            Output coordinate system
655
656
        """
657
        if src == dest:
658
            pytest.skip("Test not needed for same src and dest coordinates")
659
660
        num_nans = 5
661
        in_loc = np.arange(0, 10, dtype=float)
662
        in_loc[:num_nans] = np.nan
663
664
        out_loc = self.apex_out.convert(in_loc, in_loc, src, dest, height=320)
665
666
        for out in out_loc:
667
            assert np.all(np.isnan(out[:num_nans])), "NaN output expected"
668
            assert np.all(np.isfinite(out[num_nans:])), "Finite output expected"
669
670
        return
671
672
    @pytest.mark.parametrize("bad_lat", [91, -91])
673
    def test_convert_invalid_lat(self, bad_lat):
674
        """Test convert raises ValueError for invalid latitudes.
675
676
        Parameters
677
        ----------
678
        bad_lat : int or float
679
            Latitude ouside the supported range in degrees N
680
681
        """
682
683
        with pytest.raises(ValueError) as verr:
684
            self.apex_out.convert(bad_lat, 0, 'geo', 'geo')
685
686
        assert str(verr.value).find("must be in [-90, 90]") > 0
687
        return
688
689
    @pytest.mark.parametrize("coords", [("foobar", "geo"), ("geo", "foobar"),
690
                                        ("geo", "mlt")])
691
    def test_convert_invalid_transformation(self, coords):
692
        """Test raises NotImplementedError for bad coordinates.
693
694
        Parameters
695
        ----------
696
        coords : tuple
697
            Tuple specifying the input and output coordinate systems
698
699
        """
700
        if "mlt" in coords:
701
            estr = "datetime must be given for MLT calculations"
702
        else:
703
            estr = "Unknown coordinate transformation"
704
705
        with pytest.raises(ValueError) as verr:
706
            self.apex_out.convert(0, 0, *coords)
707
708
        assert str(verr).find(estr) >= 0
709
        return
710
711 View Code Duplication
    @pytest.mark.parametrize("method_name, out_comp",
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
712
                             [("geo2apex",
713
                               (55.94841766357422, 94.10684204101562)),
714
                              ("apex2geo",
715
                               (51.476322174072266, -66.22817993164062,
716
                                5.727287771151168e-06)),
717
                              ("geo2qd",
718
                               (56.531288146972656, 94.10684204101562)),
719
                              ("apex2qd", (60.498401178276744, 15.0)),
720
                              ("qd2apex", (59.49138097045895, 15.0))])
721
    def test_method_scalar_input(self, method_name, out_comp):
722
        """Test the user method against set values with scalars.
723
724
        Parameters
725
        ----------
726
        method_name : str
727
            Apex class method to be tested
728
        out_comp : tuple of floats
729
            Expected output values
730
731
        """
732
        # Get the desired methods
733
        user_method = getattr(self.apex_out, method_name)
734
735
        # Get the user output
736
        user_out = user_method(self.in_lat, self.in_lon, self.in_alt)
737
738
        # Evaluate the user output
739
        np.testing.assert_allclose(user_out, out_comp, rtol=1e-5, atol=1e-5)
740
741
        for out_val in user_out:
742
            assert np.asarray(out_val).shape == (), "output is not a scalar"
743
        return
744
745
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
746
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
747
    @pytest.mark.parametrize("method_args, out_shape",
748
                             [([[60, 60], 15, 100], (2,)),
749
                              ([60, [15, 15], 100], (2,)),
750
                              ([60, 15, [100, 100]], (2,)),
751
                              ([[50, 60], [15, 16], [100, 200]], (2,))])
752
    def test_method_broadcast_input(self, in_coord, out_coord, method_args,
753
                                    out_shape):
754
        """Test the user method with inputs that require some broadcasting.
755
756
        Parameters
757
        ----------
758
        in_coord : str
759
            Input coordiante system
760
        out_coord : str
761
            Output coordiante system
762
        method_args : list
763
            List of input arguments
764
        out_shape : tuple
765
            Expected shape of output values
766
767
        """
768
        if in_coord == out_coord:
769
            pytest.skip("Test not needed for same src and dest coordinates")
770
771
        # Get the desired methods
772
        method_name = "2".join([in_coord, out_coord])
773
        user_method = getattr(self.apex_out, method_name)
774
775
        # Get the user output
776
        user_out = user_method(*method_args)
777
778
        # Evaluate the user output
779
        for out_val in user_out:
780
            assert hasattr(out_val, 'shape'), "output coordinate isn't np.array"
781
            assert out_val.shape == out_shape
782
        return
783
784
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
785
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
786
    @pytest.mark.parametrize("bad_lat", [91, -91])
787
    def test_method_invalid_lat(self, in_coord, out_coord, bad_lat):
788
        """Test convert raises ValueError for invalid latitudes.
789
790
        Parameters
791
        ----------
792
        in_coord : str
793
            Input coordiante system
794
        out_coord : str
795
            Output coordiante system
796
        bad_lat : int
797
            Latitude in degrees N that is out of bounds
798
799
        """
800
        if in_coord == out_coord:
801
            pytest.skip("Test not needed for same src and dest coordinates")
802
803
        # Get the desired methods
804
        method_name = "2".join([in_coord, out_coord])
805
        user_method = getattr(self.apex_out, method_name)
806
807
        with pytest.raises(ValueError) as verr:
808
            user_method(bad_lat, 15, 100)
809
810
        assert str(verr.value).find("must be in [-90, 90]") > 0
811
        return
812
813
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
814
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
815
    @pytest.mark.parametrize("bound_lat", [90, -90])
816
    def test_method_at_lat_boundary(self, in_coord, out_coord, bound_lat):
817
        """Test user methods at the latitude boundary, with allowed excess.
818
819
        Parameters
820
        ----------
821
        in_coord : str
822
            Input coordiante system
823
        out_coord : str
824
            Output coordiante system
825
        bad_lat : int
826
            Latitude in degrees N that is at the limits of the boundary
827
828
        """
829
        if in_coord == out_coord:
830
            pytest.skip("Test not needed for same src and dest coordinates")
831
832
        # Get the desired methods
833
        method_name = "2".join([in_coord, out_coord])
834
        user_method = getattr(self.apex_out, method_name)
835
836
        # Get a latitude just beyond the limit
837
        excess_lat = np.sign(bound_lat) * (abs(bound_lat) + 1.0e-5)
838
839
        # Get the two outputs, slight tolerance outside of boundary allowed
840
        bound_out = user_method(bound_lat, 0, 100)
841
        excess_out = user_method(excess_lat, 0, 100)
842
843
        # Test the outputs
844
        np.testing.assert_allclose(excess_out, bound_out, rtol=0, atol=1e-8)
845
        return
846
847
    def test_geo2apex_undefined_warning(self):
848
        """Test geo2apex warning and fill values for an undefined location."""
849
850
        # Update the apex object
851
        self.apex_out = apexpy.Apex(date=2000, refh=10000)
852
853
        # Get the output and the warnings
854
        with warnings.catch_warnings(record=True) as warn_rec:
855
            user_lat, user_lon = self.apex_out.geo2apex(0, 0, 0)
856
857
        assert np.isnan(user_lat)
858
        assert np.isfinite(user_lon)
859
        assert len(warn_rec) == 1
860
        assert issubclass(warn_rec[-1].category, UserWarning)
861
        assert 'latitude set to NaN where' in str(warn_rec[-1].message)
862
        return
863
864
    @pytest.mark.parametrize("method_name", ["apex2qd", "qd2apex"])
865
    @pytest.mark.parametrize("delta_h", [1.0e-6, -1.0e-6])
866
    def test_quasidipole_apexheight_close(self, method_name, delta_h):
867
        """Test quasi-dipole success with a height close to the reference.
868
869
        Parameters
870
        ----------
871
        method_name : str
872
            Apex class method name to be tested
873
        delta_h : float
874
            tolerance for height in km
875
876
        """
877
        qd_method = getattr(self.apex_out, method_name)
878
        in_args = [0, 15, self.apex_out.refh + delta_h]
879
        out_coords = qd_method(*in_args)
880
881
        for i, out_val in enumerate(out_coords):
882
            np.testing.assert_almost_equal(out_val, in_args[i], decimal=3)
883
        return
884
885
    @pytest.mark.parametrize("method_name, hinc, msg",
886
                             [("apex2qd", 1.0, "is > apex height"),
887
                              ("qd2apex", -1.0, "is < reference height")])
888
    def test_quasidipole_raises_apexheight(self, method_name, hinc, msg):
889
        """Quasi-dipole raises ApexHeightError when height above reference.
890
891
        Parameters
892
        ----------
893
        method_name : str
894
            Apex class method name to be tested
895
        hinc : float
896
            Height increment in km
897
        msg : str
898
            Expected output message
899
900
        """
901
        qd_method = getattr(self.apex_out, method_name)
902
903
        with pytest.raises(apexpy.ApexHeightError) as aerr:
904
            qd_method(0, 15, self.apex_out.refh + hinc)
905
906
        assert str(aerr).find(msg) > 0
907
        return
908
909
910
class TestApexMLTMethods(object):
911
    """Test the Apex Magnetic Local Time (MLT) methods."""
912
    def setup_method(self):
913
        """Initialize all tests."""
914
        self.apex_out = apexpy.Apex(date=2000, refh=300)
915
        self.in_time = dt.datetime(2000, 2, 3, 4, 5, 6)
916
917
    def teardown_method(self):
918
        """Clean up after each test."""
919
        del self.apex_out, self.in_time
920
921
    @pytest.mark.parametrize("in_coord", ["geo", "apex", "qd"])
922
    def test_convert_to_mlt(self, in_coord):
923
        """Test the conversions to MLT using Apex convert.
924
925
        Parameters
926
        ----------
927
        in_coord : str
928
            Input coordinate system
929
930
        """
931
932
        # Get the magnetic longitude from the appropriate method
933
        if in_coord == "geo":
934
            apex_method = getattr(self.apex_out, "{:s}2apex".format(in_coord))
935
            mlon = apex_method(60, 15, 100)[1]
936
        else:
937
            mlon = 15
938
939
        # Get the output MLT values
940
        convert_mlt = self.apex_out.convert(60, 15, in_coord, 'mlt',
941
                                            height=100, ssheight=2e5,
942
                                            datetime=self.in_time)[1]
943
        method_mlt = self.apex_out.mlon2mlt(mlon, self.in_time, ssheight=2e5)
944
945
        # Test the outputs
946
        np.testing.assert_allclose(convert_mlt, method_mlt)
947
        return
948
949
    @pytest.mark.parametrize("out_coord", ["geo", "apex", "qd"])
950
    def test_convert_mlt_to_lon(self, out_coord):
951
        """Test the conversions from MLT using Apex convert.
952
953
        Parameters
954
        ----------
955
        out_coord : str
956
            Output coordinate system
957
958
        """
959
        # Get the output longitudes
960
        convert_out = self.apex_out.convert(60, 15, 'mlt', out_coord,
961
                                            height=100, ssheight=2e5,
962
                                            datetime=self.in_time,
963
                                            precision=1e-2)
964
        mlon = self.apex_out.mlt2mlon(15, self.in_time, ssheight=2e5)
965
966
        if out_coord == "geo":
967
            method_out = self.apex_out.apex2geo(60, mlon, 100,
968
                                                precision=1e-2)[:-1]
969
        elif out_coord == "qd":
970
            method_out = self.apex_out.apex2qd(60, mlon, 100)
971
        else:
972
            method_out = (60, mlon)
973
974
        # Evaluate the outputs
975
        np.testing.assert_allclose(convert_out, method_out)
976
        return
977
978
    def test_convert_geo2mlt_nodate(self):
979
        """Test convert from geo to MLT raises ValueError with no datetime."""
980
        with pytest.raises(ValueError):
981
            self.apex_out.convert(60, 15, 'geo', 'mlt')
982
        return
983
984
    @pytest.mark.parametrize("mlon_kwargs,test_mlt",
985
                             [({}, 23.019629923502603),
986
                              ({"ssheight": 100000}, 23.026712036132814)])
987
    def test_mlon2mlt_scalar_inputs(self, mlon_kwargs, test_mlt):
988
        """Test mlon2mlt with scalar inputs.
989
990
        Parameters
991
        ----------
992
        mlon_kwargs : dict
993
            Input kwargs
994
        test_mlt : float
995
            Output MLT in hours
996
997
        """
998
        mlt = self.apex_out.mlon2mlt(0, self.in_time, **mlon_kwargs)
999
1000
        np.testing.assert_allclose(mlt, test_mlt)
1001
        assert np.asarray(mlt).shape == ()
1002
        return
1003
1004
    @pytest.mark.parametrize("mlt_kwargs,test_mlon",
1005
                             [({}, 14.705535888671875),
1006
                              ({"ssheight": 100000}, 14.599319458007812)])
1007
    def test_mlt2mlon_scalar_inputs(self, mlt_kwargs, test_mlon):
1008
        """Test mlt2mlon with scalar inputs.
1009
1010
        Parameters
1011
        ----------
1012
        mlt_kwargs : dict
1013
            Input kwargs
1014
        test_mlon : float
1015
            Output longitude in degrees E
1016
1017
        """
1018
        mlon = self.apex_out.mlt2mlon(0, self.in_time, **mlt_kwargs)
1019
1020
        np.testing.assert_allclose(mlon, test_mlon)
1021
        assert np.asarray(mlon).shape == ()
1022
        return
1023
1024
    @pytest.mark.parametrize("mlon,test_mlt",
1025
                             [([0, 180], [23.019261, 11.019261]),
1026
                              (np.array([0, 180]), [23.019261, 11.019261]),
1027
                              (np.array([[0], [180]]),
1028
                               np.array([[23.019261], [11.019261]])),
1029
                              ([[0, 180], [0, 180]], [[23.019261, 11.019261],
1030
                                                      [23.019261, 11.019261]]),
1031
                              (range(0, 361, 30),
1032
                               [23.01963, 1.01963, 3.01963, 5.01963, 7.01963,
1033
                                9.01963, 11.01963, 13.01963, 15.01963, 17.01963,
1034
                                19.01963, 21.01963, 23.01963])])
1035
    def test_mlon2mlt_array(self, mlon, test_mlt):
1036
        """Test mlon2mlt with array inputs.
1037
1038
        Parameters
1039
        ----------
1040
        mlon : array-like
1041
            Input longitudes in degrees E
1042
        test_mlt : float
1043
            Output MLT in hours
1044
1045
        """
1046
        mlt = self.apex_out.mlon2mlt(mlon, self.in_time)
1047
1048
        assert mlt.shape == np.asarray(test_mlt).shape
1049
        np.testing.assert_allclose(mlt, test_mlt, rtol=1e-4)
1050
        return
1051
1052
    @pytest.mark.parametrize("mlt,test_mlon",
1053
                             [([0, 12], [14.705551, 194.705551]),
1054
                              (np.array([0, 12]), [14.705551, 194.705551]),
1055
                              (np.array([[0], [12]]),
1056
                               np.array([[14.705551], [194.705551]])),
1057
                              ([[0, 12], [0, 12]], [[14.705551, 194.705551],
1058
                                                    [14.705551, 194.705551]]),
1059
                              (range(0, 25, 2),
1060
                               [14.705551, 44.705551, 74.705551, 104.705551,
1061
                                134.705551, 164.705551, 194.705551, 224.705551,
1062
                                254.705551, 284.705551, 314.705551, 344.705551,
1063
                                14.705551])])
1064
    def test_mlt2mlon_array(self, mlt, test_mlon):
1065
        """Test mlt2mlon with array inputs.
1066
1067
        Parameters
1068
        ----------
1069
        mlt : array-like
1070
            Input MLT in hours
1071
        test_mlon : float
1072
            Output longitude in degrees E
1073
1074
        """
1075
        mlon = self.apex_out.mlt2mlon(mlt, self.in_time)
1076
1077
        assert mlon.shape == np.asarray(test_mlon).shape
1078
        np.testing.assert_allclose(mlon, test_mlon, rtol=1e-4)
1079
        return
1080
1081
    @pytest.mark.parametrize("method_name", ["mlon2mlt", "mlt2mlon"])
1082
    def test_mlon2mlt_diffdates(self, method_name):
1083
        """Test that MLT varies with universal time.
1084
1085
        Parameters
1086
        ----------
1087
        method_name : str
1088
            Name of Apex class method to be tested
1089
1090
        """
1091
        apex_method = getattr(self.apex_out, method_name)
1092
        mlt1 = apex_method(0, self.in_time)
1093
        mlt2 = apex_method(0, self.in_time + dt.timedelta(hours=1))
1094
1095
        assert mlt1 != mlt2
1096
        return
1097
1098
    @pytest.mark.parametrize("mlt_offset", [1.0, 10.0])
1099
    def test_mlon2mlt_offset(self, mlt_offset):
1100
        """Test the time wrapping logic for the MLT.
1101
1102
        Parameters
1103
        ----------
1104
        mlt_offset : float
1105
            MLT offset in hours
1106
1107
        """
1108
        mlt1 = self.apex_out.mlon2mlt(0.0, self.in_time)
1109
        mlt2 = self.apex_out.mlon2mlt(-15.0 * mlt_offset,
1110
                                      self.in_time) + mlt_offset
1111
1112
        np.testing.assert_allclose(mlt1, mlt2)
1113
        return
1114
1115
    @pytest.mark.parametrize("mlon_offset", [15.0, 150.0])
1116
    def test_mlt2mlon_offset(self, mlon_offset):
1117
        """Test the time wrapping logic for the magnetic longitude.
1118
1119
        Parameters
1120
        ----------
1121
        mlt_offset : float
1122
            MLT offset in hours
1123
1124
        """
1125
        mlon1 = self.apex_out.mlt2mlon(0, self.in_time)
1126
        mlon2 = self.apex_out.mlt2mlon(mlon_offset / 15.0,
1127
                                       self.in_time) - mlon_offset
1128
1129
        np.testing.assert_allclose(mlon1, mlon2)
1130
        return
1131
1132
    @pytest.mark.parametrize("order", [["mlt", "mlon"], ["mlon", "mlt"]])
1133
    @pytest.mark.parametrize("start_val", [0, 6, 12, 18, 22])
1134
    def test_convert_and_return(self, order, start_val):
1135
        """Test the conversion to magnetic longitude or MLT and back again.
1136
1137
        Parameters
1138
        ----------
1139
        order : list
1140
            List of strings specifying the order to run functions
1141
        start_val : int or float
1142
            Input value
1143
1144
        """
1145
        first_method = getattr(self.apex_out, "2".join(order))
1146
        second_method = getattr(self.apex_out, "2".join([order[1], order[0]]))
1147
1148
        middle_val = first_method(start_val, self.in_time)
1149
        end_val = second_method(middle_val, self.in_time)
1150
1151
        np.testing.assert_allclose(start_val, end_val)
1152
        return
1153
1154
1155
class TestApexMapMethods(object):
1156
    """Test the Apex height mapping methods."""
1157
    def setup_method(self):
1158
        """Initialize all tests."""
1159
        self.apex_out = apexpy.Apex(date=2000, refh=300)
1160
1161
    def teardown_method(self):
1162
        """Clean up after each test."""
1163
        del self.apex_out
1164
1165
    @pytest.mark.parametrize("in_args,test_mapped",
1166
                             [([60, 15, 100, 10000],
1167
                               [31.841466903686523, 17.916635513305664,
1168
                                1.7075473124350538e-6]),
1169
                              ([30, 170, 100, 500, False, 1e-2],
1170
                               [25.727270126342773, 169.60546875,
1171
                                0.00017573432705830783]),
1172
                              ([60, 15, 100, 10000, True],
1173
                               [-25.424888610839844, 27.310426712036133,
1174
                                1.2074182222931995e-6]),
1175
                              ([30, 170, 100, 500, True, 1e-2],
1176
                               [-13.76642894744873, 164.24259948730469,
1177
                                0.00056820799363777041])])
1178
    def test_map_to_height(self, in_args, test_mapped):
1179
        """Test the map_to_height function.
1180
1181
        Parameters
1182
        ----------
1183
        in_args : list
1184
            List of input arguments
1185
        test_mapped : list
1186
            List of expected outputs
1187
1188
        """
1189
        mapped = self.apex_out.map_to_height(*in_args)
1190
        np.testing.assert_allclose(mapped, test_mapped, rtol=1e-5, atol=1e-5)
1191
        return
1192
1193
    def test_map_to_height_same_height(self):
1194
        """Test the map_to_height function when mapping to same height."""
1195
        mapped = self.apex_out.map_to_height(60, 15, 100, 100, conjugate=False,
1196
                                             precision=1e-10)
1197
        np.testing.assert_allclose(mapped, (60.0, 15.000003814697266, 0.0),
1198
                                   rtol=1e-5, atol=1e-5)
1199
        return
1200
1201
    @pytest.mark.parametrize('arr_shape', [(2,), (2, 2), (1, 4)])
1202
    @pytest.mark.parametrize('ivec', range(0, 4))
1203
    def test_map_to_height_array_location(self, arr_shape, ivec):
1204
        """Test map_to_height with array input.
1205
1206
        Parameters
1207
        ----------
1208
        arr_shape : tuple
1209
            Expected array shape
1210
        ivec : int
1211
            Input argument index for vectorized input
1212
1213
        """
1214
        # Set the base input and output values
1215
        in_args = [60, 15, 100, 100]
1216
        test_mapped = [60, 15.00000381, 0.0]
1217
1218
        # Update inputs for one vectorized value
1219
        in_args[ivec] = np.full(shape=arr_shape, fill_value=in_args[ivec])
1220
1221
        # Calculate and test function
1222
        mapped = self.apex_out.map_to_height(*in_args)
1223
        for i, test_val in enumerate(test_mapped):
1224
            assert mapped[i].shape == arr_shape
1225
            np.testing.assert_allclose(mapped[i], test_val, rtol=1e-5,
1226
                                       atol=1e-5)
1227
        return
1228
1229
    @pytest.mark.parametrize("method_name,in_args",
1230
                             [("map_to_height", [0, 15, 100, 10000]),
1231
                              ("map_E_to_height",
1232
                               [0, 15, 100, 10000, [1, 2, 3]]),
1233
                              ("map_V_to_height",
1234
                               [0, 15, 100, 10000, [1, 2, 3]])])
1235
    def test_mapping_height_raises_ApexHeightError(self, method_name, in_args):
1236
        """Test map_to_height raises ApexHeightError.
1237
1238
        Parameters
1239
        ----------
1240
        method_name : str
1241
            Name of the Apex class method to test
1242
        in_args : list
1243
            List of input arguments
1244
1245
        """
1246
        apex_method = getattr(self.apex_out, method_name)
1247
1248
        with pytest.raises(apexpy.ApexHeightError) as aerr:
1249
            apex_method(*in_args)
1250
1251
        assert aerr.match("is > apex height")
1252
        return
1253
1254
    @pytest.mark.parametrize("method_name",
1255
                             ["map_E_to_height", "map_V_to_height"])
1256
    @pytest.mark.parametrize("ev_input", [([1, 2, 3, 4, 5]),
1257
                                          ([[1, 2], [3, 4], [5, 6], [7, 8]])])
1258
    def test_mapping_EV_bad_shape(self, method_name, ev_input):
1259
        """Test height mapping of E/V with baddly shaped input raises Error.
1260
1261
        Parameters
1262
        ----------
1263
        method_name : str
1264
            Name of the Apex class method to test
1265
        ev_input : list
1266
            E/V input arguments
1267
1268
        """
1269
        apex_method = getattr(self.apex_out, method_name)
1270
        in_args = [60, 15, 100, 500, ev_input]
1271
        with pytest.raises(ValueError) as verr:
1272
            apex_method(*in_args)
1273
1274
        assert str(verr.value).find("must be (3, N) or (3,) ndarray") >= 0
1275
        return
1276
1277
    def test_mapping_EV_bad_flag(self):
1278
        """Test _map_EV_to_height raises error for bad data type flag."""
1279
        with pytest.raises(ValueError) as verr:
1280
            self.apex_out._map_EV_to_height(60, 15, 100, 500, [1, 2, 3], "P")
1281
1282
        assert str(verr.value).find("unknown electric field/drift flag") >= 0
1283
        return
1284
1285
    @pytest.mark.parametrize("in_args,test_mapped",
1286
                             [([60, 15, 100, 500, [1, 2, 3]],
1287
                               [0.71152183, 2.35624876, 0.57260784]),
1288
                              ([60, 15, 100, 500, [2, 3, 4]],
1289
                               [1.56028502, 3.43916636, 0.78235384]),
1290
                              ([60, 15, 100, 1000, [1, 2, 3]],
1291
                               [0.67796492, 2.08982134, 0.55860785]),
1292
                              ([60, 15, 200, 500, [1, 2, 3]],
1293
                               [0.72377397, 2.42737471, 0.59083726]),
1294
                              ([60, 30, 100, 500, [1, 2, 3]],
1295
                               [0.68626344, 2.37530133, 0.60060124]),
1296
                              ([70, 15, 100, 500, [1, 2, 3]],
1297
                               [0.72760378, 2.18082305, 0.29141979])])
1298
    def test_map_E_to_height_scalar_location(self, in_args, test_mapped):
1299
        """Test mapping of E-field to a specified height.
1300
1301
        Parameters
1302
        ----------
1303
        in_args : list
1304
            List of input arguments
1305
        test_mapped : list
1306
            List of expected outputs
1307
1308
        """
1309
        mapped = self.apex_out.map_E_to_height(*in_args)
1310
        np.testing.assert_allclose(mapped, test_mapped, rtol=1e-5)
1311
        return
1312
1313
    @pytest.mark.parametrize('ev_flag, test_mapped',
1314
                             [('E', [0.71152183, 2.35624876, 0.57260784]),
1315
                              ('V', [0.81971957, 2.84512495, 0.69545001])])
1316
    @pytest.mark.parametrize('arr_shape', [(2,), (5,)])
1317
    @pytest.mark.parametrize('ivec', range(0, 5))
1318
    def test_map_EV_to_height_array_location(self, ev_flag, test_mapped,
1319
                                             arr_shape, ivec):
1320
        """Test mapping of E-field/drift to a specified height with arrays.
1321
1322
        Parameters
1323
        ----------
1324
        ev_flag : str
1325
            Character flag specifying whether to run 'E' or 'V' methods
1326
        test_mapped : list
1327
            List of expected outputs
1328
        arr_shape : tuple
1329
            Shape of the expected output
1330
        ivec : int
1331
            Index of the expected output
1332
1333
        """
1334
        # Set the base input and output values
1335
        eshape = list(arr_shape)
1336
        eshape.insert(0, 3)
1337
        edata = np.array([[1, 2, 3]] * np.prod(arr_shape)).transpose()
1338
        in_args = [60, 15, 100, 500, edata.reshape(tuple(eshape))]
1339
1340
        # Update inputs for one vectorized value if this is a location input
1341
        if ivec < 4:
1342
            in_args[ivec] = np.full(shape=arr_shape, fill_value=in_args[ivec])
1343
1344
        # Get the mapped output
1345
        apex_method = getattr(self.apex_out,
1346
                              "map_{:s}_to_height".format(ev_flag))
1347
        mapped = apex_method(*in_args)
1348
1349
        # Test the results
1350
        for i, test_val in enumerate(test_mapped):
1351
            assert mapped[i].shape == arr_shape
1352
            np.testing.assert_allclose(mapped[i], test_val, rtol=1e-5)
1353
        return
1354
1355
    @pytest.mark.parametrize("in_args,test_mapped",
1356
                             [([60, 15, 100, 500, [1, 2, 3]],
1357
                               [0.81971957, 2.84512495, 0.69545001]),
1358
                              ([60, 15, 100, 500, [2, 3, 4]],
1359
                               [1.83027746, 4.14346436, 0.94764179]),
1360
                              ([60, 15, 100, 1000, [1, 2, 3]],
1361
                               [0.92457698, 3.14997661, 0.85135187]),
1362
                              ([60, 15, 200, 500, [1, 2, 3]],
1363
                               [0.80388262, 2.79321504, 0.68285158]),
1364
                              ([60, 30, 100, 500, [1, 2, 3]],
1365
                               [0.76141245, 2.87884673, 0.73655941]),
1366
                              ([70, 15, 100, 500, [1, 2, 3]],
1367
                               [0.84681866, 2.5925821, 0.34792655])])
1368
    def test_map_V_to_height_scalar_location(self, in_args, test_mapped):
1369
        """Test mapping of velocity to a specified height.
1370
1371
        Parameters
1372
        ----------
1373
        in_args : list
1374
            List of input arguments
1375
        test_mapped : list
1376
            List of expected outputs
1377
1378
        """
1379
        mapped = self.apex_out.map_V_to_height(*in_args)
1380
        np.testing.assert_allclose(mapped, test_mapped, rtol=1e-5)
1381
        return
1382
1383
1384
class TestApexBasevectorMethods(object):
1385
    """Test the Apex height base vector methods."""
1386
    def setup_method(self):
1387
        """Initialize all tests."""
1388
        self.apex_out = apexpy.Apex(date=2000, refh=300)
1389
        self.lat = 60
1390
        self.lon = 15
1391
        self.height = 100
1392
        self.test_basevec = None
1393
1394
    def teardown_method(self):
1395
        """Clean up after each test."""
1396
        del self.apex_out, self.test_basevec, self.lat, self.lon, self.height
1397
1398
    def get_comparison_results(self, bv_coord, coords, precision):
1399
        """Get the base vector results using the hidden function for comparison.
1400
1401
        Parameters
1402
        ----------
1403
        bv_coord : str
1404
            Basevector coordinate scheme, expects on of 'apex', 'qd',
1405
            or 'bvectors_apex'
1406
        coords : str
1407
            Expects one of 'geo', 'apex', or 'qd'
1408
        precision : float
1409
            Float specifiying precision
1410
1411
        """
1412
        if coords == "geo":
1413
            glat = self.lat
1414
            glon = self.lon
1415
        else:
1416
            apex_method = getattr(self.apex_out, "{:s}2geo".format(coords))
1417
            glat, glon, _ = apex_method(self.lat, self.lon, self.height,
1418
                                        precision=precision)
1419
1420
        if bv_coord == 'qd':
1421
            self.test_basevec = self.apex_out._basevec(glat, glon, self.height)
1422
        elif bv_coord == 'apex':
1423
            (_, _, _, _, f1, f2, _, d1, d2, d3, _, e1, e2,
1424
             e3) = self.apex_out._geo2apexall(glat, glon, 100)
1425
            self.test_basevec = (f1, f2, d1, d2, d3, e1, e2, e3)
1426
        else:
1427
            # These are set results that need to be updated with IGRF
1428
            if coords == "geo":
1429
                self.test_basevec = (
1430
                    np.array([4.42368795e-05, 4.42368795e-05]),
1431
                    np.array([[0.01047826, 0.01047826],
1432
                              [0.33089194, 0.33089194],
1433
                              [-1.04941, -1.04941]]),
1434
                    np.array([5.3564698e-05, 5.3564698e-05]),
1435
                    np.array([[0.00865356, 0.00865356],
1436
                              [0.27327004, 0.27327004],
1437
                              [-0.8666646, -0.8666646]]))
1438
            elif coords == "apex":
1439
                self.test_basevec = (
1440
                    np.array([4.48672735e-05, 4.48672735e-05]),
1441
                    np.array([[-0.12510721, -0.12510721],
1442
                              [0.28945938, 0.28945938],
1443
                              [-1.1505738, -1.1505738]]),
1444
                    np.array([6.38577444e-05, 6.38577444e-05]),
1445
                    np.array([[-0.08790194, -0.08790194],
1446
                              [0.2033779, 0.2033779],
1447
                              [-0.808408, -0.808408]]))
1448
            else:
1449
                self.test_basevec = (
1450
                    np.array([4.46348578e-05, 4.46348578e-05]),
1451
                    np.array([[-0.12642345, -0.12642345],
1452
                              [0.29695055, 0.29695055],
1453
                              [-1.1517885, -1.1517885]]),
1454
                    np.array([6.38626285e-05, 6.38626285e-05]),
1455
                    np.array([[-0.08835986, -0.08835986],
1456
                              [0.20754464, 0.20754464],
1457
                              [-0.8050078, -0.8050078]]))
1458
1459
        return
1460
1461
    @pytest.mark.parametrize("bv_coord", ["qd", "apex"])
1462
    @pytest.mark.parametrize("coords,precision",
1463
                             [("geo", 1e-10), ("apex", 1.0e-2), ("qd", 1.0e-2)])
1464
    def test_basevectors_scalar(self, bv_coord, coords, precision):
1465
        """Test the base vector calculations with scalars.
1466
1467
        Parameters
1468
        ----------
1469
        bv_coord : str
1470
            Name of the input coordinate system
1471
        coords : str
1472
            Name of the output coordinate system
1473
        precision : float
1474
            Level of run precision requested
1475
1476
        """
1477
        # Get the base vectors
1478
        base_method = getattr(self.apex_out,
1479
                              "basevectors_{:s}".format(bv_coord))
1480
        basevec = base_method(self.lat, self.lon, self.height, coords=coords,
1481
                              precision=precision)
1482
        self.get_comparison_results(bv_coord, coords, precision)
1483
        if bv_coord == "apex":
1484
            basevec = list(basevec)
1485
            for i in range(4):
1486
                # Not able to compare indices 2, 3, 4, and 5
1487
                basevec.pop(2)
1488
1489
        # Test the results
1490
        for i, vec in enumerate(basevec):
1491
            np.testing.assert_allclose(vec, self.test_basevec[i])
1492
        return
1493
1494
    @pytest.mark.parametrize("bv_coord", ["qd", "apex"])
1495
    def test_basevectors_scalar_shape(self, bv_coord):
1496
        """Test the shape of the scalar output.
1497
1498
        Parameters
1499
        ----------
1500
        bv_coord : str
1501
            Name of the input coordinate system
1502
1503
        """
1504
        base_method = getattr(self.apex_out,
1505
                              "basevectors_{:s}".format(bv_coord))
1506
        basevec = base_method(self.lat, self.lon, self.height)
1507
1508
        for i, vec in enumerate(basevec):
1509
            if i < 2:
1510
                assert vec.shape == (2,)
1511
            else:
1512
                assert vec.shape == (3,)
1513
        return
1514
1515
    @pytest.mark.parametrize('arr_shape', [(2,), (5,)])
1516
    @pytest.mark.parametrize("bv_coord", ["qd", "apex"])
1517
    @pytest.mark.parametrize("ivec", range(3))
1518
    def test_basevectors_array(self, arr_shape, bv_coord, ivec):
1519
        """Test the output shape for array inputs.
1520
1521
        Parameters
1522
        ----------
1523
        arr_shape : tuple
1524
            Expected output shape
1525
        bv_coord : str
1526
            Name of the input coordinate system
1527
        ivec : int
1528
            Index of the evaluated output value
1529
1530
        """
1531
        # Define the input arguments
1532
        in_args = [self.lat, self.lon, self.height]
1533
        in_args[ivec] = np.full(shape=arr_shape, fill_value=in_args[ivec])
1534
1535
        # Get the basevectors
1536
        base_method = getattr(self.apex_out,
1537
                              "basevectors_{:s}".format(bv_coord))
1538
        basevec = base_method(*in_args, coords='geo', precision=1e-10)
1539
        self.get_comparison_results(bv_coord, "geo", 1e-10)
1540
        if bv_coord == "apex":
1541
            basevec = list(basevec)
1542
            for i in range(4):
1543
                # Not able to compare indices 2, 3, 4, and 5
1544
                basevec.pop(2)
1545
1546
        # Evaluate the shape and the values
1547
        for i, vec in enumerate(basevec):
1548
            test_shape = list(arr_shape)
1549
            test_shape.insert(0, 2 if i < 2 else 3)
1550
            assert vec.shape == tuple(test_shape)
1551
            assert np.all(self.test_basevec[i][0] == vec[0])
1552
            assert np.all(self.test_basevec[i][1] == vec[1])
1553
        return
1554
1555
    @pytest.mark.parametrize("coords", ["geo", "apex", "qd"])
1556
    def test_bvectors_apex(self, coords):
1557
        """Test the bvectors_apex method.
1558
1559
        Parameters
1560
        ----------
1561
        coords : str
1562
            Name of the coordiante system
1563
1564
        """
1565
        in_args = [[self.lat, self.lat], [self.lon, self.lon],
1566
                   [self.height, self.height]]
1567
        self.get_comparison_results("bvectors_apex", coords, 1e-10)
1568
1569
        basevec = self.apex_out.bvectors_apex(*in_args, coords=coords,
1570
                                              precision=1e-10)
1571
        for i, vec in enumerate(basevec):
1572
            np.testing.assert_array_almost_equal(vec, self.test_basevec[i],
1573
                                                 decimal=5)
1574
        return
1575
1576
    def test_basevectors_apex_extra_values(self):
1577
        """Test specific values in the apex base vector output."""
1578
        # Set the testing arrays
1579
        self.test_basevec = [np.array([0.092637, -0.245951, 0.938848]),
1580
                             np.array([0.939012, 0.073416, -0.07342]),
1581
                             np.array([0.055389, 1.004155, 0.257594]),
1582
                             np.array([0, 0, 1.065135])]
1583
1584
        # Get the desired output
1585
        basevec = self.apex_out.basevectors_apex(0, 15, 100, coords='geo')
1586
1587
        # Test the values not covered by `test_basevectors_scalar`
1588
        for itest, ibase in enumerate(np.arange(2, 6, 1)):
1589
            np.testing.assert_allclose(basevec[ibase],
1590
                                       self.test_basevec[itest], rtol=1e-4)
1591
        return
1592
1593
    @pytest.mark.parametrize("lat", range(0, 90, 10))
1594
    @pytest.mark.parametrize("lon", range(0, 360, 15))
1595
    def test_basevectors_apex_delta(self, lat, lon):
1596
        """Test that vectors are calculated correctly.
1597
1598
        Parameters
1599
        ----------
1600
        lat : int or float
1601
            Latitude in degrees N
1602
        lon : int or float
1603
            Longitude in degrees E
1604
1605
        """
1606
        # Get the apex base vectors and sort them for easy testing
1607
        (f1, f2, f3, g1, g2, g3, d1, d2, d3, e1, e2,
1608
         e3) = self.apex_out.basevectors_apex(lat, lon, 500)
1609
        fvec = [np.append(f1, 0), np.append(f2, 0), f3]
1610
        gvec = [g1, g2, g3]
1611
        dvec = [d1, d2, d3]
1612
        evec = [e1, e2, e3]
1613
1614
        for idelta, jdelta in [(i, j) for i in range(3) for j in range(3)]:
1615
            delta = 1 if idelta == jdelta else 0
1616
            np.testing.assert_allclose(np.sum(fvec[idelta] * gvec[jdelta]),
1617
                                       delta, rtol=0, atol=1e-5)
1618
            np.testing.assert_allclose(np.sum(dvec[idelta] * evec[jdelta]),
1619
                                       delta, rtol=0, atol=1e-5)
1620
        return
1621
1622
    def test_basevectors_apex_invalid_scalar(self):
1623
        """Test warning and fill values for base vectors with bad inputs."""
1624
        self.apex_out = apexpy.Apex(date=2000, refh=10000)
1625
        invalid = np.full(shape=(3,), fill_value=np.nan)
1626
1627
        # Get the output and the warnings
1628
        with warnings.catch_warnings(record=True) as warn_rec:
1629
            basevec = self.apex_out.basevectors_apex(0, 0, 0)
1630
1631
        for i, bvec in enumerate(basevec):
1632
            if i < 2:
1633
                assert not np.allclose(bvec, invalid[:2])
1634
            else:
1635
                np.testing.assert_allclose(bvec, invalid)
1636
1637
        assert issubclass(warn_rec[-1].category, UserWarning)
1638
        assert 'set to NaN where' in str(warn_rec[-1].message)
1639
        return
1640
1641
1642
class TestApexGetMethods(object):
1643
    """Test the Apex `get` methods."""
1644
    def setup_method(self):
1645
        """Initialize all tests."""
1646
        self.apex_out = apexpy.Apex(date=2000, refh=300)
1647
1648
    def teardown_method(self):
1649
        """Clean up after each test."""
1650
        del self.apex_out
1651
1652
    @pytest.mark.parametrize("alat, aheight",
1653
                             [(10, 507.409702543805),
1654
                              (60, 20313.026999999987),
1655
                              ([10, 60],
1656
                               [507.409702543805, 20313.026999999987]),
1657
                              ([[10], [60]],
1658
                               [[507.409702543805], [20313.026999999987]])])
1659
    def test_get_apex(self, alat, aheight):
1660
        """Test the apex height retrieval results.
1661
1662
        Parameters
1663
        ----------
1664
        alat : int or float
1665
            Apex latitude in degrees N
1666
        aheight : int or float
1667
            Apex height in km
1668
1669
        """
1670
        alt = self.apex_out.get_apex(alat)
1671
        np.testing.assert_allclose(alt, aheight)
1672
        return
1673
1674
    @pytest.mark.parametrize("glat,glon,height,test_bmag",
1675
                             [([80], [100], [300], 5.100682377815247e-05),
1676
                              ([80, 80], [100], [300],
1677
                               [5.100682377815247e-05, 5.100682377815247e-05]),
1678
                              ([[80], [80]], [100], [300],
1679
                               [[5.100682377815247e-05],
1680
                                [5.100682377815247e-05]]),
1681
                              (range(50, 90, 8), range(0, 360, 80), [300] * 5,
1682
                               np.array([4.18657154e-05, 5.11118114e-05,
1683
                                         4.91969854e-05, 5.10519207e-05,
1684
                                         4.90054816e-05])),
1685
                              (90.0, 0, 1000, 3.7834718823432923e-05)])
1686
    def test_get_babs(self, glat, glon, height, test_bmag):
1687
        """Test the method to get the magnitude of the magnetic field.
1688
1689
        Parameters
1690
        ----------
1691
        glat : list
1692
            List of latitudes in degrees N
1693
        glon : list
1694
            List of longitudes in degrees E
1695
        height : list
1696
            List of heights in km
1697
        test_bmag : float
1698
            Expected B field magnitude
1699
1700
        """
1701
        bmag = self.apex_out.get_babs(glat, glon, height)
1702
        np.testing.assert_allclose(bmag, test_bmag, rtol=0, atol=1e-5)
1703
        return
1704
1705
    @pytest.mark.parametrize("bad_lat", [(91), (-91)])
1706
    def test_get_apex_with_invalid_lat(self, bad_lat):
1707
        """Test get methods raise ValueError for invalid latitudes.
1708
1709
        Parameters
1710
        ----------
1711
        bad_lat : int or float
1712
            Bad input latitude in degrees N
1713
1714
        """
1715
1716
        with pytest.raises(ValueError) as verr:
1717
            self.apex_out.get_apex(bad_lat)
1718
1719
        assert str(verr.value).find("must be in [-90, 90]") > 0
1720
        return
1721
1722
    @pytest.mark.parametrize("bad_lat", [(91), (-91)])
1723
    def test_get_babs_with_invalid_lat(self, bad_lat):
1724
        """Test get methods raise ValueError for invalid latitudes.
1725
1726
        Parameters
1727
        ----------
1728
        bad_lat : int or float
1729
            Bad input latitude in degrees N
1730
1731
        """
1732
1733
        with pytest.raises(ValueError) as verr:
1734
            self.apex_out.get_babs(bad_lat, 15, 100)
1735
1736
        assert str(verr.value).find("must be in [-90, 90]") > 0
1737
        return
1738
1739
    @pytest.mark.parametrize("bound_lat", [(90), (-90)])
1740
    def test_get_at_lat_boundary(self, bound_lat):
1741
        """Test get methods at the latitude boundary, with allowed excess.
1742
1743
        Parameters
1744
        ----------
1745
        bound_lat : int or float
1746
            Boundary input latitude in degrees N
1747
1748
        """
1749
        # Get a latitude just beyond the limit
1750
        excess_lat = np.sign(bound_lat) * (abs(bound_lat) + 1.0e-5)
1751
1752
        # Get the two outputs, slight tolerance outside of boundary allowed
1753
        bound_out = self.apex_out.get_apex(bound_lat)
1754
        excess_out = self.apex_out.get_apex(excess_lat)
1755
1756
        # Test the outputs
1757
        np.testing.assert_allclose(excess_out, bound_out, rtol=0, atol=1e-8)
1758
        return
1759
1760
    @pytest.mark.parametrize("apex_height", [-100, 0, 300, 10000])
1761
    def test_get_height_at_equator(self, apex_height):
1762
        """Test that `get_height` returns apex height at equator.
1763
1764
        Parameters
1765
        ----------
1766
        apex_height : float
1767
            Apex height
1768
1769
        """
1770
1771
        assert apex_height == self.apex_out.get_height(0.0, apex_height)
1772
        return
1773
1774
    @pytest.mark.parametrize("lat, height", [
1775
        (-90, -6371.009), (-80, -6088.438503309167), (-70, -5274.8091854339655),
1776
        (-60, -4028.256749999999), (-50, -2499.1338178752017),
1777
        (-40, -871.8751821247979), (-30, 657.2477500000014),
1778
        (-20, 1903.8001854339655), (-10, 2717.4295033091657), (0, 3000.0),
1779
        (10, 2717.4295033091657), (20, 1903.8001854339655),
1780
        (30, 657.2477500000014), (40, -871.8751821247979),
1781
        (50, -2499.1338178752017), (60, -4028.256749999999),
1782
        (70, -5274.8091854339655), (80, -6088.438503309167)])
1783
    def test_get_height_along_fieldline(self, lat, height):
1784
        """Test that `get_height` returns expected height of field line.
1785
1786
        Parameters
1787
        ----------
1788
        lat : float
1789
            Input latitude
1790
        height : float
1791
            Output field-line height for line with apex of 3000 km
1792
1793
        """
1794
1795
        fheight = self.apex_out.get_height(lat, 3000.0)
1796
        assert abs(height - fheight) < 1.0e-7, \
1797
            "bad height calculation: {:.7f} != {:.7f}".format(height, fheight)
1798
        return
1799
1800
1801
class TestApexMethodExtrapolateIGRF(object):
1802
    """Test the Apex methods on a year when IGRF must be extrapolated.
1803
1804
    Notes
1805
    -----
1806
    Extrapolation should be done using a year within 5 years of the latest IGRF
1807
    model epoch.
1808
1809
    """
1810
1811
    def setup_method(self):
1812
        """Initialize all tests."""
1813
        self.apex_out = apexpy.Apex(date=2030, refh=300)
1814
        self.in_lat = 60
1815
        self.in_lon = 15
1816
        self.in_alt = 100
1817
        self.in_time = dt.datetime(2029, 2, 3, 4, 5, 6)
1818
        return
1819
1820
    def teardown_method(self):
1821
        """Clean up after each test."""
1822
        del self.apex_out, self.in_lat, self.in_lon, self.in_alt
1823
        return
1824
1825 View Code Duplication
    @pytest.mark.parametrize("method_name, out_comp",
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1826
                             [("geo2apex",
1827
                               (56.30344009399414, 91.99834442138672)),
1828
                              ("apex2geo",
1829
                               (54.22148513793945, -67.09271240234375,
1830
                                4.268868451617891e-06)),
1831
                              ("geo2qd",
1832
                               (56.87860870361328, 91.99834442138672)),
1833
                              ("apex2qd", (60.498401178276744, 15.0)),
1834
                              ("qd2apex", (59.49138097045895, 15.0))])
1835
    def test_method_scalar_input(self, method_name, out_comp):
1836
        """Test the user method against set values with scalars.
1837
1838
        Parameters
1839
        ----------
1840
        method_name : str
1841
            Apex class method to be tested
1842
        out_comp : tuple of floats
1843
            Expected output values
1844
1845
        """
1846
        # Get the desired methods
1847
        user_method = getattr(self.apex_out, method_name)
1848
1849
        # Get the user output
1850
        user_out = user_method(self.in_lat, self.in_lon, self.in_alt)
1851
1852
        # Evaluate the user output
1853
        np.testing.assert_allclose(user_out, out_comp, rtol=1e-5, atol=1e-5)
1854
1855
        for out_val in user_out:
1856
            assert np.asarray(out_val).shape == (), "output is not a scalar"
1857
        return
1858
1859
    def test_convert_to_mlt(self):
1860
        """Test conversion from mlon to mlt with scalars."""
1861
1862
        # Get user output
1863
        user_out = self.apex_out.mlon2mlt(self.in_lon, self.in_time)
1864
1865
        # Set comparison values
1866
        out_comp = 23.933631388346353
1867
1868
        # Evaluate user output
1869
        np.testing.assert_allclose(user_out, out_comp, rtol=1e-5, atol=1e-5)
1870
        return
1871