Passed
Pull Request — main (#103)
by Angeline
01:40
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():
51
    def setup_method(self):
52
        self.in_shape = None
53
        self.calc_val = None
54
        self.test_val = None
55
56
    def teardown_method(self):
57
        del self.in_shape, self.calc_val, self.test_val
58
59
    def eval_output(self, rtol=1e-7, atol=0.0):
60
        """Evaluate the values and shape of the calculated and expected output.
61
        """
62
        np.testing.assert_allclose(self.calc_val, self.test_val, rtol=rtol,
63
                                   atol=atol)
64
        assert np.asarray(self.calc_val).shape == self.in_shape
65
        return
66
67
    @pytest.mark.parametrize('lat', [90, 0, -90, np.nan])
68
    def test_checklat_scalar(self, lat):
69
        """Test good latitude check with scalars."""
70
        self.calc_val = helpers.checklat(lat)
71
72
        if np.isnan(lat):
73
            assert np.isnan(self.calc_val)
74
        else:
75
            assert self.calc_val == lat
76
        return
77
78
    @pytest.mark.parametrize('lat', [(90 + 1e-5), (-90 - 1e-5)])
79
    def test_checklat_scalar_clip(self, lat):
80
        """Test good latitude check with scalars just beyond the lat limits."""
81
        self.calc_val = helpers.checklat(lat)
82
        self.test_val = np.sign(lat) * np.floor(abs(lat))
83
        assert self.calc_val == self.test_val
84
        return
85
86
    @pytest.mark.parametrize('in_args,msg',
87
                             [([90 + 1e-4], "lat must be in"),
88
                              ([-90 - 1e-4, 'glat'], "glat must be in"),
89
                              ([[-90 - 1e-5, -90, 0, 90, 90 + 1e-4], 'glat'],
90
                               "glat must be in"),
91
                              ([[-90 - 1e-4, -90, np.nan, np.nan, 90 + 1e-5]],
92
                               'lat must be in')])
93
    def test_checklat_error(self, in_args, msg):
94
        """Test bad latitude raises ValueError with appropriate message."""
95
        with pytest.raises(ValueError) as verr:
96
            helpers.checklat(*in_args)
97
98
        assert str(verr.value).startswith(msg)
99
        return
100
101
    @pytest.mark.parametrize('lat,test_lat',
102
                             [(np.linspace(-90 - 1e-5, 90 + 1e-5, 3),
103
                               [-90, 0, 90]),
104
                              (np.linspace(-90, 90, 3), [-90, 0, 90]),
105
                              ([-90 - 1e-5, 0, 90, np.nan],
106
                               [-90, 0, 90, np.nan]),
107
                              ([[-90, 0], [0, 90]], [[-90, 0], [0, 90]]),
108
                              ([[-90], [0], [90]], [[-90], [0], [90]])])
109
    def test_checklat_array(self, lat, test_lat):
110
        """Test good latitude with finite values."""
111
        self.calc_val = helpers.checklat(lat)
112
        self.in_shape = np.asarray(lat).shape
113
        self.test_val = test_lat
114
        self.eval_output(atol=1e-8)
115
        return
116
117
    @pytest.mark.parametrize('lat,test_sin', [
118
        (60, 0.96076892283052284), (10, 0.33257924500670238),
119
        ([60, 10], [0.96076892283052284, 0.33257924500670238]),
120
        ([[60, 10], [60, 10]], [[0.96076892283052284, 0.33257924500670238],
121
                                [0.96076892283052284, 0.33257924500670238]])])
122
    def test_getsinIm(self, lat, test_sin):
123
        """Test sin(Im) calculation for scalar and array inputs."""
124
        self.calc_val = helpers.getsinIm(lat)
125
        self.in_shape = np.asarray(lat).shape
126
        self.test_val = test_sin
127
        self.eval_output()
128
        return
129
130
    @pytest.mark.parametrize('lat,test_cos', [
131
        (60, 0.27735009811261463), (10, 0.94307531289434765),
132
        ([60, 10], [0.27735009811261463, 0.94307531289434765]),
133
        ([[60, 10], [60, 10]], [[0.27735009811261463, 0.94307531289434765],
134
                                [0.27735009811261463, 0.94307531289434765]])])
135
    def test_getcosIm(self, lat, test_cos):
136
        """Test cos(Im) calculation for scalar and array inputs."""
137
        self.calc_val = helpers.getcosIm(lat)
138
        self.in_shape = np.asarray(lat).shape
139
        self.test_val = test_cos
140
        self.eval_output()
141
        return
142
143
    @pytest.mark.parametrize('in_time,year', [
144
        (dt.datetime(2001, 1, 1), 2001), (dt.date(2001, 1, 1), 2001),
145
        (dt.datetime(2002, 1, 1), 2002),
146
        (dt.datetime(2005, 2, 3, 4, 5, 6), 2005.090877283105),
147
        (dt.datetime(2005, 12, 11, 10, 9, 8), 2005.943624682902)])
148
    def test_toYearFraction(self, in_time, year):
149
        """Test the datetime to fractional year calculation."""
150
        self.calc_val = helpers.toYearFraction(in_time)
151
        np.testing.assert_allclose(self.calc_val, year)
152
        return
153
154
    @pytest.mark.parametrize('gc_lat,gd_lat', [
155
        (0, 0), (90, 90), (30, 30.166923849507356), (60, 60.166364190170931),
156
        ([0, 90, 30], [0, 90, 30.166923849507356]),
157
        ([[0, 30], [90, 60]], [[0, 30.16692384950735],
158
                               [90, 60.166364190170931]])])
159
    def test_gc2gdlat(self, gc_lat, gd_lat):
160
        """Test geocentric to geodetic calculation."""
161
        self.calc_val = helpers.gc2gdlat(gc_lat)
162
        self.in_shape = np.asarray(gc_lat).shape
163
        self.test_val = gd_lat
164
        self.eval_output()
165
        return
166
167
    @pytest.mark.parametrize('in_time,test_loc', [
168
        (dt.datetime(2005, 2, 3, 4, 5, 6), (-16.505391672592904,
169
                                            122.17768157084515)),
170
        (dt.datetime(2010, 12, 11, 10, 9, 8), (-23.001554595838947,
171
                                               26.008999999955023)),
172
        (dt.datetime(2021, 11, 20, 12, 12, 12, 500000),
173
         (-19.79733856741465, -6.635177076865062)),
174
        (dt.datetime(1601, 1, 1, 0, 0, 0), (-23.06239721771427,
175
                                            -178.90131731228584)),
176
        (dt.datetime(2100, 12, 31, 23, 59, 59), (-23.021061422069053,
177
                                                 -179.23129780639425))])
178
    def test_subsol(self, in_time, test_loc):
179
        """Test the subsolar location calculation."""
180
        self.calc_val = helpers.subsol(in_time)
181
        np.testing.assert_allclose(self.calc_val, test_loc)
182
        return
183
184
    @pytest.mark.parametrize('in_time', [dt.datetime(1600, 12, 31, 23, 59, 59),
185
                                         dt.datetime(2101, 1, 1, 0, 0, 0)])
186
    def test_bad_subsol_date(self, in_time):
187
        """Test raises ValueError for bad time in subsolar calculation."""
188
        with pytest.raises(ValueError) as verr:
189
            helpers.subsol(in_time)
190
191
        assert str(verr.value).startswith('Year must be in')
192
        return
193
194
    @pytest.mark.parametrize('in_time', [None, 2015.0])
195
    def test_bad_subsol_input(self, in_time):
196
        """Test raises ValueError for bad input type in subsolar calculation."""
197
        with pytest.raises(ValueError) as verr:
198
            helpers.subsol(in_time)
199
200
        assert str(verr.value).startswith('input must be datetime')
201
        return
202
203
    @pytest.mark.parametrize('in_dates', [
204
        np.arange(np.datetime64("2000"), np.datetime64("2001"),
205
                  np.timedelta64(1, 'M')).astype('datetime64[s]').reshape((3,
206
                                                                           4)),
207
        np.arange(np.datetime64("1601"), np.datetime64("2100"),
208
                  np.timedelta64(1, 'Y')).astype('datetime64[s]')])
209
    def test_subsol_datetime64_array(self, in_dates):
210
        """Verify subsolar point calculation using an array of np.datetime64.
211
212
        Notes
213
        -----
214
        Tested by ensuring the array of np.datetime64 is equivalent to
215
        converting using single dt.datetime values
216
217
        """
218
        # Get the datetime64 output
219
        ss_out = helpers.subsol(in_dates)
220
221
        # Get the datetime scalar output for comparison
222
        self.in_shape = in_dates.shape
223
        true_out = [list(), list()]
224
        for in_date in in_dates.flatten():
225
            dtime = datetime64_to_datetime(in_date)
226
            out = helpers.subsol(dtime)
227
            true_out[0].append(out[0])
228
            true_out[1].append(out[1])
229
230
        # Evaluate the two outputs
231
        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...
232
            self.test_val = np.array(true_out[i]).reshape(self.in_shape)
233
            self.eval_output()
234
235
        return
236