Source code for pytcl.astronomical.time_systems

"""
Time system conversions for astronomical and tracking applications.

This module provides conversions between various time systems:
- UTC (Coordinated Universal Time)
- TAI (International Atomic Time)
- TT (Terrestrial Time)
- GPS (GPS Time)
- Julian Date (JD) and Modified Julian Date (MJD)
- Unix time
- Sidereal time (GMST, GAST)
"""

import warnings
from typing import List, Tuple

# Constants
JD_UNIX_EPOCH = 2440587.5  # Julian date of Unix epoch (1970-01-01 00:00:00 UTC)
JD_GPS_EPOCH = 2444244.5  # Julian date of GPS epoch (1980-01-06 00:00:00 UTC)
JD_J2000 = 2451545.0  # Julian date of J2000.0 epoch (2000-01-01 12:00:00 TT)
MJD_OFFSET = 2400000.5  # JD - MJD offset

# TAI-UTC offset at GPS epoch (1980-01-06)
TAI_UTC_AT_GPS_EPOCH = 19  # seconds

# TT-TAI offset (constant)
TT_TAI_OFFSET = 32.184  # seconds


[docs] class LeapSecondTable: """ Table of leap seconds for UTC-TAI conversion. The leap second table contains the dates when leap seconds were added and the cumulative TAI-UTC offset. Attributes ---------- entries : list of tuple List of (year, month, day, tai_utc_offset) tuples. Notes ----- This table must be updated when new leap seconds are announced. Last update: 2017-01-01 (37 seconds). No leap second has been added since 2017; the table is current as of the last library release. """
[docs] def __init__(self) -> None: # Leap second table: (year, month, day, TAI-UTC offset in seconds) # From IERS Bulletin C self.entries: List[Tuple[int, int, int, int]] = [ (1972, 1, 1, 10), (1972, 7, 1, 11), (1973, 1, 1, 12), (1974, 1, 1, 13), (1975, 1, 1, 14), (1976, 1, 1, 15), (1977, 1, 1, 16), (1978, 1, 1, 17), (1979, 1, 1, 18), (1980, 1, 1, 19), (1981, 7, 1, 20), (1982, 7, 1, 21), (1983, 7, 1, 22), (1985, 7, 1, 23), (1988, 1, 1, 24), (1990, 1, 1, 25), (1991, 1, 1, 26), (1992, 7, 1, 27), (1993, 7, 1, 28), (1994, 7, 1, 29), (1996, 1, 1, 30), (1997, 7, 1, 31), (1999, 1, 1, 32), (2006, 1, 1, 33), (2009, 1, 1, 34), (2012, 7, 1, 35), (2015, 7, 1, 36), (2017, 1, 1, 37), ]
[docs] def get_offset(self, year: int, month: int, day: int) -> int: """ Get TAI-UTC offset for a given date. Parameters ---------- year : int Year. month : int Month (1-12). day : int Day of month. Returns ------- int TAI-UTC offset in seconds. Warns ----- UserWarning If the date precedes 1972-01-01, the table's first entry. The returned offset is `0`, not the true (drifting, sub-integer) TAI-UTC value for that era. """ offset = 0 for y, m, d, o in self.entries: if (year, month, day) >= (y, m, d): offset = o else: break if (year, month, day) < self.entries[0][:3]: # stacklevel=3 targets get_leap_seconds's caller, not # get_leap_seconds itself -- that's the documented public # entry point, so its callers should see their own line, not # a line inside this module. A direct LeapSecondTable(). # get_offset(...) call is consequently attributed one frame # high (to its caller's caller); that's the accepted # tradeoff of one stacklevel serving both paths. warnings.warn( f"UTC before 1972 ({year:04d}-{month:02d}-{day:02d}) predates " "the leap-second table; TAI-UTC was a drifting rubber-second " "offset (~8 s in 1970), not the 0 s returned here. Use " "astropy for pre-1972 epochs.", UserWarning, stacklevel=3, ) return offset
# Global leap second table instance _LEAP_SECOND_TABLE = LeapSecondTable()
[docs] def get_leap_seconds(year: int, month: int, day: int) -> int: """ Get the number of leap seconds (TAI-UTC offset) for a given date. Parameters ---------- year : int Year. month : int Month (1-12). day : int Day of month. Returns ------- int TAI-UTC offset in seconds. Warns ----- UserWarning If the date precedes 1972-01-01, the table's first entry. The returned offset is `0`, not the true (drifting, sub-integer) TAI-UTC value for that era. Examples -------- >>> get_leap_seconds(2020, 1, 1) 37 >>> get_leap_seconds(1980, 1, 6) 19 """ return _LEAP_SECOND_TABLE.get_offset(year, month, day)
[docs] def cal_to_jd( year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: float = 0.0, ) -> float: """ Convert calendar date to Julian Date. Parameters ---------- year : int Year (negative for BCE). month : int Month (1-12). day : int Day of month (1-31). hour : int, optional Hour (0-23). minute : int, optional Minute (0-59). second : float, optional Second (0-59.999...). Returns ------- float Julian Date. Examples -------- >>> cal_to_jd(2000, 1, 1, 12, 0, 0) # J2000.0 2451545.0 >>> cal_to_jd(1970, 1, 1, 0, 0, 0) # Unix epoch 2440587.5 Notes ----- Uses the algorithm from Meeus, "Astronomical Algorithms", 2nd ed. Valid for dates after October 15, 1582 (Gregorian calendar). """ if month <= 2: year -= 1 month += 12 A = int(year / 100) B = 2 - A + int(A / 4) jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + B - 1524.5 # Add time of day jd += (hour + minute / 60.0 + second / 3600.0) / 24.0 return jd
[docs] def jd_to_cal(jd: float) -> Tuple[int, int, int, int, int, float]: """ Convert Julian Date to calendar date. Parameters ---------- jd : float Julian Date. Returns ------- tuple (year, month, day, hour, minute, second). Examples -------- >>> jd_to_cal(2451545.0) # J2000.0 (2000, 1, 1, 12, 0, 0.0) """ jd = jd + 0.5 Z = int(jd) F = jd - Z if Z < 2299161: A = Z else: alpha = int((Z - 1867216.25) / 36524.25) A = Z + 1 + alpha - int(alpha / 4) B = A + 1524 C = int((B - 122.1) / 365.25) D = int(365.25 * C) E = int((B - D) / 30.6001) day = B - D - int(30.6001 * E) if E < 14: month = E - 1 else: month = E - 13 if month > 2: year = C - 4716 else: year = C - 4715 # Convert fractional day to time hours_frac = F * 24.0 hour = int(hours_frac) minutes_frac = (hours_frac - hour) * 60.0 minute = int(minutes_frac) second = (minutes_frac - minute) * 60.0 return year, month, day, hour, minute, second
[docs] def mjd_to_jd(mjd: float) -> float: """ Convert Modified Julian Date to Julian Date. Parameters ---------- mjd : float Modified Julian Date. Returns ------- float Julian Date. Examples -------- >>> mjd = 44239.0 # 1980-01-01 >>> jd = mjd_to_jd(mjd) >>> jd 2444239.5 Notes ----- MJD = JD - 2400000.5 """ return mjd + MJD_OFFSET
[docs] def jd_to_mjd(jd: float) -> float: """ Convert Julian Date to Modified Julian Date. Parameters ---------- jd : float Julian Date. Returns ------- float Modified Julian Date. Examples -------- >>> jd = 2444239.5 # 1980-01-01 >>> mjd = jd_to_mjd(jd) >>> mjd 44239.0 """ return jd - MJD_OFFSET
[docs] def unix_to_jd(unix_time: float) -> float: """ Convert Unix timestamp to Julian Date. Parameters ---------- unix_time : float Unix timestamp (seconds since 1970-01-01 00:00:00 UTC). Returns ------- float Julian Date. Examples -------- >>> unix_to_jd(0.0) # Unix epoch 2440587.5 """ return JD_UNIX_EPOCH + unix_time / 86400.0
[docs] def jd_to_unix(jd: float) -> float: """ Convert Julian Date to Unix timestamp. Parameters ---------- jd : float Julian Date. Returns ------- float Unix timestamp. Examples -------- >>> jd = 2440587.5 # 1970-01-01 00:00:00 UTC >>> unix_to_jd = jd_to_unix(jd) >>> unix_to_jd 0.0 """ return (jd - JD_UNIX_EPOCH) * 86400.0
[docs] def utc_to_tai( year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: float = 0.0, ) -> float: """ Convert UTC to TAI (Julian Date). Parameters ---------- year, month, day : int Calendar date. hour, minute : int, optional Time of day. second : float, optional Seconds (including fractional). Returns ------- float TAI as Julian Date. Notes ----- TAI = UTC + leap_seconds """ leap_seconds = get_leap_seconds(year, month, day) jd_utc = cal_to_jd(year, month, day, hour, minute, second) return jd_utc + leap_seconds / 86400.0
[docs] def tai_to_utc(jd_tai: float) -> Tuple[float, int]: """ Convert TAI (Julian Date) to UTC. Parameters ---------- jd_tai : float TAI as Julian Date. Returns ------- jd_utc : float UTC as Julian Date. leap_seconds : int Number of leap seconds applied. Notes ----- The leap-second lookup is by table, so the conversion is exact except within one second of an insertion. Instants inside a leap second itself -- 23:59:60 on an insertion date -- have no distinct representation here and are attributed to the preceding second (gh-25): TAI 2017-01-01T00:00:36.0 returns UTC 2016-12-31T23:59:59.0, not the following midnight. Because of this, the returned UTC value is non-monotone across an inserted second -- both the leap second and the ordinary second before it map onto the same UTC second, so `jd_utc` briefly steps backward as `jd_tai` increases through the insertion. Sub-second work spanning a leap-second boundary needs a library that models UTC as a discontinuous scale, such as astropy. """ table_start = _LEAP_SECOND_TABLE.entries[0][:3] jd_utc = jd_tai for _ in range(3): year, month, day, _, _, _ = jd_to_cal(jd_utc) leap_seconds = get_leap_seconds(year, month, day) next_utc = jd_tai - leap_seconds / 86400.0 if next_utc == jd_utc: break next_date = jd_to_cal(next_utc)[:3] if (year, month, day) >= table_start and next_date < table_start: # The table's first entry applies to TAI instants whose own # UTC answer reads as a date before the table starts (e.g. TAI # 1972-01-01T00:00:05 -> UTC 1971-12-31T23:59:55, leap=10). # Re-deriving leap seconds from that answer's own date would # look it up as pre-1972 and get 0, then bounce back to the # in-table date next pass -- a permanent two-cycle whose # result depends on the parity of the iteration cap. Accept # this UTC estimate now, using the leap value that produced # it, instead of taking that further step. jd_utc = next_utc break jd_utc = next_utc return jd_utc, leap_seconds
[docs] def tai_to_tt(jd_tai: float) -> float: """ Convert TAI (Julian Date) to TT (Terrestrial Time). Parameters ---------- jd_tai : float TAI as Julian Date. Returns ------- float TT as Julian Date. Notes ----- TT = TAI + 32.184 seconds (constant offset) """ return jd_tai + TT_TAI_OFFSET / 86400.0
[docs] def tt_to_tai(jd_tt: float) -> float: """ Convert TT (Terrestrial Time) to TAI (Julian Date). Parameters ---------- jd_tt : float TT as Julian Date. Returns ------- float TAI as Julian Date. """ return jd_tt - TT_TAI_OFFSET / 86400.0
[docs] def utc_to_tt( year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: float = 0.0, ) -> float: """ Convert UTC to TT (Terrestrial Time). Parameters ---------- year, month, day : int Calendar date. hour, minute : int, optional Time of day. second : float, optional Seconds. Returns ------- float TT as Julian Date. Notes ----- TT = UTC + leap_seconds + 32.184 """ jd_tai = utc_to_tai(year, month, day, hour, minute, second) return tai_to_tt(jd_tai)
[docs] def tt_to_utc(jd_tt: float) -> Tuple[float, int]: """ Convert TT to UTC. Parameters ---------- jd_tt : float TT as Julian Date. Returns ------- jd_utc : float UTC as Julian Date. leap_seconds : int Number of leap seconds applied. """ jd_tai = tt_to_tai(jd_tt) return tai_to_utc(jd_tai)
[docs] def tai_to_gps(jd_tai: float) -> float: """ Convert TAI to GPS time. Parameters ---------- jd_tai : float TAI as Julian Date. Returns ------- float GPS time as Julian Date. Notes ----- GPS time = TAI - 19 seconds (offset at GPS epoch) GPS time does not include leap seconds after 1980. """ return jd_tai - TAI_UTC_AT_GPS_EPOCH / 86400.0
[docs] def gps_to_tai(jd_gps: float) -> float: """ Convert GPS time to TAI. Parameters ---------- jd_gps : float GPS time as Julian Date. Returns ------- float TAI as Julian Date. """ return jd_gps + TAI_UTC_AT_GPS_EPOCH / 86400.0
[docs] def utc_to_gps( year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: float = 0.0, ) -> float: """ Convert UTC to GPS time. Parameters ---------- year, month, day : int Calendar date. hour, minute : int, optional Time of day. second : float, optional Seconds. Returns ------- float GPS time as Julian Date. """ jd_tai = utc_to_tai(year, month, day, hour, minute, second) return tai_to_gps(jd_tai)
[docs] def gps_to_utc(jd_gps: float) -> Tuple[float, int]: """ Convert GPS time to UTC. Parameters ---------- jd_gps : float GPS time as Julian Date. Returns ------- jd_utc : float UTC as Julian Date. leap_seconds : int Number of leap seconds applied. """ jd_tai = gps_to_tai(jd_gps) return tai_to_utc(jd_tai)
[docs] def gps_week_seconds(jd_gps: float) -> Tuple[int, float]: """ Convert GPS Julian Date to GPS week and seconds of week. Parameters ---------- jd_gps : float GPS time as Julian Date. Returns ------- week : int GPS week number. seconds : float Seconds into the week. Examples -------- >>> gps_week_seconds(JD_GPS_EPOCH) (0, 0.0) """ days_since_epoch = jd_gps - JD_GPS_EPOCH week = int(days_since_epoch / 7) day_of_week = days_since_epoch - week * 7 seconds = day_of_week * 86400.0 return week, seconds
[docs] def gps_week_to_utc(week: int, seconds: float) -> Tuple[float, int]: """ Convert GPS week and seconds to UTC. Parameters ---------- week : int GPS week number. seconds : float Seconds into the week. Returns ------- jd_utc : float UTC as Julian Date. leap_seconds : int Number of leap seconds applied. """ jd_gps = JD_GPS_EPOCH + week * 7 + seconds / 86400.0 return gps_to_utc(jd_gps)
__all__ = [ # Julian dates "cal_to_jd", "jd_to_cal", "mjd_to_jd", "jd_to_mjd", # Time scales "utc_to_tai", "tai_to_utc", "tai_to_tt", "tt_to_tai", "utc_to_tt", "tt_to_utc", "tai_to_gps", "gps_to_tai", "utc_to_gps", "gps_to_utc", # Unix time "unix_to_jd", "jd_to_unix", # GPS week "gps_week_seconds", "gps_week_to_utc", # Leap seconds "get_leap_seconds", "LeapSecondTable", # Constants "JD_UNIX_EPOCH", "JD_GPS_EPOCH", "JD_J2000", "MJD_OFFSET", "TT_TAI_OFFSET", ]