Passed
Pull Request — main (#103)
by Angeline
01:37
created

TestHelpers.test_subsol_datetime64_array()   A

Complexity

Conditions 3

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 33
rs 9.45
c 0
b 0
f 0
cc 3
nop 2
1
# -*- coding: utf-8 -*-
2
"""Test the apexpy.helper submodule
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 pytest
18
19
from apexpy import helpers
20
21
22
def datetime64_to_datetime(dt64):
23
    """Convert numpy datetime64 object to a datetime datetime object.
24
25
    Parameters
26
    ----------
27
    dt64 : np.datetime64
28
        Numpy datetime64 object
29
30
    Returns
31
    -------
32
    dt.datetime
33
        Equivalent datetime object with a resolution of days
34
35
    Notes
36
    -----
37
    Works outside 32 bit int second range of 1970
38
39
    """
40
    year_floor = dt64.astype('datetime64[Y]')
41
    month_floor = dt64.astype('datetime64[M]')
42
    day_floor = dt64.astype('datetime64[D]')
43
    year = year_floor.astype(int) + 1970
44
    month = (month_floor
45
             - year_floor).astype('timedelta64[M]').astype(int) + 1
46
    day = (day_floor - month_floor).astype('timedelta64[D]').astype(int) + 1
47
    return dt.datetime(year, month, day)
48
49
50
class TestHelpers(object):
51
    def setup_method(self):
52
        """Set up a clean test environment."""
53
        self.in_shape = None
54
        self.calc_val = None
55
        self.test_val = None
56
57
    def teardown_method(self):
58
        """Clean up the test environment."""
59
        del self.in_shape, self.calc_val, self.test_val
60
61
    def eval_output(self, rtol=1e-7, atol=0.0):
62
        """Evaluate the values and shape of the calculated and expected output.
63
        """
64
        np.testing.assert_allclose(self.calc_val, self.test_val, rtol=rtol,
65
                                   atol=atol)
66
        assert np.asarray(self.calc_val).shape == self.in_shape
67
        return
68
69
    @pytest.mark.parametrize('lat', [90, 0, -90, np.nan])
70
    def test_checklat_scalar(self, lat):
71
        """Test good latitude check with scalars."""
72
        self.calc_val = helpers.checklat(lat)
73
74
        if np.isnan(lat):
75
            assert np.isnan(self.calc_val)
76
        else:
77
            assert self.calc_val == lat
78
        return
79
80
    @pytest.mark.parametrize('lat', [(90 + 1e-5), (-90 - 1e-5)])
81
    def test_checklat_scalar_clip(self, lat):
82
        """Test good latitude check with scalars just beyond the lat limits."""
83
        self.calc_val = helpers.checklat(lat)
84
        self.test_val = np.sign(lat) * np.floor(abs(lat))
85
        assert self.calc_val == self.test_val
86
        return
87
88
    @pytest.mark.parametrize('in_args,msg',
89
                             [([90 + 1e-4], "lat must be in"),
90
                              ([-90 - 1e-4, 'glat'], "glat must be in"),
91
                              ([[-90 - 1e-5, -90, 0, 90, 90 + 1e-4], 'glat'],
92
                               "glat must be in"),
93
                              ([[-90 - 1e-4, -90, np.nan, np.nan, 90 + 1e-5]],
94
                               'lat must be in')])
95
    def test_checklat_error(self, in_args, msg):
96
        """Test bad latitude raises ValueError with appropriate message."""
97
        with pytest.raises(ValueError) as verr:
98
            helpers.checklat(*in_args)
99
100
        assert str(verr.value).startswith(msg)
101
        return
102
103
    @pytest.mark.parametrize('lat,test_lat',
104
                             [(np.linspace(-90 - 1e-5, 90 + 1e-5, 3),
105
                               [-90, 0, 90]),
106
                              (np.linspace(-90, 90, 3), [-90, 0, 90]),
107
                              ([-90 - 1e-5, 0, 90, np.nan],
108
                               [-90, 0, 90, np.nan]),
109
                              ([[-90, 0], [0, 90]], [[-90, 0], [0, 90]]),
110
                              ([[-90], [0], [90]], [[-90], [0], [90]])])
111
    def test_checklat_array(self, lat, test_lat):
112
        """Test good latitude with finite values."""
113
        self.calc_val = helpers.checklat(lat)
114
        self.in_shape = np.asarray(lat).shape
115
        self.test_val = test_lat
116
        self.eval_output(atol=1e-8)
117
        return
118
119
    @pytest.mark.parametrize('lat,test_sin', [
120
        (60, 0.96076892283052284), (10, 0.33257924500670238),
121
        ([60, 10], [0.96076892283052284, 0.33257924500670238]),
122
        ([[60, 10], [60, 10]], [[0.96076892283052284, 0.33257924500670238],
123
                                [0.96076892283052284, 0.33257924500670238]])])
124
    def test_getsinIm(self, lat, test_sin):
125
        """Test sin(Im) calculation for scalar and array inputs."""
126
        self.calc_val = helpers.getsinIm(lat)
127
        self.in_shape = np.asarray(lat).shape
128
        self.test_val = test_sin
129
        self.eval_output()
130
        return
131
132
    @pytest.mark.parametrize('lat,test_cos', [
133
        (60, 0.27735009811261463), (10, 0.94307531289434765),
134
        ([60, 10], [0.27735009811261463, 0.94307531289434765]),
135
        ([[60, 10], [60, 10]], [[0.27735009811261463, 0.94307531289434765],
136
                                [0.27735009811261463, 0.94307531289434765]])])
137
    def test_getcosIm(self, lat, test_cos):
138
        """Test cos(Im) calculation for scalar and array inputs."""
139
        self.calc_val = helpers.getcosIm(lat)
140
        self.in_shape = np.asarray(lat).shape
141
        self.test_val = test_cos
142
        self.eval_output()
143
        return
144
145
    @pytest.mark.parametrize('in_time,year', [
146
        (dt.datetime(2001, 1, 1), 2001), (dt.date(2001, 1, 1), 2001),
147
        (dt.datetime(2002, 1, 1), 2002),
148
        (dt.datetime(2005, 2, 3, 4, 5, 6), 2005.090877283105),
149
        (dt.datetime(2005, 12, 11, 10, 9, 8), 2005.943624682902)])
150
    def test_toYearFraction(self, in_time, year):
151
        """Test the datetime to fractional year calculation."""
152
        self.calc_val = helpers.toYearFraction(in_time)
153
        np.testing.assert_allclose(self.calc_val, year)
154
        return
155
156
    @pytest.mark.parametrize('gc_lat,gd_lat', [
157
        (0, 0), (90, 90), (30, 30.166923849507356), (60, 60.166364190170931),
158
        ([0, 90, 30], [0, 90, 30.166923849507356]),
159
        ([[0, 30], [90, 60]], [[0, 30.16692384950735],
160
                               [90, 60.166364190170931]])])
161
    def test_gc2gdlat(self, gc_lat, gd_lat):
162
        """Test geocentric to geodetic calculation."""
163
        self.calc_val = helpers.gc2gdlat(gc_lat)
164
        self.in_shape = np.asarray(gc_lat).shape
165
        self.test_val = gd_lat
166
        self.eval_output()
167
        return
168
169
    @pytest.mark.parametrize('in_time,test_loc', [
170
        (dt.datetime(2005, 2, 3, 4, 5, 6), (-16.505391672592904,
171
                                            122.17768157084515)),
172
        (dt.datetime(2010, 12, 11, 10, 9, 8), (-23.001554595838947,
173
                                               26.008999999955023)),
174
        (dt.datetime(2021, 11, 20, 12, 12, 12, 500000),
175
         (-19.79733856741465, -6.635177076865062)),
176
        (dt.datetime(1601, 1, 1, 0, 0, 0), (-23.06239721771427,
177
                                            -178.90131731228584)),
178
        (dt.datetime(2100, 12, 31, 23, 59, 59), (-23.021061422069053,
179
                                                 -179.23129780639425))])
180
    def test_subsol(self, in_time, test_loc):
181
        """Test the subsolar location calculation."""
182
        self.calc_val = helpers.subsol(in_time)
183
        np.testing.assert_allclose(self.calc_val, test_loc)
184
        return
185
186
    @pytest.mark.parametrize('in_time', [dt.datetime(1600, 12, 31, 23, 59, 59),
187
                                         dt.datetime(2101, 1, 1, 0, 0, 0)])
188
    def test_bad_subsol_date(self, in_time):
189
        """Test raises ValueError for bad time in subsolar calculation."""
190
        with pytest.raises(ValueError) as verr:
191
            helpers.subsol(in_time)
192
193
        assert str(verr.value).startswith('Year must be in')
194
        return
195
196
    @pytest.mark.parametrize('in_time', [None, 2015.0])
197
    def test_bad_subsol_input(self, in_time):
198
        """Test raises ValueError for bad input type in subsolar calculation."""
199
        with pytest.raises(ValueError) as verr:
200
            helpers.subsol(in_time)
201
202
        assert str(verr.value).startswith('input must be datetime')
203
        return
204
205
    @pytest.mark.parametrize('in_dates', [
206
        np.arange(np.datetime64("2000"), np.datetime64("2001"),
207
                  np.timedelta64(1, 'M')).astype('datetime64[s]').reshape((3,
208
                                                                           4)),
209
        np.arange(np.datetime64("1601"), np.datetime64("2100"),
210
                  np.timedelta64(1, 'Y')).astype('datetime64[s]')])
211
    def test_subsol_datetime64_array(self, in_dates):
212
        """Verify subsolar point calculation using an array of np.datetime64.
213
214
        Notes
215
        -----
216
        Tested by ensuring the array of np.datetime64 is equivalent to
217
        converting using single dt.datetime values
218
219
        """
220
        # Get the datetime64 output
221
        ss_out = helpers.subsol(in_dates)
222
223
        # Get the datetime scalar output for comparison
224
        self.in_shape = in_dates.shape
225
        true_out = [list(), list()]
226
        for in_date in in_dates.flatten():
227
            dtime = datetime64_to_datetime(in_date)
228
            out = helpers.subsol(dtime)
229
            true_out[0].append(out[0])
230
            true_out[1].append(out[1])
231
232
        # Evaluate the two outputs
233
        for i, self.calc_val in enumerate(ss_out):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable self does not seem to be defined.
Loading history...
234
            self.test_val = np.array(true_out[i]).reshape(self.in_shape)
235
            self.eval_output()
236
237
        return
238