Completed
Push — main ( fcb71d...fb1ae8 )
by Angeline
20s queued 16s
created

TestHelpers.test_checklat_scalar_clip()   A

Complexity

Conditions 1

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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