#!/usr/bin/env python
u"""
time.py
Written by Tyler Sutterley (05/2022)
Utilities for calculating time operations
PYTHON DEPENDENCIES:
numpy: Scientific Computing Tools For Python
https://numpy.org
dateutil: powerful extensions to datetime
https://dateutil.readthedocs.io/en/stable/
lxml: processing XML and HTML in Python
https://pypi.python.org/pypi/lxml
PROGRAM DEPENDENCIES:
utilities.py: download and management utilities for syncing files
UPDATE HISTORY:
Updated 05/2022: changed keyword arguments to camel case
Updated 04/2022: updated docstrings to numpy documentation format
Updated 04/2021: updated NIST ftp server url for leap-seconds.list
Updated 03/2021: replaced numpy bool/int to prevent deprecation warnings
Updated 02/2021: NASA CDDIS anonymous ftp access discontinued
Updated 01/2021: added ftp connection checks
add date parser for cases when only a calendar date with no units
Updated 12/2020: merged with convert_julian and convert_calendar_decimal
added calendar_days routine to get number of days per month
Updated 09/2020: added wrapper function for merging Bulletin-A files
can parse date strings in form "time-units since yyyy-mm-dd hh:mm:ss"
Updated 08/2020: added NASA Earthdata routines for downloading from CDDIS
Written 07/2020
"""
import re
import datetime
import numpy as np
import ATM1b_QFIT.utilities
# PURPOSE: convert times from seconds since epoch1 to time since epoch2
[docs]def convert_delta_time(delta_time, epoch1=None, epoch2=None, scale=1.0):
"""
Convert delta time from seconds since ``epoch1`` to time since ``epoch2``
Parameters
----------
delta_time: float
seconds since epoch1
epoch1: tuple or NoneType, default None
epoch for input delta_time
epoch2: tuple or NoneType, default None
epoch for output delta_time
scale: float, default 1.0
scaling factor for converting time to output units
"""
epoch1 = datetime.datetime(*epoch1)
epoch2 = datetime.datetime(*epoch2)
delta_time_epochs = (epoch2 - epoch1).total_seconds()
# subtract difference in time and rescale to output units
return scale*(delta_time - delta_time_epochs)
# PURPOSE: calculate the delta time from calendar date
# http://scienceworld.wolfram.com/astronomy/JulianDate.html
[docs]def convert_calendar_dates(year, month, day, hour=0.0, minute=0.0, second=0.0,
epoch=(1992,1,1,0,0,0), scale=1.0):
"""
Calculate the time in time units since ``epoch`` from calendar dates
Parameters
----------
year: float
calendar year
month: float
month of the year
day: float
day of the month
hour: float, default 0.0
hour of the day
minute: float, default 0.0
minute of the hour
second: float, default 0.0
second of the minute
epoch: tuple, default (1992,1,1,0,0,0)
epoch for output delta_time
scale: float, default 1.0
scaling factor for converting time to output units
Returns
-------
delta_time: float
days since epoch
"""
# calculate date in Modified Julian Days (MJD) from calendar date
# MJD: days since November 17, 1858 (1858-11-17T00:00:00)
MJD = 367.0*year - np.floor(7.0*(year + np.floor((month+9.0)/12.0))/4.0) - \
np.floor(3.0*(np.floor((year + (month - 9.0)/7.0)/100.0) + 1.0)/4.0) + \
np.floor(275.0*month/9.0) + day + hour/24.0 + minute/1440.0 + \
second/86400.0 + 1721028.5 - 2400000.5
epoch1 = datetime.datetime(1858,11,17,0,0,0)
epoch2 = datetime.datetime(*epoch)
delta_time_epochs = (epoch2 - epoch1).total_seconds()
# return the date in days since epoch
return scale*np.array(MJD - delta_time_epochs/86400.0,dtype=np.float64)
# PURPOSE: Count number of leap seconds that have passed for each GPS time
[docs]def count_leap_seconds(GPS_Time, truncate=True):
"""
Counts the number of leap seconds between a given GPS time and UTC
Parameters
----------
GPS_Time: float
seconds since January 6, 1980 at 00:00:00
truncate: bool, default True
Reduce list of leap seconds to positive GPS times
Returns
-------
n_leaps: float
number of elapsed leap seconds
"""
# get the valid leap seconds
leaps = get_leap_seconds(truncate=truncate)
# number of leap seconds prior to GPS_Time
n_leaps = np.zeros_like(GPS_Time,dtype=np.float64)
for i,leap in enumerate(leaps):
count = np.count_nonzero(GPS_Time >= leap)
if (count > 0):
indices = np.nonzero(GPS_Time >= leap)
n_leaps[indices] += 1.0
# return the number of leap seconds for converting to UTC
return n_leaps
# PURPOSE: Define GPS leap seconds
[docs]def get_leap_seconds(truncate=True):
"""
Gets a list of GPS times for when leap seconds occurred
Parameters
----------
truncate: bool, default True
Reduce list of leap seconds to positive GPS times
Returns
-------
GPS time: float
GPS seconds when leap seconds occurred
"""
leap_secs = ATM1b_QFIT.utilities.get_data_path(['data','leap-seconds.list'])
# find line with file expiration as delta time
with open(leap_secs,'r') as fid:
secs, = [re.findall(r'\d+',i).pop() for i in fid.read().splitlines()
if re.match(r'^(?=#@)',i)]
# check that leap seconds file is still valid
expiry = datetime.datetime(1900,1,1) + datetime.timedelta(seconds=int(secs))
today = datetime.datetime.now()
update_leap_seconds() if (expiry < today) else None
# get leap seconds
leap_UTC,TAI_UTC = np.loadtxt(ATM1b_QFIT.utilities.get_data_path(leap_secs)).T
# TAI time is ahead of GPS by 19 seconds
TAI_GPS = 19.0
# convert leap second epochs from NTP to GPS
# convert from time of 2nd leap second to time of 1st leap second
leap_GPS = convert_delta_time(leap_UTC+TAI_UTC-TAI_GPS-1,
epoch1=(1900,1,1,0,0,0), epoch2=(1980,1,6,0,0,0))
# return the GPS times of leap second occurance
if truncate:
return leap_GPS[leap_GPS >= 0].astype(np.float64)
else:
return leap_GPS.astype(np.float64)
# PURPOSE: connects to servers and downloads leap second files
[docs]def update_leap_seconds(timeout=20, verbose=False, mode=0o775):
"""
Connects to servers to download leap-seconds.list files from NIST servers
- https://www.nist.gov/pml/time-and-frequency-division/leap-seconds-faqs
Servers and Mirrors
- ftp://ftp.nist.gov/pub/time/leap-seconds.list
- https://www.ietf.org/timezones/data/leap-seconds.list
Parameters
----------
timeout: int, default 20
timeout in seconds for blocking operations
verbose: bool, default False
print file information about output file
mode: oct, default 0o775
permissions mode of output file
"""
# local version of file
FILE = 'leap-seconds.list'
LOCAL = ATM1b_QFIT.utilities.get_data_path(['data',FILE])
HASH = ATM1b_QFIT.utilities.get_hash(LOCAL)
# try downloading from NIST ftp servers
HOST = ['ftp.nist.gov','pub','time',FILE]
try:
ATM1b_QFIT.utilities.check_ftp_connection(HOST[0])
ATM1b_QFIT.utilities.from_ftp(HOST, timeout=timeout, local=LOCAL,
hash=HASH, verbose=verbose, mode=mode)
except:
pass
else:
return
# try downloading from Internet Engineering Task Force (IETF) mirror
REMOTE = ['https://www.ietf.org','timezones','data',FILE]
try:
ATM1b_QFIT.utilities.from_http(REMOTE, timeout=timeout, local=LOCAL,
hash=HASH, verbose=verbose, mode=mode)
except:
pass
else:
return