# ALMA - Atacama Large Millimeter Array
# (c) European Southern Observatory, 2024
# (c) Associated Universities Inc., 2024
# Copyright by ESO (in the framework of the ALMA collaboration),
# Copyright by AUI (in the framework of the ALMA collaboration),
# All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307 USA
#
# File ArrayTime.py
#
import math
from .Interval import Interval
import pyasdm.utils
[docs]class ArrayTime(Interval):
"""
The ArrayTime class implements the concept of a point in time, implemented
as an Interval of time since 17 November 1858 00:00:00 UTC, the beginning of the
modified Julian Day.
<p>
All dates are assumed to be in the Gregorian calendar, including those
prior to October 15, 1582. So, if you are interested in very old dates,
this isn't the most convenient class to use.
<p>
Internally the time is kept in units of nanoseconds (10<sup>-9</sup> seconds).
The base time is 17 November 1858 00:00:00 UTC, and the maximum time is to the
year 2151 (2151-02-25T23:47:16.854775807). This differs from the OMG Time service
The OMG time is in units of 100 nanoseconds using the beginning of the Gregorian
calandar,15 October 1582 00:00:00 UTC, as the base time.
The reason for this increased accuracy is that the Control system is capable of
measuring time to an accuracy of 40 nanoseconds. Therefore, by adhering to the
representation of time used in the OMG Time Serivce we would be losing precision.
<p>
The Time class is an extension of the Interval class, since all times
are intervals since 17 November 1858 00:00:00 UTC.
<p>
All times in this class are assumed to be International
Atomic Time (TAI). A specific TAI time differs from the corresponding
UTC time by an offset that is an integral number of seconds.
<p>
In the methods that give various quantities associated with
calendar times, this class does not apply any UTC corrections.
Therefore, if you use these methods to produce calendar times, the
results will differ from civil time by a few seconds. The classes
UTCTime and LocalTime take the UTC and timezone corrections into
account.
<p>
The main reference used in crafting these methods is
Astronomical Algorithms by Jean Meeus, second edition,
2000, Willmann-Bell, Inc., ISBN 0-943396-61-1. See
chapter 7, "Julian day", and chapter 12, "Sidereal Time".
<p>
This version adapted from the c++ and java implementations, originally authored by Allen Farris.
"""
# multiple constructors
# no arguments: 0 nanoseconds, start of Julian day, 17 November 1858 00:00:00 UTC
# single string argument:
# FITS formatted string: "YYYY-MM-DDThh:mm:ss.ssss" where "T" may be replaced by a space
# double precision float, which must include a decimal point: modified julian date
# a single integer : integer number of nanoseconds since 15 October 1582 00:00:00 UTC
# ArrayTime (the copy constructor)
# a float : modified julain date
# integer : nanoseconds since the beginning of modified julian date
# year, month, day : integers except day may be expressed as a float
# year, month, day, hour, minute, second : integers exccept day may be expressed as a float
# mjd, second : integer modified julian day and seconds within that day (may be a float)
def __init__(self, *args):
# initialize it to 0
super().__init__()
if len(args) > 0:
if len(args) == 1:
thisArg = args[0]
if isinstance(thisArg, ArrayTime):
# another ArrayTime
self.set(thisArg.get())
elif isinstance(thisArg, str):
value = 0
if ":" in thisArg:
# FITS string
value = self.FITSString(thisArg)
elif "." in thisArg:
# string encoded double as modified Julian day
value = self.mjdToUnit(float(thisArg))
else:
# string encoded integer as nanoseconds
value = int(thisArg)
# value should be nanoseconds here
self.set(int(value))
elif isinstance(thisArg, float):
# modified Julian day
self.set(self.mjdToUnit(float(thisArg)))
elif isinstance(thisArg, int):
# integer nanoseconds
self.set(thisArg)
else:
raise ValueError("invalid argument value")
elif len(args) == 2:
# mjd, seconds
modifiedJulianDay = int(args[0])
secondsInADay = float(args[1])
self.set(
modifiedJulainDay * 8640000000000 + int(secondsInADay * 100000000.0)
)
elif len(args) == 3:
# year, month, day
self.initFloatDay(int(args[0]), int(args[1]), float(args[2]))
elif len(args) == 6:
# year, month, day, hour, minute, second
year = int(args[0])
month = int(args[1])
day = int(args[2])
hour = int(args[3])
minute = int(args[4])
second = float(args[5])
if (
hour < 0
or hour > 23
or minute < 0
or minute >> 59
or second < 0.0
or second >= 60.0
):
raise ValueError("invalid hour, minute or second value")
self.init(year, month, day, hour, minute, second)
else:
raise ValueError("invalid ArrayTime constructor used")
# constants used in conversions
_numberSigDigitsInASecond = 9
_unitsInASecond = 1000000000
_unitsInADayL = 86400000000000
_unitsInADayD = 86400000000000.0
_unitsInADayDiv100 = 864000000000.0
_julianDayOfBase = 2400000.5
_julianDayOfBaseInUnitsInADayDiv100 = 2073600432000000000
[docs] def getJD(self, mjd=None):
"""
When no argument is used, return the value of this ArrayTime as a Julian day,
otherwise convert the given Modified Julain day to the corresponding Julian day
@param mjd The Julian day, otherwise use the value of this ArrayTime
@returns The Modified Julain day
"""
if mjd is None:
return self.unitToJD(self.get())
# convert the argument to MJD
return mjd + 2400000.5
[docs] def getMJD(self, jd=None):
"""
When no argument is used, return the value of this ArrayTime as a Modified Julian day,
otherwise convert the given Julain day to the corresponding Modified Julian day
@param mjd The Julian day, otherwise use the value of this ArrayTime
@returns The Modified Julain day
"""
if jd is None:
return self.unitToMJD(self.get())
# convert the argumentto MJD
return jd - 2400000.5
[docs] def toFITS(self):
"""
Return this Time as a FITS formatted string, which is of the
form 'YYYY-MM-DDThh:mm:ss.ssss'
"""
yy, mm, dd, hh, min, sec, frac = self.getDateTime()
s = str(yy)
s = s + "-"
if mm < 10:
s = s + "0"
s = s + str(mm)
s = s + "-"
if dd < 10:
s = s + "0"
s = s + str(dd)
s = s + "T"
if hh < 10:
s = s + "0"
s = s + str(hh)
s = s + ":"
if min < 10:
s = s + "0"
s = s + str(min)
s = s + ":"
if sec < 10:
s = s + "0"
s = s + str(sec)
# apply fractions of a second
fracStr = str(frac)
s = s + "."
# The statement below is sensitive to the number of significant
# digits in a fraction. If units are nanoseconds,
# then we will have 9 significant digits in a fraction
# string.
tmp = "0000000000000000"
s = s + tmp[0 : self._numberSigDigitsInASecond - len(fracStr)]
s = s + fracStr
return s
[docs] def getDateTime(self):
"""
Return this time as a tuple of integers representing (in order):
year,
month (varies from 1 to 12),
day (varies from 1 to 28, 29, 30, or 31),
hour (varies from 0 to 23),
minute (varies from 0 to 59),
second (varies from 0 to 59), and
the number of nanoseconds that remain in this fraction of a second.
"""
fractionOfADay = int(self.get() % self._unitsInADayL)
if fractionOfADay < 0:
fractionOfADay = self._unitsInADayL - fractionOfADay
nsec = int(fractionOfADay / self._unitsInASecond)
frac = fractionOfADay - nsec * self._unitsInASecond
nmin = int(nsec / 60)
second = nsec - nmin * 60
hour = int(nmin / 60)
minute = nmin - hour * 60
jd = self.unitToJD(self.get())
# For this algorithm see Meeus, chapter 7, p. 63.
x = jd + 0.5 # Make the 12h UT adjustment.
Z = int(x)
F = x - Z
A = Z
alpha = 0
if Z >= 2299161:
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) + F
month = (E-1) if (E<14) else (E-13)
year = (C-4716) if (month>2) else (C-4715)
day = int(day)
month = month
year = year
return (year, month, day, hour, minute, second, frac)
[docs] def getTimeOfDay(self):
"""
Return the time of day in hours and fractions thereof.
"""
x = self.unitToJD(self.get()) + 0.5
return (x - int(x)) * 24.0
[docs] def getDayOfWeek(self):
"""
Return the day number of the week of this time.
Day numbers start from 0-Sunday
"""
return (int(self.unitToJD(self.get()) + 1.5)) % 7
[docs] def getDayOfYear(self):
"""
Return the day number of year of this time.
"""
dateTuple = self.getDateTime()
year = dateTuple[0]
month = dateTuple[1]
day = dateTuple[2]
leapYearMult = 1 if self.isLeapYear(year) else 2
# watch for auto promotion of integer division to floats in python
return (
(int((275 * month) / 9)) - (leapYearMult * int((month + 9) / 12)) + day - 30
)
[docs] def timeOfDayToString(self):
"""
Return the time of day as a string in the form "hh:mm:ss
"""
dateTuple = self.getDateTime()
hour = dateTuple[3]
minute = dateTuple[4]
sec = dateTuple[5]
s = ""
if hour < 10:
s = s + "0"
s = s + str(hour) + ":"
if minute < 10:
s = s + "0"
s = s + str(minute) + ":"
if sec < 10:
s = s + "0"
s = s + str(sec)
return s
[docs] def getLocalSiderealTime(self, longitudeInHours):
"""
Return the local sidereal time for this time
in hours and fractions of an hour at the specified longitude
"""
return self.getGreenwichMeanSiderealTime() - longitudeInHours
[docs] def getGreenwichMeanSiderealTime(self):
"""
Return the Greenwich mean sidereal time for this time
in hours and fractions of an hour.
"""
jd = self.unitToJD(self.get())
t0 = jd - 2451545.0
t = t0 / 36525.0
tt = t * t
x = (
280.46061837 + 360.98564736629 * t0 + tt * (0.000387933 - (t / 38710000.0))
) / 15.0
y = math.fmod(x, 24.0)
if y < 0:
y = 24.0 + y
return y
[docs] def initFloatDay(self, year, month, day):
"""
Initialize this time as appropriate for year, month, and day
"""
# For this algorithm see Meeus, chapter 7, p. 61.
iday = int(day)
# check for valid month
if month < 1 or month > 12:
raise ValueError(
"Illegal value of month: " + year + "-" + month + "-" + day
)
# check for valid day
if (iday < 1 or iday > 31) or ((month == 4 or month == 6 or month == 9 or month == 11) and iday > 30) or (month == 2 and (iday > (29 if self.isLeapYear(year) else 28))):
raise ValueError("Illegal value of day: " + year + "-" + month + "-" + day)
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)) + iday + B - 1524.5
)
u = self.jdToUnit(jd)
# Now add the fraction of a day.
u += int(((day - iday) * self._unitsInADayD + 0.5))
self.set(u)
return u
[docs] def init(self, year, month, day, hour, minute, second):
if (
hour < 0
or hour > 23
or minute < 0
or minute > 59
or second < 0.0
or second >= 60.0
):
raise ValueError("Invalid time: " + hour + ":" + minute + ":" + second)
return self.initFloatDay(
year, month, (day + (((((second / 60.0) + minute) / 60.0) + hour) / 24.0))
)
[docs] def FITSString(self, t):
"""
Return a unit of time, as a long, from a FITS-formatted string that
specifies the time. The format must be of the form:
YYYY-MM-DDThh:mm:ss.ssss
Leading zeros are required if months, days, hours, minutes, or seconds
are single digits. The value for months ranges from "01" to "12".
The "T" separting the data and time values is optional. If the "T" is
not present, then a space MUST be present.
A ValueError is raised if the string is not a valid
time.
"""
if (
len(t) < 19
or t[4] != "-"
or t[7] != "-"
or (t[10] != "T" and t[10] != " ")
or t[13] != ":"
or t[16] != ":"
):
raise ValueError("Invalid time format: " + t)
yyyy = 0
mm = 0
dd = 0
hh = 0
min = 0
sec = 0.0
try:
yyyy = int(t[0:4])
mm = int(t[5:7])
dd = int(t[8:10])
hh = int(t[11:13])
min = int(t[14:16])
sec = float(t[17:])
except TypeError as exc:
raise TypeError("Invalid time format: " + t)
return self.init(yyyy, mm, dd, hh, min, sec)
[docs] def unitToJD(self, unit):
"""
Convert a unit of time in units since the base time to a Julian day.
@param unit The unit to be converted.
@return The Julian day corresponding to the specified unit of time.
"""
return (1.0 * unit / self._unitsInADayD) + self._julianDayOfBase
[docs] def unitToMJD(self, unit):
"""
Convert a unit of time in units since the base time to a Modified Julian day.
@param unit The unit to be converted.
@return The Modified Julian day corresponding to the specified unit of time.
"""
return 1.0 * unit / self._unitsInADayD
[docs] def jdToUnit(self, jd):
"""
Convert a Julian day to a unit of time in tens of nanoseconds
since 15 October 1582 00:00:00 UTC.
@param jd The Julian day to be converted.
@return The unit corresponding to the specified Julian day.
"""
return (
int(jd * self._unitsInADayDiv100) - self._julianDayOfBaseInUnitsInADayDiv100
) * 100
[docs] def mjdToUnit(self, mjd):
"""
Convert a Modified Julian day to units since the base time.
@param mjd The Modified Julian day to be converted.
@return The unit corresponding to the specified Modified Julian day.
"""
return int(mjd * self._unitsInADayD)
[docs] @staticmethod
def isLeapYear(year):
"""
Return true if the specified year is a leap year.
@param year the year in the Gregorian calendar.
@return true if the specified year is a leap year.
"""
if year % 4 != 0:
return False
if year % 100 == 0 and year % 400 != 0:
return False
return True
[docs] @staticmethod
def add(time, interval):
"""
Generate a new ArrayTime by adding an Interval
to the specified ArrayTime.
@param time an ArrayTime
@param interval The interval to be added to the time.
@return A new ArrayTime formed by adding an Interval
to the specified ArrayTime.
"""
t = ArrayTime(time)
t.add(interval)
return t
[docs] @staticmethod
def sub(time, interval):
"""
Generate a new ArrayTime by subtracting an Interval
from the specified ArrayTime.
@param time an ArrayTime
@param interval The interval to be subtracted from the time.
@return A new ArrayTime formed by subtracting an Interval
from the specified ArrayTime.
"""
t = ArrayTime(time)
t.sub(interval)
return t
[docs] @staticmethod
def getInstance(stringList):
"""
Retrieve a value from a list of strings and convert that to an ArrayTime.
This is used when parsing ArrayTime lists from an XML representation to
eventually construct a list of ArrayTime instances. The values are expected
to be integers representing nanoseconds.
Returns a tuple of (ArrayTime, stringList) where ArrayTime is the new ArrayTime
created by this call and stringList is the remaining, unused, part of
stringList after removing the first element.
"""
if not isinstance(stringList, list):
raise ValueError("stringList is not a list")
# this will raise an error if there aren't any elements on stringList
intVal = int(stringList[0])
return (ArrayTime(intVal), stringList[1:])
[docs] def toBin(self, eos):
"""
Write this ArrayTime as a long in nanoseconds to an EndianOutput instance.
"""
eos.writeLong(self.get())
[docs] @staticmethod
def listToBin(arrayTimeList, eos):
"""
Write a list of ArrayTime to the EndianOutput.
The list may have 1, 2 or 3 dimensions.
"""
if not isinstance(arrayTimeList, list):
raise ValueError("arrayTimeList is not a list")
# this is used to determine the number of dimensions
listDims = pyasdm.utils.getListDims(arrayTimeList)
ndims = len(listDims)
if ndims == 1:
ArrayTime.listTo1DBin(arrayTimeList, eos)
elif ndims == 2:
ArrayTime.listTo2DBin(arrayTimeList, eos)
elif ndims == 3:
ArrayTime.listTo3DBin(arrayTimeList, eos)
else:
raise ValueError(
"unsupport number of dimensions in arrayTimeList in ArrayTime.listToBin : "
+ str(ndims)
)
[docs] @staticmethod
def listTo1DBin(atList, eos):
"""
Write a 1D list of ArrayTime to the EndianOutput instance.
"""
if not isinstance(atList, list):
raise ValueError("atList is not a list")
# ndim is always written, even for 0-element lists
eos.writeInt(len(atList))
# only check the first value
if (len(atList) > 0) and not isinstance(atList[0], ArrayTime):
raise (ValueError("atList is not a list off ArrayTime"))
for thisAt in atList:
thisAt.toBin(eos)
[docs] @staticmethod
def listTo2DBin(atList, eos):
"""
Write a 2D list of ArrayTime to the EndianOutput instance.
"""
if not isinstance(atList, list):
raise ValueError("atList is not a list")
ndim1 = len(atList)
ndim2 = 0
if ndim1 > 0:
# only check the first value in the outer list
if not isinstance(atList[0], list):
raise ValueError("atList is not a 2D list")
ndim2 = len(atList[0])
if ndim2 > 0 and not isinstance(atList[0][0], ArrayTime):
raise ValueError("atList is not a 2D list of ArrayTime")
# ndims are always written, even for 0-element lists
eos.writeInt(ndim1)
eos.writeInt(ndim2)
for thisList in atList:
for thisAt in thisList:
thisAt.toBin(eos)
[docs] @staticmethod
def listTo3DBin(atList, eos):
"""
Write a 3D list of ArrayTime to the EndianOutput instance.
"""
if not isinstance(atList, list):
raise ValueError("atList is not a list")
ndim1 = len(atList)
ndim2 = 0
ndim3 = 0
if ndim1 > 0:
# only check the first value in the outer list
if not isinstance(atList[0], list):
raise ValueError("atList is not a 3D list")
ndim2 = len(atList[0])
if ndim2 > 0:
if not isinstance(atList[0][0], list):
raise ValueError("atList is not a 3D list")
ndim3 = len(atList[0][0])
if ndim3 > 0 and not isinstance(atList[0][0][0], ArrayTime):
raise ValueError("atList is not a 3D list of ArrayTime")
# ndims are always written, even for 0-element lists
eos.writeInt(ndim1)
eos.writeInt(ndim2)
eos.writeInt(ndim3)
for thisList in atList:
for thisMidList in thisList:
for thisAt in thisMidList:
thisAt.toBin(eos)
[docs] @staticmethod
def fromBin(eis):
"""
Read the binary representation of an ArrayTime
from an EndianInput instance and use the read value to set an
ArrayTime in nanoseconds.
return an ArrayTime
"""
return ArrayTime(eis.readLongLong())
[docs] @staticmethod
def from1Bin(eis):
"""
Read the binary representation of 1D list of ArrayTime
from an EndianInput instance
return a 1D list of ArrayTime
"""
result = []
ndim = eis.readInt()
for i in range(ndim):
result.append(ArrayTime.fromBin(eis))
return result
[docs] @staticmethod
def from2Bin(eis):
"""
Read the binary representation of 2D list of ArrayTime
from an EndianInput instance
return a 2D list of ArrayTime
"""
result = []
ndim1 = eis.readInt()
ndim2 = eis.readInt()
for i in range(ndim1):
innerList = []
for j in range(ndim2):
innerList.append(ArrayTime.fromBin(eis))
result.append(innerList)
return result
[docs] @staticmethod
def from3Bin(eis):
"""
Read the binary representation of 3D list of ArrayTime
from an EndianInput instance
return a 3D list of ArrayTime
"""
result = []
ndim1 = eis.readInt()
ndim2 = eis.readInt()
ndim3 = eis.readInt()
for i in range(ndim1):
middleList = []
for j in range(ndim2):
innerList = []
for k in range(ndim3):
innerList.append(ArrayTime.fromBin(eis))
middleList.append(innerList)
result.append(middleList)
return result
# utcCorrection not implemented here, the table of leap seconds by JD has not
# been updated in some time and does not appear to be used anywhere