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