#!/urs/bin/env python
#
# Copyright (C) 2018, the ixpeobssim team.
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Module for time conversions and related utilities.
"""
from __future__ import print_function, division
import time
import datetime
from matplotlib.dates import date2num
# pylint: disable=invalid-name
# The mission start date and time.
MISSION_START_DATETIME = datetime.datetime(2017, 1, 1)
# The Unix time of the mission start (January 1, 2017).
# This is shamelessly taken from http://www.unixtimestamp.com
MISSION_START_UNIX_TIME = 1483228800
# Modified Julian Date at the mission start (January 1, 2017).
# Taken from http://www.csgnetwork.com/julianmodifdateconv.html
MISSION_START_MJD = 57754
# Fractional part of the reference MJD (32.184 secs + 37 leap secs).
MISSION_START_MJDREFF = 8.0074074074e-04
# Default datetime format string.
DATETIME_FMT = '%Y-%m-%dT%H:%M:%S.%f'
# Launch date.
LAUNCH_DATETIME = '2021-12-09T06:00:00.0'
[docs]
class xTimeInterval:
"""Small convenience class to encapsulate the very concept of a time interval.
This was added after https://bitbucket.org/ixpesw/ixpeobssim/issues/417
in an attempt to avoid code duplications wherever we have objects (e.g., GTIs,
observation epochs or calibration runs) that have a start and a stop time.
Note that all the times are assumed to be in MET.
"""
def __init__(self, start_met, stop_met):
"""Constructor.
"""
if stop_met <= start_met:
raise ValueError
self.start_met = start_met
self.stop_met = stop_met
[docs]
def bounds(self):
"""Return the bounds of the epoch in the form of a two-element tuple (start, stop).
"""
return (self.start_met, self.stop_met)
@property
def duration(self):
"""Return the total duration of the time interval in seconds.
"""
return self.stop_met - self.start_met
def __str__(self):
"""String formatting.
"""
return '%.3f--%.3f (%.3f s)' % (self.start_met, self.stop_met, self.duration)
[docs]
class UTCTimezone(datetime.tzinfo):
"""Derived tzinfo object to support the UTC timezone with Python 2.
See https://docs.python.org/2/library/datetime.html#tzinfo-objects
for more details.
"""
__ZERO = datetime.timedelta(0)
[docs]
def utcoffset(self, dt):
return self.__ZERO
[docs]
def dst(self, dt):
return self.__ZERO
[docs]
def tzname(self, dt):
return 'UTC'
"""UTC tzinfo object.
"""
try:
# In Python 3 we have native support for UTC tzinfo.
UTC_TZINFO = datetime.timezone.utc
except AttributeError:
# Hack for Python 2: use the custom tzinfo derived class defined above.
UTC_TZINFO = UTCTimezone()
[docs]
def unix_to_met(ut):
"""Convert a Unix time to a MET.
Args
----
ut : float
The input Unix time.
Returns
-------
float
The mission elapsed time corresponding to the input Unix time.
"""
return ut - MISSION_START_UNIX_TIME
[docs]
def met_to_unix(met):
"""Convert a MET to a Unix time.
Args
----
met : float
The input mission elapsed time.
Returns
-------
float
The Unix time corresponding to the input mission elapsed time.
"""
return met + MISSION_START_UNIX_TIME
def _datetime(ut, tzinfo=UTC_TZINFO):
"""Convenience "private" function to convert a Unix time into a
datetime object.
Args
----
ut : float
The input Unix time.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
Returns
-------
datetime.datetime instance
The date and time corresponding to the input Unix time.
"""
return datetime.datetime.fromtimestamp(ut, tzinfo)
[docs]
def unix_to_string(ut, tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Convert a Unix time to a string expressing time and date.
Args
----
ut : float
The input Unix time.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return _datetime(ut, tzinfo).strftime(fmt)
[docs]
def unix_to_string_utc(ut, fmt=DATETIME_FMT):
"""Convert a Unix time to a string representing UTC date and time.
Args
----
ut : float
The input Unix time.
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return unix_to_string(ut, UTC_TZINFO, fmt)
[docs]
def unix_to_string_local(ut, fmt=DATETIME_FMT):
"""Convert a Unix time to a string representing local date and time.
Args
----
ut : float
The input Unix time.
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return unix_to_string(ut, None, fmt)
[docs]
def met_to_string(met, tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Convert a MET to a string expressing time and date.
Args
----
met : float
The input mission elapsed time.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return unix_to_string(met_to_unix(met), tzinfo, fmt)
[docs]
def met_to_string_utc(met, fmt=DATETIME_FMT):
"""Convert a MET to a string representing UTC date and time.
Args
----
met : float
The input mission elapsed time.
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return met_to_string(met, UTC_TZINFO, fmt)
[docs]
def met_to_string_local(met, fmt=DATETIME_FMT):
"""Convert a MET to a string representing local date and time.
Args
----
met : float
The input mission elapsed time.
fmt : string
The format for the output string
Returns
-------
string
The string corresponding to the input time
"""
return met_to_string(met, None, fmt)
def _datetime_to_timestamp(dt, tzinfo):
"""Python 2 emulation of datetime.datetime.timestamp() method.
This is used with Python 2x in the _timestamp() convenience function below.
See https://stackoverflow.com/questions/19801727 and
https://hg.python.org/cpython/file/3.3/Lib/datetime.py#l1428 for
more information.
"""
if tzinfo is None:
return time.mktime(dt.timetuple()) + dt.microsecond / 1.e6
_epoch = datetime.datetime(1970, 1, 1, tzinfo=tzinfo)
return (dt - _epoch).total_seconds()
def _timestamp(string, tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Convenience "private" function to convert a string representing a
date and time into a Unix time.
Args
----
string : string
The input datetime string.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The Unix time corresponding to the input string.
"""
# Create a naive datetime object corresponding to the input string.
dt = datetime.datetime.strptime(string, fmt)
# If necessary set the timezone information (note that datetime objects
# are immutable, so we need to create a new copy, here.)
if dt is not None:
dt = dt.replace(tzinfo=tzinfo)
# And, finally, convert into a Unix time.
try:
# Python 3.
return dt.timestamp()
except AttributeError:
# Hack for supporting Python 2.
return _datetime_to_timestamp(dt, tzinfo)
def _format_datetime_lazy(string):
"""Convenience string-processing function to adapt a string expressing a
date correctly in our %Y-%m-%dT%H:%M:%S.%f format, adding the missing
fields, if any.
"""
if not 'T' in string:
# This is a pure date---add all the missing fields.
return '%sT00:00:00.0' % string
if '.' in string:
# We assume that this is a well-formed, complete datetime string.
return string
# If we are here it means that we have a partially formed datetime and we
# need to fill in the blanks.
fields = string.split('T')[1].split(':')
assert len(fields) > 0
return '%s%s.0' % (string, ':00' * (3 - len(fields)))
[docs]
def string_to_unix(string, tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Convert a string expressing time and date to a Unix time.
Args
----
string : string
The input datetime string.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The Unix time corresponding to the input string.
"""
return _timestamp(string, tzinfo, fmt=fmt)
[docs]
def string_to_unix_utc(string, fmt=DATETIME_FMT):
"""Convert a string expressing a UTC time and date to a Unix time.
Args
----
string : string
The input datetime string.
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The Unix time corresponding to the input string.
"""
return _timestamp(string, UTC_TZINFO, fmt=fmt)
[docs]
def string_to_unix_local(string, fmt=DATETIME_FMT):
"""Convert a string expressing a local time and date to a Unix time.
Args
----
string : string
The input datetime string.
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The Unix time corresponding to the input string.
"""
return _timestamp(string, None, fmt=fmt)
[docs]
def string_to_met(string, tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Convert a string expressing time and date to a MET.
Args
----
string : string
The input datetime string.
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The mission elapsed time corresponding to the input string.
"""
return unix_to_met(string_to_unix(string, tzinfo, fmt=fmt))
[docs]
def string_to_met_utc(string, lazy=False, fmt=DATETIME_FMT):
"""Convert a string expressing a UTC time and date to a MET.
Args
----
string : string
The input datetime string.
lazy : bool
Flag to attempt and auto-fix partially formed datetime strings.
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The mission elapsed time corresponding to the input string.
"""
if lazy:
string = _format_datetime_lazy(string)
return string_to_met(string, UTC_TZINFO, fmt=fmt)
LAUNCH_MET = string_to_met_utc(LAUNCH_DATETIME)
[docs]
def string_to_met_local(string, fmt=DATETIME_FMT):
"""Convert a string expressing a local time and date to a MET.
Args
----
string : string
The input datetime string.
fmt : string
An optional format specifier for non standard input string
Returns
-------
float
The mission elapsed time corresponding to the input string.
"""
return string_to_met(string, None, fmt=fmt)
[docs]
def current_time():
"""Return the current unix time.
Returns
-------
float
The current Unix time.
"""
return time.time()
[docs]
def current_met():
"""Return the current mission elapsed time.
Returns
-------
float
The current mission elapsed time.
"""
return unix_to_met(current_time())
[docs]
def current_datetime_string(tzinfo=UTC_TZINFO, fmt=DATETIME_FMT):
"""Return a string with the current date and time.
Args
----
tzinfo : a datetime.timezone instance or None
The timezone info (use None for local time).
Returns
-------
string
The current date and time string.
"""
return unix_to_string(current_time(), tzinfo, fmt)
[docs]
def current_datetime_string_utc(fmt=DATETIME_FMT):
"""Return a string with the current UTC date and time.
Args
----
fmt : string
An optional format specifier for non standard input string
Returns
-------
string
The current UTC date and time string.
"""
return unix_to_string(current_time(), UTC_TZINFO, fmt)
[docs]
def current_datetime_string_local(fmt=DATETIME_FMT):
"""Return a string with the current UTC date and time.
Args
----
fmt : string
An optional format specifier for non standard input string
Returns
-------
string
The current local date and time string.
"""
return unix_to_string(current_time(), None, fmt)
__SECONDS_IN_DAY = 86400.
__SECONDS_IN_YEAR = 31557600.
[docs]
def seconds_to_days(seconds):
"""Convert seconds to days.
"""
return seconds / __SECONDS_IN_DAY
[docs]
def days_to_seconds(days):
"""Convert days to seconds.
"""
return days * __SECONDS_IN_DAY
[docs]
def seconds_to_years(seconds):
"""Convert seconds to days.
"""
return seconds / __SECONDS_IN_YEAR
[docs]
def years_to_seconds(years):
"""Convert years to seconds.
"""
return years * __SECONDS_IN_YEAR
[docs]
def met_to_mjd(met):
"""Convert a MET in the corresponding Modified Julian date.
Args
----
met : float
The mission elapsed time.
Returns
-------
float
The Modified Julian Date.
"""
return MISSION_START_MJD + seconds_to_days(met)
[docs]
def mjd_to_met(mjd):
"""Convert a MJD in the corresponding Mission Elapsed Time.
Args
----
mjd : float
The time ref to Modified Julian Day.
Returns
-------
float
The mission elapsed time.
"""
met = mjd - MISSION_START_MJD - MISSION_START_MJDREFF
return days_to_seconds(met)
MJD_OFFSET = 2400000.5
[docs]
def met_to_jd(met):
"""Convert a MET in the corresponding Julian date.
Args
----
met : float
The mission elapsed time.
Returns
-------
float
The Julian Date.
"""
return met_to_mjd(met) + MJD_OFFSET
MET_TO_NUM_OFFSET = date2num(MISSION_START_DATETIME)
[docs]
def met_to_num(met):
"""Convenience conversion factor to turn a MET into a number that matplotlib
can interpret natively as a datetime.
According to https://matplotlib.org/3.1.1/api/dates_api.html
matplotlib represents dates using floating point numbers specifying the
number of days since 0001-01-01 UTC, plus 1.
"""
return MET_TO_NUM_OFFSET + seconds_to_days(met)