Passed
Pull Request — develop (#128)
by
unknown
04:24
created

TestHelpers.test_subsol_datetime64_array()   A

Complexity

Conditions 3

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 38
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
    """Test class for the helper sub-module."""
52
53
    def setup_method(self):
54
        """Set up a clean test environment."""
55
        self.in_shape = None
56
        self.calc_val = None
57
        self.test_val = None
58
59
    def teardown_method(self):
60
        """Clean up the test environment."""
61
        del self.in_shape, self.calc_val, self.test_val
62
63
    def eval_output(self, rtol=1e-7, atol=0.0):
64
        """Evaluate the values and shape of the calculated and expected output.
65
        """
66
        np.testing.assert_allclose(self.calc_val, self.test_val, rtol=rtol,
67
                                   atol=atol)
68
        assert np.asarray(self.calc_val).shape == self.in_shape
69
        return
70
71
    @pytest.mark.parametrize('lat', [90, 0, -90, np.nan])
72
    def test_checklat_scalar(self, lat):
73
        """Test good latitude check with scalars.
74
75
        Parameters
76
        ----------
77
        lat : int or float
78
            Latitude in degrees N
79
80
        """
81
        self.calc_val = helpers.checklat(lat)
82
83
        if np.isnan(lat):
84
            assert np.isnan(self.calc_val)
85
        else:
86
            assert self.calc_val == lat
87
        return
88
89
    @pytest.mark.parametrize('lat', [(90 + 1e-5), (-90 - 1e-5)])
90
    def test_checklat_scalar_clip(self, lat):
91
        """Test good latitude check with scalars just beyond the lat limits.
92
93
        Parameters
94
        ----------
95
        lat : int or float
96
            Latitude in degrees N
97
98
        """
99
        self.calc_val = helpers.checklat(lat)
100
        self.test_val = np.sign(lat) * np.floor(abs(lat))
101
        assert self.calc_val == self.test_val
102
        return
103
104
    @pytest.mark.parametrize('in_args,msg',
105
                             [([90 + 1e-4], "lat must be in"),
106
                              ([-90 - 1e-4, 'glat'], "glat must be in"),
107
                              ([[-90 - 1e-5, -90, 0, 90, 90 + 1e-4], 'glat'],
108
                               "glat must be in"),
109
                              ([[-90 - 1e-4, -90, np.nan, np.nan, 90 + 1e-5]],
110
                               'lat must be in')])
111
    def test_checklat_error(self, in_args, msg):
112
        """Test bad latitude raises ValueError with appropriate message.
113
114
        Parameters
115
        ----------
116
        in_args : list
117
            List of input arguments
118
        msg : str
119
            Expected error message
120
121
        """
122
        with pytest.raises(ValueError) as verr:
123
            helpers.checklat(*in_args)
124
125
        assert str(verr.value).startswith(msg)
126
        return
127
128
    @pytest.mark.parametrize('lat,test_lat',
129
                             [(np.linspace(-90 - 1e-5, 90 + 1e-5, 3),
130
                               [-90, 0, 90]),
131
                              (np.linspace(-90, 90, 3), [-90, 0, 90]),
132
                              ([-90 - 1e-5, 0, 90, np.nan],
133
                               [-90, 0, 90, np.nan]),
134
                              ([[-90, 0], [0, 90]], [[-90, 0], [0, 90]]),
135
                              ([[-90], [0], [90]], [[-90], [0], [90]])])
136
    def test_checklat_array(self, lat, test_lat):
137
        """Test good latitude with finite values.
138
139
        Parameters
140
        ----------
141
        lat : array-like
142
            Latitudes in degrees N
143
        test_lat : list-like
144
            Output latitudes in degrees N
145
146
        """
147
        self.calc_val = helpers.checklat(lat)
148
        self.in_shape = np.asarray(lat).shape
149
        self.test_val = test_lat
150
        self.eval_output(atol=1e-8)
151
        return
152
153
    @pytest.mark.parametrize('lat,test_sin', [
154
        (60, 0.96076892283052284), (10, 0.33257924500670238),
155
        ([60, 10], [0.96076892283052284, 0.33257924500670238]),
156
        ([[60, 10], [60, 10]], [[0.96076892283052284, 0.33257924500670238],
157
                                [0.96076892283052284, 0.33257924500670238]])])
158
    def test_getsinIm(self, lat, test_sin):
159
        """Test sin(Im) calculation for scalar and array inputs.
160
161
        Parameters
162
        ----------
163
        lat : float
164
            Latitude in degrees N
165
        test_sin : float
166
            Output value
167
168
        """
169
        self.calc_val = helpers.getsinIm(lat)
170
        self.in_shape = np.asarray(lat).shape
171
        self.test_val = test_sin
172
        self.eval_output()
173
        return
174
175
    @pytest.mark.parametrize('lat,test_cos', [
176
        (60, 0.27735009811261463), (10, 0.94307531289434765),
177
        ([60, 10], [0.27735009811261463, 0.94307531289434765]),
178
        ([[60, 10], [60, 10]], [[0.27735009811261463, 0.94307531289434765],
179
                                [0.27735009811261463, 0.94307531289434765]])])
180
    def test_getcosIm(self, lat, test_cos):
181
        """Test cos(Im) calculation for scalar and array inputs.
182
183
        Parameters
184
        ----------
185
        lat : float
186
            Latitude in degrees N
187
        test_cos : float
188
            Expected output
189
190
        """
191
        self.calc_val = helpers.getcosIm(lat)
192
        self.in_shape = np.asarray(lat).shape
193
        self.test_val = test_cos
194
        self.eval_output()
195
        return
196
197
    @pytest.mark.parametrize('in_time,year', [
198
        (dt.datetime(2001, 1, 1), 2001), (dt.date(2001, 1, 1), 2001),
199
        (dt.datetime(2002, 1, 1), 2002),
200
        (dt.datetime(2005, 2, 3, 4, 5, 6), 2005.090877283105),
201
        (dt.datetime(2005, 12, 11, 10, 9, 8), 2005.943624682902)])
202
    def test_toYearFraction(self, in_time, year):
203
        """Test the datetime to fractional year calculation.
204
205
        Parameters
206
        ----------
207
        in_time : dt.datetime or dt.date
208
            Input time in a datetime format
209
        year : int or float
210
            Output year with fractional values
211
212
        """
213
        self.calc_val = helpers.toYearFraction(in_time)
214
        np.testing.assert_allclose(self.calc_val, year)
215
        return
216
217
    @pytest.mark.parametrize('gc_lat,gd_lat', [
218
        (0, 0), (90, 90), (30, 30.166923849507356), (60, 60.166364190170931),
219
        ([0, 90, 30], [0, 90, 30.166923849507356]),
220
        ([[0, 30], [90, 60]], [[0, 30.16692384950735],
221
                               [90, 60.166364190170931]])])
222
    def test_gc2gdlat(self, gc_lat, gd_lat):
223
        """Test geocentric to geodetic calculation.
224
225
        Parameters
226
        ----------
227
        gc_lat : int or float
228
            Geocentric latitude in degrees N
229
        gd_lat : int or float
230
            Geodetic latitude in degrees N
231
232
        """
233
        self.calc_val = helpers.gc2gdlat(gc_lat)
234
        self.in_shape = np.asarray(gc_lat).shape
235
        self.test_val = gd_lat
236
        self.eval_output()
237
        return
238
239
    @pytest.mark.parametrize('in_time,test_loc', [
240
        (dt.datetime(2005, 2, 3, 4, 5, 6), (-16.505391672592904,
241
                                            122.17768157084515)),
242
        (dt.datetime(2010, 12, 11, 10, 9, 8), (-23.001554595838947,
243
                                               26.008999999955023)),
244
        (dt.datetime(2021, 11, 20, 12, 12, 12, 500000),
245
         (-19.79733856741465, -6.635177076865062)),
246
        (dt.datetime(1601, 1, 1, 0, 0, 0), (-23.06239721771427,
247
                                            -178.90131731228584)),
248
        (dt.datetime(2100, 12, 31, 23, 59, 59), (-23.021061422069053,
249
                                                 -179.23129780639425))])
250
    def test_subsol(self, in_time, test_loc):
251
        """Test the subsolar location calculation.
252
253
        Parameters
254
        ----------
255
        in_time : dt.datetime
256
            Input time
257
        test_loc : tuple
258
            Expected output
259
260
        """
261
        self.calc_val = helpers.subsol(in_time)
262
        np.testing.assert_allclose(self.calc_val, test_loc)
263
        return
264
265
    @pytest.mark.parametrize('in_time', [dt.datetime(1600, 12, 31, 23, 59, 59),
266
                                         dt.datetime(2101, 1, 1, 0, 0, 0)])
267
    def test_bad_subsol_date(self, in_time):
268
        """Test raises ValueError for bad time in subsolar calculation.
269
270
        Parameters
271
        ----------
272
        in_time : dt.datetime
273
            Input time
274
275
        """
276
        with pytest.raises(ValueError) as verr:
277
            helpers.subsol(in_time)
278
279
        assert str(verr.value).startswith('Year must be in')
280
        return
281
282
    @pytest.mark.parametrize('in_time', [None, 2015.0])
283
    def test_bad_subsol_input(self, in_time):
284
        """Test raises ValueError for bad input type in subsolar calculation.
285
286
        Parameters
287
        ----------
288
        in_time : NoneType or float
289
            Badly formatted input time
290
291
        """
292
        with pytest.raises(ValueError) as verr:
293
            helpers.subsol(in_time)
294
295
        assert str(verr.value).startswith('input must be datetime')
296
        return
297
298
    @pytest.mark.parametrize('in_dates', [
299
        np.arange(np.datetime64("2000"), np.datetime64("2001"),
300
                  np.timedelta64(1, 'M')).astype('datetime64[s]').reshape((3,
301
                                                                           4)),
302
        np.arange(np.datetime64("1601"), np.datetime64("2100"),
303
                  np.timedelta64(1, 'Y')).astype('datetime64[s]')])
304
    def test_subsol_datetime64_array(self, in_dates):
305
        """Verify subsolar point calculation using an array of np.datetime64.
306
307
        Parameters
308
        ----------
309
        in_time : array-like
310
            Array of input times
311
312
        Notes
313
        -----
314
        Tested by ensuring the array of np.datetime64 is equivalent to
315
        converting using single dt.datetime values
316
317
        """
318
        # Get the datetime64 output
319
        ss_out = helpers.subsol(in_dates)
320
321
        # Get the datetime scalar output for comparison
322
        self.in_shape = in_dates.shape
323
        true_out = [list(), list()]
324
        for in_date in in_dates.flatten():
325
            dtime = datetime64_to_datetime(in_date)
326
            out = helpers.subsol(dtime)
327
            true_out[0].append(out[0])
328
            true_out[1].append(out[1])
329
330
        # Evaluate the two outputs
331
        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...
332
            self.test_val = np.array(true_out[i]).reshape(self.in_shape)
333
            self.eval_output()
334
335
        return
336