Source code for gbm.time

# time.py: Module containing time-related functions
#
#     Authors: William Cleveland (USRA),
#              Adam Goldstein (USRA) and
#              Daniel Kocevski (NASA)
#
#     Portions of the code are Copyright 2020 William Cleveland and
#     Adam Goldstein, Universities Space Research Association
#     All rights reserved.
#
#     Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
#
#     This program is free software: you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation, either version 3 of the License, or
#     (at your option) any later version.
#
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

import datetime
import warnings

from astropy.time import Time
from astropy.time.formats import TimeFromEpoch


[docs]class TimeFermiSec(TimeFromEpoch): """Represents the number of seconds elapsed since Jan 1, 2001 00:00:00 UTC including leap seconds""" name = 'fermi' unit = 1.0 / 86400 # in days (1 day == 86400 seconds) epoch_val = '2001-01-01 00:01:04.184' epoch_val2 = None epoch_scale = 'tt' # Scale for epoch_val class attribute epoch_format = 'iso' # Format for epoch_val class attribute
def round_half_to_nearest_even(num): n = int(num) v = abs(num - n) if v > 0.5 or (v == 0.5 and n % 2): return n + 1 - 2 * int(n < 0) else: return n
[docs]def hms_to_fraction_of_day(value): """The fraction of day as computed by the original ops code. Args: value (:class:`datetime.datetime`): The date/time Returns: float: The fraction of day """ result = round_half_to_nearest_even((( value.hour * 3600 + value.minute * 60 + value.second) / 86400) * 1000) return min(result, 999)
[docs]def fraction_of_day_to_hms(value): """The hour, minute, second for a given fraction of day Args: value (float): The fraction of day Returns: (int, int, int): Hour, minute, second """ s = int((value / 1000) * 86400) h = s // 3600 s -= h * 3600 m = s // 60 s -= m * 60 return h, m, s
[docs]class Met: """Class representing the Fermi MET Epoch and allowing time conversions to and from it. Parameters: secs (float): The MET Attributes: bn (str): The MET converted to bust number format: 'YYMMDDfff' datetime (:class:`datetime.datetime`): A datetime object for the MET gps (float): The number of seconds since Jan 6, 1980 00:00:00 (leap seconds are removed) jd (float): The Julian Date associated with the MET met (float): The MET mjd (float): The modified Julian Date associated with the MET time (:class:`astropy.time.Time`): The astropy time object for the MET unix (float): The number of seconds since Jan 1, 1970 00:00:00 with the leap seconds removed ymd (str): The MET converted to the form `YYMMDD` in UTC ymd_h (str): The MET converted to the form of YYMMDD_HHz in UTC """ # Mission Elapsed Time (Number of seconds since 2001-01-01 00:00:00 UTC) def __init__(self, secs): """Creates a Met object with the time set to the number of seconds since Jan 1, 2001 00:00:00 UTC including the leap seconds""" if secs < 0: warnings.warn("Time before GBM mission epoch") # raise Exception("Time before GBM mission epoch") self.__time = Time(secs, format='fermi')
[docs] @classmethod def from_iso(cls, str_time): """Create a new Met object from an ISO-format UTC string Args: str_time (str): The ISO string Returns: :class:`Met`: The Met object """ if '.' in str_time: dt = datetime.datetime.strptime(str_time, '%Y-%m-%dT%H:%M:%S.%f') else: dt = datetime.datetime.strptime(str_time, '%Y-%m-%dT%H:%M:%S') return cls.from_datetime(dt)
@property def met(self): return self.__time.fermi # Astropy Time @property def time(self): return self.__time
[docs] @classmethod def from_time(cls, atime): """Creates a new Met object from an astropy.Time object Args: atime (:class:`astropy.time.Time`): The astropy time object Returns: :class:`Met`: The Met object """ obj = cls(0) obj.__time = atime if obj.met < 0: raise Exception("Time before GBM mission epoch") return obj
# Python's datetime @property def datetime(self): try: return self.__time.utc.to_datetime(datetime.timezone.utc) except ValueError: # Repeat last met for a leap second return Met(self.met - 1).datetime
[docs] @classmethod def from_datetime(cls, dt): """Creates a new Met object from a datetime.datetime object Args: dt (:class:`datetime.datetime`): The datetime object Returns: :class:`Met`: The Met object """ return cls.from_time(Time(dt, format='datetime'))
# Unix timestamp (Number of seconds since 1970-01-01 00:00:00 UTC (leap seconds are ignored)) @property def unix(self): return self.datetime.timestamp()
[docs] @classmethod def from_unix(cls, unix): """Creates a new Met object from a Unix timestamp Args: unix (float): A Unix time Returns: :class:`Met`: The Met object """ return cls.from_datetime(datetime.datetime.utcfromtimestamp(unix))
# GPS timestamp (Number of seconds since Jan 6, 1980 00:00:00 UTC (leap seconds are ignored)) @property def gps(self): return self.__time.gps
[docs] @classmethod def from_gps(cls, gps): """Creates a new Met object from a GPS timestamp Args: gsp (float): A GPS time Returns: :class:`Met`: The Met object """ return cls.from_time(Time(gps, format='gps'))
# Julian date @property def jd(self): return self.__time.jd
[docs] @classmethod def from_jd(cls, jd): """Creates a new Met object from a Julian Date Args: jd (float): A Julian Date Returns: :class:`Met`: The Met object """ return cls.from_time(Time(jd, format='jd'))
# Modified Julian Date @property def mjd(self): return self.__time.utc.mjd
[docs] @classmethod def from_mjd(cls, mjd): """Creates a new Met object from a Modified Julian Date Args: mjd (float): A Modified Julian Date Returns: :class:`Met`: The Met object """ return cls.from_time(Time(mjd, format='mjd'))
# GBM Burst Number (YYMMDDFFF) @property def bn(self): # Adjust to match a known bug in the old pipeline software adj_met = self.met if 157766399.0 < adj_met < 252460801.0: adj_met += 1 elif 252460800.0 < adj_met <= 253497600.0: adj_met += 2 # To ensure compatibility with the number produced by the pipeline, we are doing it the inefficient way m = Met(adj_met) utc_val = m.datetime fraction = hms_to_fraction_of_day(utc_val) return "{}{:03d}".format(utc_val.strftime("%y%m%d"), fraction)
[docs] @classmethod def from_bn(cls, bn): """Creates a new Met object from a 'YYMMDDfff' string Args: bn (str): A burst number string Returns: :class:`Met`: The Met object """ dt = datetime.datetime.strptime(bn[:6], '%y%m%d') hms = fraction_of_day_to_hms(int(bn[6:])) dt = datetime.datetime(dt.year, dt.month, dt.day, hms[0], hms[1], hms[2], tzinfo=datetime.timezone.utc) obj = cls.from_datetime(dt) # Adjust to match a known bug in the old pipeline software adj_met = obj.met if 157766400.0 < adj_met < 252460802.0: adj_met -= 1 elif 252460802.0 < adj_met <= 253497602.0: adj_met -= 2 return Met(adj_met)
# Year, Month, and Day as YYMMDD @property def ymd(self): return self.datetime.strftime("%y%m%d")
[docs] @classmethod def from_ymd(cls, ymd): """Creates a new Met object from a 'YYMMDD' string Args: ymd (str): A YYMMDD string Returns: :class:`Met`: The Met object """ dt = datetime.datetime.strptime(ymd, '%y%m%d') return cls.from_datetime(dt)
# Year, Month, Day, and Hour as YYMMDD_HH @property def ymd_h(self): return self.datetime.strftime("%y%m%d_%Hz")
[docs] @classmethod def from_ymd_h(cls, ymd): """Creates a new Met object from a 'YYMMDD_HHz' string Args: ymd (str): A YYMMDD_HHz string Returns: :class:`Met`: The Met object """ dt = datetime.datetime.strptime(ymd, '%y%m%d_%Hz') return cls.from_datetime(dt)
# Current time
[docs] @classmethod def now(cls): """Creates a new Met object from the current time Returns: :class:`Met`: The Met object """ m = cls(0) m.__time = Time.now() return m
# String functions
[docs] def iso(self): """Returns the MET value as a string in the form of yyyy-mm-ddTHH:MM:SS in UT Returns: :str: the ISO string """ return self.datetime.strftime("%Y-%m-%dT%H:%M:%S")
def __repr__(self): """Returns a string representation of the Met object""" return "<Met seconds = {:.6f}>".format(self.met) # Math functions
[docs] def add(self, x): """Returns an Met object with its value set to this object's value with x seconds added to it. Can also use the ``+`` operator. Args: x (float): seconds to add Returns: :class:`Met`: The Met object """ if not (isinstance(x, int) or isinstance(x, float)): raise ValueError("Can only add int or float to Met") return Met(self.met + x)
[docs] def sub(self, x): """Returns an Met object with its value set to this object's value with x seconds subtracted from it. Can also use the ``-`` operator. Args: x (float): seconds to subtract Returns: :class:`Met`: The Met object """ if isinstance(x, Met): return self.met - x.met elif isinstance(x, int) or isinstance(x, float): return Met(self.met - x) raise ValueError("Can only subtract int, float or Met from Met")
# Overriding built-in operators def __add__(self, other): return self.add(other) def __sub__(self, other): return self.sub(other) def __lt__(self, other): if isinstance(other, Met): return self.met < other.met else: raise TypeError( "'<' not supported between instances of 'Met' and '{}'".format( type(other))) def __le__(self, other): if isinstance(other, Met): return self.met <= other.met else: raise TypeError( "'<=' not supported between instances of 'Met' and '{}'".format( type(other))) def __gt__(self, other): if isinstance(other, Met): return self.met > other.met else: raise TypeError( "'>' not supported between instances of 'Met' and '{}'".format( type(other))) def __ge__(self, other): if isinstance(other, Met): return self.met >= other.met else: raise TypeError( "'>=' not supported between instances of 'Met' and '{}'".format( type(other))) def __eq__(self, other): if isinstance(other, Met): return self.met == other.met else: raise TypeError( "'==' not supported between instances of 'Met' and '{}'".format( type(other))) def __ne__(self, other): if isinstance(other, Met): return self.met != other.met else: raise TypeError( "'!=' not supported between instances of 'Met' and '{}'".format( type(other)))
# Some time related functions
[docs]def inclusive_date_range(start, stop, step=datetime.timedelta(days=1)): """Creates a list of Met from start to stop times Args: start (:class:`Met`): The start MET stop (:class:`Met`): The end MET step (:class:`datetime.timedelta, optional): The step size. Default is 1 day. Returns: list of :class:`Met`: The list of Met objects """ d = start result = [] if start <= stop: earliest, latest = start, stop else: earliest, latest = stop, start while earliest <= d <= latest: result.append(d) d += step return result
[docs]def dates_range_from(num_days, dt=datetime.datetime.utcnow().date()): """Creates a list of dates within the given range Args: num_days (int): Number of days to include in the list dt (:class:`datetime.date`, optional): The last date to be included in the list. Default is current date. Returns: list: List of date values representing hours. """ d = dt - datetime.timedelta(days=num_days - 1) return inclusive_date_range(d, dt)
[docs]def hours_range_from(num_hours, dt=datetime.datetime.utcnow()): """Creates a list of datetimes within the given range Args: num_hours (int): Number of hours to include in the list dt (:class:`datetime.datetime`, optional): The last hour to be included in the list (datetime will be truncated to hour value). Default is current hour. Returns: list: List of datetime values representing hours. """ d = datetime.datetime(dt.year, dt.month, dt.day, dt.hour, 0, 0) d -= datetime.timedelta(hours=num_hours - 1) return inclusive_date_range(d, dt, datetime.timedelta(hours=1))
[docs]def dates_from_hours(hours): """Converts a list of hours to a list of days spanned Args: hours (list of :class:`datetime.date`): List of hours Returns: list: The list of dates """ return inclusive_date_range(hours[0].date(), hours[-1].date())