"""
The `mjd` module provides time conversion routines to and from Modified Julian Date. It provides convenient
functions which convert scalars, arrays or sequences of various time representations (strings, datetimes and numpy datetime64 into floats) into
corresponding scalars or arrays of modified julian date. The code uses the time functions native to python and makes
no attempt to implements IERS (Earth Rotation) details such as leap seconds etc. The following functions are provided,
- `utc_to_mjd`. Converts scalars, arrays and sequences of various time representations to floating point MJD
- `mjd_to_utc`, Converts scalars, arrays and sequences of floating point MJD to numpy.datetime64
- `mjd_to_datetime64`, converts scalar mjd to numpy.datetime64
- `mjd_to_datetime`, converts scalar mjd to datetime.datetime
- `datetime64_to_datetime`, converts numpy.datetime64 to datetime.datetime
"""
from typing import Union, List, Tuple
from datetime import datetime, timedelta
from collections.abc import Sequence
import numbers
import numpy as np
import math
import time
import string
import re
from astropy.time import Time
_g_mjd0str = '1858-11-17 00:00:00.000000'
_t0_64 = np.datetime64(_g_mjd0str)
_t0 = datetime.strptime(_g_mjd0str, '%Y-%m-%d %H:%M:%S.%f')
# ------------------------------------------------------------------------------
# datetime64_to_mjd
# ------------------------------------------------------------------------------
def datetime64_to_mjd(utc: np.datetime64) -> float:
"""
Converts a numpy datetime64 to a floating point MJD. Only scalar values are supported
by this function.
Parameters
----------
utc : numpy.datetime64
A time specified with a numpy datetime64 object. Explicit time zones are
not currently supported. Only single scalar values are supported in this function.
Returns
-------
float
The corresponding modified julian date.
"""
mjd = (utc - _t0_64).astype('timedelta64[us]') / np.timedelta64(1, 'us') / 86400000000.0
return mjd
# ------------------------------------------------------------------------------
# mjd_to_datetime64
# ------------------------------------------------------------------------------
[docs]
def mjd_to_datetime64(mjd: float) -> np.datetime64:
"""
Converts an modified julian date to a numpy.datetime64. This function only works
on scalar values of mjd.
Parameters
----------
mjd : float
The modified julian date.
Returns
-------
numpy.datetime64
The numpy.datetime64 corresponding to the modified julian date
"""
dt = np.timedelta64(int(mjd * 86400000000.0 + 0.5), 'us')
utc = (_t0_64 + dt).astype('datetime64[us]')
return utc
# ------------------------------------------------------------------------------
# mjd_to_datetime64
# ------------------------------------------------------------------------------
[docs]
def mjd_to_datetime(mjd: float) -> datetime:
"""
Converts a modified julian date to a datetime.datetime object. This function only works
on scalar values of mjd. Time zones are not set.
Parameters
----------
mjd : float
The modified julian date.
Returns
-------
datetime.datetime
The datetime.datetime corresponding to the modified julian date. No time zone is set
"""
dt = timedelta(days=mjd)
utc = _t0 + dt
return utc
# ------------------------------------------------------------------------------
# datetime_to_mjd
# ------------------------------------------------------------------------------
def datetime_to_mjd(t: datetime) -> float:
"""
Converts a python datetime object to a floating point MJD. Only scalar values are supported
by this function.
Parameters
----------
utc : datetime.datetime
A time specified with a regular python datetime.datetime object. Explicit time zones are
not currently supported. Only single scalar values are supported in this function.
Returns
-------
float
The corresponding modified julian date.
"""
dt = t - _t0 # from datetime
mjd = float(dt.days) + (float(dt.seconds) + float(dt.microseconds) * 1.0E-06) / 86400.0 # to mjd as a floating point
return mjd
# ------------------------------------------------------------------------------
# mjd_to_utc
# ------------------------------------------------------------------------------
[docs]
def mjd_to_ut(mjd):
"""
A convenience function that converts scalars or arrays of floating point modified julian dates to universal time representations.
The universal time is always returned as a scalar or array of numpy.datetime64 object(s).
Parameters
----------
mjd : scalar, array, or sequence of float
The input can be a scalar, numpy array or regular python list/tuple of floats storing MJD values.
Returns
-------
numpy.datetime64 or numpy.ndarray< numpy.datetime64 >
Returns either a scalar or array of numpy.datetime64 that matches the input object
"""
if (type(mjd) is np.ndarray) or (isinstance(mjd, Sequence)):
if type(mjd) is np.ndarray:
utc = np.zeros_like(mjd, dtype='datetime64[us]')
with np.nditer([mjd, utc],
flags=['buffered'], # create the nupy iterator object
op_flags=[['readonly'], ['writeonly']]) as it:
for a, b in it: # and convert each elemnet
b[...] = mjd_to_datetime64(a) # from datetime to mjd
utc = it.operands[1]
else:
utc = np.zeros([len(mjd)])
for i in range(len(mjd)):
utc[i] = mjd_to_datetime64(mjd[i])
else:
utc = mjd_to_datetime64(mjd)
return utc
# ------------------------------------------------------------------------------
# datetime64_to_mjd
# ------------------------------------------------------------------------------
[docs]
def ut_to_mjd(utc):
"""
A convenience function that converts various arrays or scalar representations of UT to modified julian date.
Scalar input values will return as a scalar float modified julian date while array, list or tuple input values are all returned
as numpy arrays of mjd in float64.
Parameters
----------
utc : scalar, list, tuple or array
The input time which represents a coordinated universal time. It can be represented by
(i) a string in a supported python datetime.isoformat
(ii) a number. The number is assumed to represent a modified julian date.
(iii) a datetime.datetime object.
(iv) a numpy.datetime64 object.
The *utc* object can be a scalar, list, tuple or numpy array. The same representation format must be used for
all objects in the arrays and sequences.
Returns
-------
scalar or numpy.ndarray of float
The mjd is returned as a scalar if the input was a scalar or as a numpy array for input sequences and arrays
"""
if type(utc) is np.ndarray:
# Handle an numpy array of MJD values, datetime64, datetime objects or mjd numbers
if utc.dtype.kind == 'M': # if we have a numpy array of datetime64 object
mjd = datetime64_to_mjd(utc) # then convert it to floating point MJD
elif (utc.dtype.kind == 'O') and (len(utc) > 0) and (type(utc[0]) is datetime): # otherwiseif we have a numpy array of datetime objects
mjd = np.zeros_like(utc, dtype=np.float64) # create the output array
with np.nditer([utc, mjd],
flags=['refs_ok', 'buffered'], # create the nupy iterator object
op_flags=[['readonly'], ['writeonly']]) as it:
for a, b in it: # and convert each elemnet
t = datetime_to_mjd(a) # from datetime to mjd
b[...] = t
mjd = it.operands[1]
elif (utc.dtype.kind == 'U'): # otherwise if the array is strings
mjd = np.zeros_like(utc, dtype=np.float64) # then make an array to hold the answer
with np.nditer([utc, mjd],
flags=['refs_ok', 'buffered'],
op_flags=[['readonly'], ['writeonly']]) as it:
for a, b in it: # For each element in the array
t = datetime.fromisoformat(str(a)) # convert the string to datetime
b[...] = datetime_to_mjd(t) # and from datetime to mjd
mjd = it.operands[1]
else: # otherwise we have an array of something else, I assume its array of numbers
mjd = np.array(utc, dtype=np.float64) # so try and convert the array of numbers to floating point MJD.
elif type(utc) is str: # if we have a single string
t = datetime.fromisoformat(utc) # then convert the string to datetime
mjd = datetime_to_mjd(t) # and from datetime to mjd
elif isinstance(utc, Sequence): # if we have a list or tuple but not numpy array
newutc = np.array(utc) # then convert the array to numpy array
mjd = ut_to_mjd(newutc) # and convert the numpy array to mjd
else: # Its not a numpy array, its not a sequence, so assume its a scalar
if (type(utc) is datetime): # if its a datetime object
mjd = datetime_to_mjd(utc) # to mjd as a floating point
elif type(utc) is np.datetime64: # if we have a single numpy.datetime64
mjd = float(datetime64_to_mjd(utc)) # then convert it
elif isinstance(utc, numbers.Number): # if we have a number
mjd = float(utc) # then assume its already an MJD
else: # otherwise
mjd = math.nan
raise Exception('utc_to_mjd, unsupported data type {}. Cannot convert to mjd'.format(str(type(utc))))
return mjd
# ------------------------------------------------------------------------------
# utc_to_astropy
# ------------------------------------------------------------------------------
[docs]
def utc_to_astropy(utc):
"""
A convenience function that converts list, arrays or scalar representations of UTC to an astropy Time object.
The function is a shallow wrapper for strings, datetime and datetime64 arrays as numpy arrays of mjd in float64.
Parameters
----------
utc : scalar, list, tuple or array
The input time which represents a coordinated universal time. It can be represented by
(i) a string in a supported python datetime.isoformat
(ii) a number. The number is assumed to represent a modified julian date.
(iii) a datetime.datetime object.
(iv) a numpy.datetime64 object.
The *utc* object can be a scalar, list, tuple or numpy array. The same representation format must be used for
all objects in the arrays and sequences.
Returns
-------
scalar or numpy.ndarray of float
The mjd is returned as a scalar if the input was a scalar or as a numpy array for input sequences and arrays
"""
formattype = None
if (type(utc) is np.ndarray) and utc.dtype.kind in ['i', 'u', 'f']:
formattype = 'mjd'
elif isinstance(utc, Sequence):
if (isinstance(utc[0], numbers.Number)):
formattype = 'mjd'
elif isinstance(utc, numbers.Number):
formattype = 'mjd'
t = Time(utc, format=formattype, scale='utc') if formattype is not None else Time(utc, scale='utc')
return t
# ---------------------------------------------------------------------------
# This is an old class from the early OSIRIS days in 2001. Its needs to be
# updated before it is used
# ----------------------------------------------------------------------------
class MJD:
def __init__(self, mjd=0.0):
self.m_mjd = mjd
def from_date(self, year, month, day):
self.from_utc(year, month, day, 0, 0, 0)
def from_utc(self, year, month, day, hour, minutes, seconds):
tupl = [int(year), int(month), int(day), int(hour), int(minutes), int(seconds), 0, 0, -1]
secs = time.mktime(tupl) - time.timezone
self.FromUnixSeconds(secs)
def from_str(self, utcstr):
pattern = "[%s%s]+" % (string.punctuation, string.whitespace) # Get all of the puctuation and whitespace charcaters
s = re.split(pattern, utcstr) # split the string with white space and punctuation
t = [0, 1, 1, 0, 0, 0] # get default values for fields not suplied by user
n = min(len(s), 6) # make sure we dont use more than 6 fields
for i in range(n):
t[i] = int(s[i]) # now fill in the values supplied by user
self.from_utc(t[0], t[1], t[2], t[3], t[4], t[5]) # convert those times to UTC
def as_unix_seconds(self):
secs = (self.m_mjd - 40587.0) * 86400.0
if (secs < 0):
secs = 0
return secs
def AsTimeTuple(self):
return time.gmtime(self.as_unix_seconds())
def FromUnixSeconds(self, secs):
self.m_mjd = secs / 86400.0 + 40587.0
def SetTo(self, mjd):
self.m_mjd = mjd
def FromSystem(self):
self.FromUnixSeconds(time.time())
def MJD(self):
return self.m_mjd
def as_utc_str(self, doticks=0):
secs = self.as_unix_seconds() + 0.0005 # Round the seconds to the nearest millisecond
tupl = time.gmtime(secs) # Now get the time using the system call
utcstr = time.strftime("%Y-%m-%d %H:%M:%S", tupl) # Encode as a string
if (doticks): # add the milliseconds if required
ticks = int((secs - int(secs)) * 1000.0)
utcstr = "%s.%03d" % (utcstr, ticks)
return utcstr
def AsDateStr(self):
secs = self.as_unix_seconds() + 0.0005 # Round the seconds to the nearest millisecond
tupl = time.gmtime(secs) # Now get the time using the system call
datestr = time.strftime("%Y-%m-%d", tupl) # Encode as a string
return datestr
def AsLocalTimeStr(self, formatstr="%H:%M"):
secs = self.as_unix_seconds()
tupl = time.localtime(secs)
str = time.strftime(formatstr, tupl)
return str
def __add__(self, other):
self.m_mjd = self.m_mjd + float(other)
return self
def __sub__(self, other):
self.mjd = self.m_mjd - float(other)
return self
def __mul__(self, other):
self.m_mjd = self.m_mjd * float(other)
return self
def __div__(self, other):
self.m_mjd = self.m_mjd / float(other)
return self
def __neg__(self):
self.m_mjd = -self.m_mjd
return self
def __pos__(self):
return self
def __abs__(self):
self.m_mjd = abs(self.m_mjd)
return self
def __int__(self):
return int(self.m_mjd)
def __long__(self):
return int(self.m_mjd)
def __float__(self):
return float(self.m_mjd)
def __str__(self):
return self.as_utc_str(1)