# Licensed under a 3-clause BSD style license - see LICENSE.rst import functools import itertools import erfa import numpy as np import pytest from astropy import units as u from astropy.time import Time from astropy.time.core import SIDEREAL_TIME_MODELS from astropy.utils import iers allclose_hours = functools.partial(np.allclose, rtol=1e-15, atol=3e-8) # 0.1 ms atol; IERS-B files change at that level. within_1_second = functools.partial(np.allclose, rtol=1.0, atol=1.0 / 3600.0) within_2_seconds = functools.partial(np.allclose, rtol=1.0, atol=2.0 / 3600.0) def test_doc_string_contains_models(): """The doc string is formatted; this ensures this remains working.""" for kind in ("mean", "apparent"): for model in SIDEREAL_TIME_MODELS[kind]: assert model in Time.sidereal_time.__doc__ class TestERFATestCases: """Test that we reproduce the test cases given in erfa/src/t_erfa_c.c""" def setup_class(cls): # Sidereal time tests use the following JD inputs. cls.time_ut1 = Time(2400000.5, 53736.0, scale="ut1", format="jd") cls.time_tt = Time(2400000.5, 53736.0, scale="tt", format="jd") # but tt!=ut1 at these dates, unlike what is assumed, so we cannot # reproduce this exactly. Now it does not really matter, # but may as well fake this (and avoid IERS table lookup here) cls.time_ut1.delta_ut1_utc = 0.0 cls.time_ut1.delta_ut1_utc = ( 24 * 3600 * ( (cls.time_ut1.tt.jd1 - cls.time_tt.jd1) + (cls.time_ut1.tt.jd2 - cls.time_tt.jd2) ) ) def test_setup(self): assert np.allclose( (self.time_ut1.tt.jd1 - self.time_tt.jd1) + (self.time_ut1.tt.jd2 - self.time_tt.jd2), 0.0, atol=1.0e-14, ) @pytest.mark.parametrize( "erfa_test_input", ( (1.754174972210740592, 1e-12, "eraGmst00"), (1.754174971870091203, 1e-12, "eraGmst06"), (1.754174981860675096, 1e-12, "eraGmst82"), (1.754166138018281369, 1e-12, "eraGst00a"), (1.754166136510680589, 1e-12, "eraGst00b"), (1.754166137675019159, 1e-12, "eraGst06a"), (1.754166136020645203, 1e-12, "eraGst94"), ), ) def test_iau_models(self, erfa_test_input): result, precision, name = erfa_test_input if name[4] == "m": kind = "mean" model_name = f"IAU{20 if name[7] == '0' else 19:2d}{name[7:]:s}" else: kind = "apparent" model_name = f"IAU{20 if name[6] == '0' else 19:2d}{name[6:].upper():s}" assert kind in SIDEREAL_TIME_MODELS.keys() assert model_name in SIDEREAL_TIME_MODELS[kind] gst = self.time_ut1.sidereal_time(kind, "greenwich", model_name) assert np.allclose(gst.to_value("radian"), result, rtol=1.0, atol=precision) def test_era(self): # Separate since it does not use the same time. time_ut1 = Time(2400000.5, 54388.0, format="jd", scale="ut1") era = time_ut1.earth_rotation_angle("tio") expected = 0.4022837240028158102 assert np.abs(era.to_value(u.radian) - expected) < 1e-12 class TestST: """Test Greenwich Sidereal Time. Unlike above, this is relative to what was found earlier, so checks changes in implementation, including leap seconds, rather than correctness""" @classmethod def setup_class(cls): cls.orig_auto_download = iers.conf.auto_download iers.conf.auto_download = False cls.t1 = Time( [ "2012-06-30 12:00:00", "2012-06-30 23:59:59", "2012-06-30 23:59:60", "2012-07-01 00:00:00", "2012-07-01 12:00:00", ], scale="utc", ) cls.t2 = Time(cls.t1, location=("120d", "10d")) @classmethod def teardown_class(cls): iers.conf.auto_download = cls.orig_auto_download def test_gmst(self): """Compare Greenwich Mean Sidereal Time with what was found earlier""" gmst_compare = np.array( [ 6.5968497894730564, 18.629426164144697, 18.629704702452862, 18.629983240761003, 6.6628381828899643, ] ) gmst = self.t1.sidereal_time("mean", "greenwich") assert allclose_hours(gmst.value, gmst_compare) def test_gst(self): """Compare Greenwich Apparent Sidereal Time with what was found earlier""" gst_compare = np.array( [ 6.5971168570494854, 18.629694220878296, 18.62997275921186, 18.630251297545389, 6.6631074284018244, ] ) gst = self.t1.sidereal_time("apparent", "greenwich") assert allclose_hours(gst.value, gst_compare) def test_era(self): """Compare ERA relative to erfa.era00 test case.""" t = Time(2400000.5, 54388.0, format="jd", location=(0, 0), scale="ut1") era = t.earth_rotation_angle() expected = 0.4022837240028158102 * u.radian # Without the TIO locator/polar motion, this should be close already. assert np.abs(era - expected) < 1e-10 * u.radian # And with it, one should reach full precision. sp = erfa.sp00(t.tt.jd1, t.tt.jd2) iers_table = iers.earth_orientation_table.get() xp, yp = (c.to_value(u.rad) for c in iers_table.pm_xy(t)) r = erfa.rx(-yp, erfa.ry(-xp, erfa.rz(sp, np.eye(3)))) expected1 = expected + (np.arctan2(r[0, 1], r[0, 0]) << u.radian) assert np.abs(era - expected1) < 1e-12 * u.radian # Now try at a longitude different from 0. t2 = Time(2400000.5, 54388.0, format="jd", location=(45, 0), scale="ut1") era2 = t2.earth_rotation_angle() r2 = erfa.rz(np.deg2rad(45), r) expected2 = expected + (np.arctan2(r2[0, 1], r2[0, 0]) << u.radian) assert np.abs(era2 - expected2) < 1e-12 * u.radian def test_gmst_gst_close(self): """Check that Mean and Apparent are within a few seconds.""" gmst = self.t1.sidereal_time("mean", "greenwich") gst = self.t1.sidereal_time("apparent", "greenwich") assert within_2_seconds(gst.value, gmst.value) def test_gmst_era_close(self): """Check that mean sidereal time and earth rotation angle are close.""" gmst = self.t1.sidereal_time("mean", "greenwich") era = self.t1.earth_rotation_angle("tio") assert within_2_seconds(era.value, gmst.value) def test_gmst_independent_of_self_location(self): """Check that Greenwich time does not depend on self.location""" gmst1 = self.t1.sidereal_time("mean", "greenwich") gmst2 = self.t2.sidereal_time("mean", "greenwich") assert allclose_hours(gmst1.value, gmst2.value) def test_gmst_vs_lmst(self): """Check that Greenwich and local sidereal time differ.""" gmst = self.t1.sidereal_time("mean", "greenwich") lmst = self.t1.sidereal_time("mean", 0) assert allclose_hours(lmst.value, gmst.value) assert np.all(np.abs(lmst - gmst) > 1e-10 * u.hourangle) @pytest.mark.parametrize("kind", ("mean", "apparent")) def test_lst(self, kind): """Compare Local Sidereal Time with what was found earlier, as well as with what is expected from GMST """ lst_compare = { "mean": np.array( [ 14.596849789473058, 2.629426164144693, 2.6297047024528588, 2.6299832407610033, 14.662838182889967, ] ), "apparent": np.array( [ 14.597116857049487, 2.6296942208782959, 2.6299727592118565, 2.6302512975453887, 14.663107428401826, ] ), } gmst2 = self.t2.sidereal_time(kind, "greenwich") lmst2 = self.t2.sidereal_time(kind) assert allclose_hours(lmst2.value, lst_compare[kind]) assert allclose_hours( (lmst2 - gmst2).wrap_at("12h").value, self.t2.location.lon.to_value("hourangle"), ) # check it also works when one gives longitude explicitly lmst1 = self.t1.sidereal_time(kind, self.t2.location.lon) assert allclose_hours(lmst1.value, lst_compare[kind]) def test_lst_string_longitude(self): lmst1 = self.t1.sidereal_time("mean", longitude="120d") lmst2 = self.t2.sidereal_time("mean") assert allclose_hours(lmst1.value, lmst2.value) def test_lst_needs_location(self): with pytest.raises(ValueError): self.t1.sidereal_time("mean") with pytest.raises(ValueError): self.t1.sidereal_time("mean", None) def test_lera(self): lera_compare = np.array( [ 14.586176631122177, 2.618751847545134, 2.6190303858265067, 2.619308924107852, 14.652162695594276, ] ) gera2 = self.t2.earth_rotation_angle("tio") lera2 = self.t2.earth_rotation_angle() assert allclose_hours(lera2.value, lera_compare) assert allclose_hours( (lera2 - gera2).wrap_at("12h").value, self.t2.location.lon.to_value("hourangle"), ) # Check it also works when one gives location explicitly. # This also verifies input of a location generally. lera1 = self.t1.earth_rotation_angle(self.t2.location) assert allclose_hours(lera1.value, lera_compare) class TestModelInterpretation: """Check that models are different, and that wrong models are recognized""" @classmethod def setup_class(cls): cls.orig_auto_download = iers.conf.auto_download iers.conf.auto_download = False cls.t = Time(["2012-06-30 12:00:00"], scale="utc", location=("120d", "10d")) @classmethod def teardown_class(cls): iers.conf.auto_download = cls.orig_auto_download @pytest.mark.parametrize("kind", ("mean", "apparent")) def test_model_uniqueness(self, kind): """Check models give different answers, yet are close.""" for model1, model2 in itertools.combinations( SIDEREAL_TIME_MODELS[kind].keys(), 2 ): gst1 = self.t.sidereal_time(kind, "greenwich", model1) gst2 = self.t.sidereal_time(kind, "greenwich", model2) assert np.all(gst1.value != gst2.value) assert within_1_second(gst1.value, gst2.value) lst1 = self.t.sidereal_time(kind, None, model1) lst2 = self.t.sidereal_time(kind, None, model2) assert np.all(lst1.value != lst2.value) assert within_1_second(lst1.value, lst2.value) @pytest.mark.parametrize( ("kind", "other"), (("mean", "apparent"), ("apparent", "mean")) ) def test_wrong_models_raise_exceptions(self, kind, other): with pytest.raises(ValueError): self.t.sidereal_time(kind, "greenwich", "nonsense") for model in set(SIDEREAL_TIME_MODELS[other].keys()) - set( SIDEREAL_TIME_MODELS[kind].keys() ): with pytest.raises(ValueError): self.t.sidereal_time(kind, "greenwich", model) with pytest.raises(ValueError): self.t.sidereal_time(kind, None, model)