1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
3import os
4import warnings
5from pathlib import Path
6
7import pytest
8import numpy as np
9
10from astropy.tests.helper import assert_quantity_allclose
11from astropy.utils.data import get_pkg_data_filename
12from astropy.utils.iers import iers
13from astropy import units as u
14from astropy.table import QTable
15from astropy.time import Time, TimeDelta
16
17CI = os.environ.get('CI', False)
18
19FILE_NOT_FOUND_ERROR = getattr(__builtins__, 'FileNotFoundError', OSError)
20
21try:
22    iers.IERS_A.open('finals2000A.all')  # check if IERS_A is available
23except OSError:
24    HAS_IERS_A = False
25else:
26    HAS_IERS_A = True
27
28IERS_A_EXCERPT = get_pkg_data_filename(os.path.join('data', 'iers_a_excerpt'))
29
30
31def setup_module():
32    # Need auto_download so that IERS_B won't be loaded and cause tests to
33    # fail. Files to be downloaded are handled appropriately in the tests.
34    iers.conf.auto_download = True
35
36
37def teardown_module():
38    # This setting is to be consistent with astropy/conftest.py
39    iers.conf.auto_download = False
40
41
42class TestBasic():
43    """Basic tests that IERS_B returns correct values"""
44
45    @pytest.mark.parametrize('iers_cls', (iers.IERS_B, iers.IERS))
46    def test_simple(self, iers_cls):
47        """Test the default behaviour for IERS_B and IERS."""
48        # Arguably, IERS itself should not be used at all, but it used to
49        # provide IERS_B by default so we check that it continues to do so.
50        # Eventually, IERS should probably be deprecated.
51        iers_cls.close()
52        assert iers_cls.iers_table is None
53        iers_tab = iers_cls.open()
54        assert iers_cls.iers_table is not None
55        assert iers_cls.iers_table is iers_tab
56        assert isinstance(iers_tab, QTable)
57        assert isinstance(iers_tab, iers.IERS_B)
58        assert (iers_tab['UT1_UTC'].unit / u.second).is_unity()
59        assert (iers_tab['PM_x'].unit / u.arcsecond).is_unity()
60        assert (iers_tab['PM_y'].unit / u.arcsecond).is_unity()
61        jd1 = np.array([2456108.5, 2456108.5, 2456108.5, 2456109.5, 2456109.5])
62        jd2 = np.array([0.49999421, 0.99997685, 0.99998843, 0., 0.5])
63        ut1_utc = iers_tab.ut1_utc(jd1, jd2)
64        assert isinstance(ut1_utc, u.Quantity)
65        assert (ut1_utc.unit / u.second).is_unity()
66        # IERS files change at the 0.1 ms level; see gh-6981
67        assert_quantity_allclose(ut1_utc, [-0.5868211, -0.5868184, -0.5868184,
68                                           0.4131816, 0.41328895] * u.s,
69                                 atol=0.1*u.ms)
70        # should be future-proof; surely we've moved to another planet by then
71        with pytest.raises(IndexError):
72            ut1_utc2, status2 = iers_tab.ut1_utc(1e11, 0.)
73        # also check it returns the right status
74        ut1_utc2, status2 = iers_tab.ut1_utc(jd1, jd2, return_status=True)
75        assert np.all(status2 == iers.FROM_IERS_B)
76        ut1_utc4, status4 = iers_tab.ut1_utc(1e11, 0., return_status=True)
77        assert status4 == iers.TIME_BEYOND_IERS_RANGE
78
79        # check it works via Time too
80        t = Time(jd1, jd2, format='jd', scale='utc')
81        ut1_utc3 = iers_tab.ut1_utc(t)
82        assert_quantity_allclose(ut1_utc3, [-0.5868211, -0.5868184, -0.5868184,
83                                            0.4131816, 0.41328895] * u.s,
84                                 atol=0.1*u.ms)
85
86        # Table behaves properly as a table (e.g. can be sliced)
87        assert len(iers_tab[:2]) == 2
88
89    def test_open_filename(self):
90        iers.IERS_B.close()
91        iers.IERS_B.open(iers.IERS_B_FILE)
92        assert iers.IERS_B.iers_table is not None
93        assert isinstance(iers.IERS_B.iers_table, QTable)
94        iers.IERS_B.close()
95        with pytest.raises(FILE_NOT_FOUND_ERROR):
96            iers.IERS_B.open('surely this does not exist')
97
98    def test_open_network_url(self):
99        iers.IERS_A.close()
100        iers.IERS_A.open(Path(IERS_A_EXCERPT).as_uri())
101        assert iers.IERS_A.iers_table is not None
102        assert isinstance(iers.IERS_A.iers_table, QTable)
103        iers.IERS_A.close()
104
105
106class TestIERS_AExcerpt():
107    def test_simple(self):
108        # Test the IERS A reader. It is also a regression tests that ensures
109        # values do not get overridden by IERS B; see #4933.
110        iers_tab = iers.IERS_A.open(IERS_A_EXCERPT)
111
112        assert (iers_tab['UT1_UTC'].unit / u.second).is_unity()
113        assert 'P' in iers_tab['UT1Flag']
114        assert 'I' in iers_tab['UT1Flag']
115        assert 'B' in iers_tab['UT1Flag']
116        assert np.all((iers_tab['UT1Flag'] == 'I') |
117                      (iers_tab['UT1Flag'] == 'P') |
118                      (iers_tab['UT1Flag'] == 'B'))
119
120        assert (iers_tab['dX_2000A'].unit / u.marcsec).is_unity()
121        assert (iers_tab['dY_2000A'].unit / u.marcsec).is_unity()
122        assert 'P' in iers_tab['NutFlag']
123        assert 'I' in iers_tab['NutFlag']
124        assert 'B' in iers_tab['NutFlag']
125        assert np.all((iers_tab['NutFlag'] == 'P') |
126                      (iers_tab['NutFlag'] == 'I') |
127                      (iers_tab['NutFlag'] == 'B'))
128
129        assert (iers_tab['PM_x'].unit / u.arcsecond).is_unity()
130        assert (iers_tab['PM_y'].unit / u.arcsecond).is_unity()
131        assert 'P' in iers_tab['PolPMFlag']
132        assert 'I' in iers_tab['PolPMFlag']
133        assert 'B' in iers_tab['PolPMFlag']
134        assert np.all((iers_tab['PolPMFlag'] == 'P') |
135                      (iers_tab['PolPMFlag'] == 'I') |
136                      (iers_tab['PolPMFlag'] == 'B'))
137
138        t = Time([57053., 57054., 57055.], format='mjd')
139        ut1_utc, status = iers_tab.ut1_utc(t, return_status=True)
140        assert status[0] == iers.FROM_IERS_B
141        assert np.all(status[1:] == iers.FROM_IERS_A)
142        # These values are *exactly* as given in the table, so they should
143        # match to double precision accuracy.
144        assert_quantity_allclose(ut1_utc,
145                                 [-0.4916557, -0.4925323, -0.4934373] * u.s,
146                                 atol=0.1*u.ms)
147
148        dcip_x, dcip_y, status = iers_tab.dcip_xy(t, return_status=True)
149        assert status[0] == iers.FROM_IERS_B
150        assert np.all(status[1:] == iers.FROM_IERS_A)
151        # These values are *exactly* as given in the table, so they should
152        # match to double precision accuracy.
153        print(dcip_x)
154        print(dcip_y)
155        assert_quantity_allclose(dcip_x,
156                                 [-0.086, -0.093, -0.087] * u.marcsec,
157                                 atol=1.*u.narcsec)
158        assert_quantity_allclose(dcip_y,
159                                 [0.094, 0.081, 0.072] * u.marcsec,
160                                 atol=1*u.narcsec)
161
162        pm_x, pm_y, status = iers_tab.pm_xy(t, return_status=True)
163        assert status[0] == iers.FROM_IERS_B
164        assert np.all(status[1:] == iers.FROM_IERS_A)
165        assert_quantity_allclose(pm_x,
166                                 [0.003734, 0.004581, 0.004623] * u.arcsec,
167                                 atol=0.1*u.marcsec)
168        assert_quantity_allclose(pm_y,
169                                 [0.310824, 0.313150, 0.315517] * u.arcsec,
170                                 atol=0.1*u.marcsec)
171
172        # Table behaves properly as a table (e.g. can be sliced)
173        assert len(iers_tab[:2]) == 2
174
175
176@pytest.mark.skipif('not HAS_IERS_A')
177class TestIERS_A():
178
179    def test_simple(self):
180        """Test that open() by default reads a 'finals2000A.all' file."""
181        # Ensure we remove any cached table (gh-5131).
182        iers.IERS_A.close()
183        iers_tab = iers.IERS_A.open()
184        jd1 = np.array([2456108.5, 2456108.5, 2456108.5, 2456109.5, 2456109.5])
185        jd2 = np.array([0.49999421, 0.99997685, 0.99998843, 0., 0.5])
186        ut1_utc, status = iers_tab.ut1_utc(jd1, jd2, return_status=True)
187        assert np.all(status == iers.FROM_IERS_B)
188        assert_quantity_allclose(ut1_utc, [-0.5868211, -0.5868184, -0.5868184,
189                                           0.4131816, 0.41328895] * u.s,
190                                 atol=0.1*u.ms)
191        ut1_utc2, status2 = iers_tab.ut1_utc(1e11, 0., return_status=True)
192        assert status2 == iers.TIME_BEYOND_IERS_RANGE
193
194        tnow = Time.now()
195
196        ut1_utc3, status3 = iers_tab.ut1_utc(tnow, return_status=True)
197        assert status3 == iers.FROM_IERS_A_PREDICTION
198        assert ut1_utc3 != 0.
199
200
201class TestIERS_Auto():
202
203    def setup_class(self):
204        """Set up useful data for the tests.
205        """
206        self.N = 40
207        self.ame = 30.0
208        self.iers_a_file_1 = get_pkg_data_filename(
209            os.path.join('data', 'finals2000A-2016-02-30-test'))
210        self.iers_a_file_2 = get_pkg_data_filename(
211            os.path.join('data', 'finals2000A-2016-04-30-test'))
212        self.iers_a_url_1 = Path(self.iers_a_file_1).as_uri()
213        self.iers_a_url_2 = Path(self.iers_a_file_2).as_uri()
214        self.t = Time.now() + TimeDelta(10, format='jd') * np.arange(self.N)
215
216    def teardown_method(self, method):
217        """Run this after every test.
218        """
219        iers.IERS_Auto.close()
220
221    def test_interpolate_error_formatting(self):
222        """Regression test: make sure the error message in
223        IERS_Auto._check_interpolate_indices() is formatted correctly.
224        """
225        with iers.conf.set_temp('iers_auto_url', self.iers_a_url_1):
226            with iers.conf.set_temp('iers_auto_url_mirror', self.iers_a_url_1):
227                with iers.conf.set_temp('auto_max_age', self.ame):
228                    with pytest.raises(ValueError) as err:
229                        iers_table = iers.IERS_Auto.open()
230                        with warnings.catch_warnings():
231                            # Ignoring this if it comes up -- IERS_Auto predictive
232                            # values are older than 30.0 days but downloading the
233                            # latest table did not find newer values
234                            warnings.simplefilter('ignore', iers.IERSStaleWarning)
235                            iers_table.ut1_utc(self.t.jd1, self.t.jd2)
236        assert str(err.value) == iers.INTERPOLATE_ERROR.format(self.ame)
237
238    def test_auto_max_age_none(self):
239        """Make sure that iers.INTERPOLATE_ERROR's advice about setting
240        auto_max_age = None actually works.
241        """
242        with iers.conf.set_temp('iers_auto_url', self.iers_a_url_1):
243            with iers.conf.set_temp('auto_max_age', None):
244                iers_table = iers.IERS_Auto.open()
245                delta = iers_table.ut1_utc(self.t.jd1, self.t.jd2)
246        assert isinstance(delta, np.ndarray)
247        assert delta.shape == (self.N,)
248        assert_quantity_allclose(delta, np.array([-0.2246227]*self.N)*u.s)
249
250    def test_auto_max_age_minimum(self):
251        """Check that the minimum auto_max_age is enforced.
252        """
253        with iers.conf.set_temp('iers_auto_url', self.iers_a_url_1):
254            with iers.conf.set_temp('auto_max_age', 5.0):
255                with pytest.raises(ValueError) as err:
256                    iers_table = iers.IERS_Auto.open()
257                    _ = iers_table.ut1_utc(self.t.jd1, self.t.jd2)
258        assert str(err.value) == 'IERS auto_max_age configuration value must be larger than 10 days'
259
260    def test_no_auto_download(self):
261        with iers.conf.set_temp('auto_download', False):
262            t = iers.IERS_Auto.open()
263        assert type(t) is iers.IERS_B
264
265    @pytest.mark.remote_data
266    def test_simple(self):
267
268        with iers.conf.set_temp('iers_auto_url', self.iers_a_url_1):
269
270            dat = iers.IERS_Auto.open()
271            assert dat['MJD'][0] == 57359.0 * u.d
272            assert dat['MJD'][-1] == 57539.0 * u.d
273
274            # Pretend we are accessing at a time 7 days after start of predictive data
275            predictive_mjd = dat.meta['predictive_mjd']
276            dat._time_now = Time(predictive_mjd, format='mjd') + 7 * u.d
277
278            # Look at times before and after the test file begins.  0.1292905 is
279            # the IERS-B value from MJD=57359.  The value in
280            # finals2000A-2016-02-30-test has been replaced at this point.
281            assert np.allclose(dat.ut1_utc(Time(50000, format='mjd').jd).value, 0.1293286)
282            assert np.allclose(dat.ut1_utc(Time(60000, format='mjd').jd).value, -0.2246227)
283
284            # Now pretend we are accessing at time 60 days after start of predictive data.
285            # There will be a warning when downloading the file doesn't give new data
286            # and an exception when extrapolating into the future with insufficient data.
287            dat._time_now = Time(predictive_mjd, format='mjd') + 60 * u.d
288            assert np.allclose(dat.ut1_utc(Time(50000, format='mjd').jd).value, 0.1293286)
289            with pytest.warns(iers.IERSStaleWarning, match='IERS_Auto predictive '
290                              'values are older') as warns, \
291                 pytest.raises(ValueError, match='interpolating from IERS_Auto '
292                               'using predictive values'):
293                dat.ut1_utc(Time(60000, format='mjd').jd)
294            assert len(warns) == 1
295
296            # Warning only if we are getting return status
297            with pytest.warns(iers.IERSStaleWarning, match='IERS_Auto '
298                              'predictive values are older') as warns:
299                dat.ut1_utc(Time(60000, format='mjd').jd, return_status=True)
300            assert len(warns) == 1
301
302            # Now set auto_max_age = None which says that we don't care how old the
303            # available IERS-A file is.  There should be no warnings or exceptions.
304            with iers.conf.set_temp('auto_max_age', None):
305                dat.ut1_utc(Time(60000, format='mjd').jd)
306
307        # Now point to a later file with same values but MJD increased by
308        # 60 days and see that things work.  dat._time_now is still the same value
309        # as before, i.e. right around the start of predictive values for the new file.
310        # (In other words this is like downloading the latest file online right now).
311        with iers.conf.set_temp('iers_auto_url', self.iers_a_url_2):
312
313            # Look at times before and after the test file begins.  This forces a new download.
314            assert np.allclose(dat.ut1_utc(Time(50000, format='mjd').jd).value, 0.1293286)
315            assert np.allclose(dat.ut1_utc(Time(60000, format='mjd').jd).value, -0.3)
316
317            # Now the time range should be different.
318            assert dat['MJD'][0] == 57359.0 * u.d
319            assert dat['MJD'][-1] == (57539.0 + 60) * u.d
320
321
322@pytest.mark.remote_data
323def test_IERS_B_parameters_loading_into_IERS_Auto():
324    A = iers.IERS_Auto.open()
325    B = iers.IERS_B.open()
326
327    ok_A = A["MJD"] <= B["MJD"][-1]
328    assert not np.all(ok_A), "IERS B covers all of IERS A: should not happen"
329
330    # We only overwrite IERS_B values in the IERS_A table that were already
331    # there in the first place.  Better take that into account.
332    ok_A &= np.isfinite(A["UT1_UTC_B"])
333
334    i_B = np.searchsorted(B["MJD"], A["MJD"][ok_A])
335
336    assert np.all(np.diff(i_B) == 1), "Valid region not contiguous"
337    assert np.all(A["MJD"][ok_A] == B["MJD"][i_B])
338    # Check that values are copied correctly.  Since units are not
339    # necessarily the same, we use allclose with very strict tolerance.
340    for name in ("UT1_UTC", "PM_x", "PM_y", "dX_2000A", "dY_2000A"):
341        assert_quantity_allclose(
342            A[name][ok_A], B[name][i_B], rtol=1e-15,
343            err_msg=("Bug #9206 IERS B parameter {} not copied over "
344                     "correctly to IERS Auto".format(name)))
345
346
347# Issue with FTP, rework test into previous one when it's fixed
348@pytest.mark.skipif("CI", reason="Flaky on CI")
349@pytest.mark.remote_data
350def test_iers_a_dl():
351    iersa_tab = iers.IERS_A.open(iers.IERS_A_URL, cache=False)
352    try:
353        # some basic checks to ensure the format makes sense
354        assert len(iersa_tab) > 0
355        assert 'UT1_UTC_A' in iersa_tab.colnames
356    finally:
357        iers.IERS_A.close()
358
359
360@pytest.mark.remote_data
361def test_iers_a_dl_mirror():
362    iersa_tab = iers.IERS_A.open(iers.IERS_A_URL_MIRROR, cache=False)
363    try:
364        # some basic checks to ensure the format makes sense
365        assert len(iersa_tab) > 0
366        assert 'UT1_UTC_A' in iersa_tab.colnames
367    finally:
368        iers.IERS_A.close()
369
370
371@pytest.mark.remote_data
372def test_iers_b_dl():
373    iersb_tab = iers.IERS_B.open(iers.IERS_B_URL, cache=False)
374    try:
375        # some basic checks to ensure the format makes sense
376        assert len(iersb_tab) > 0
377        assert 'UT1_UTC' in iersb_tab.colnames
378    finally:
379        iers.IERS_B.close()
380