2287 lines
76 KiB
Python
2287 lines
76 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# PyMeeus: Python module implementing astronomical algorithms.
|
|
# Copyright (C) 2018 Dagoberto Salazar
|
|
#
|
|
# This program 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 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 Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
import calendar
|
|
import datetime
|
|
from math import radians, cos, sin, asin, sqrt, acos, degrees
|
|
|
|
from pymeeus.base import TOL, get_ordinal_suffix, iint
|
|
from pymeeus.Angle import Angle
|
|
|
|
|
|
"""
|
|
.. module:: Epoch
|
|
:synopsis: Class to handle time
|
|
:license: GNU Lesser General Public License v3 (LGPLv3)
|
|
|
|
.. moduleauthor:: Dagoberto Salazar
|
|
"""
|
|
|
|
|
|
DAY2SEC = 86400.0
|
|
"""Number of seconds per day"""
|
|
|
|
DAY2MIN = 1440.0
|
|
"""Number of minutes per day"""
|
|
|
|
DAY2HOURS = 24.0
|
|
"""Number of hours per day"""
|
|
|
|
LEAP_TABLE = {
|
|
1972.5: 1,
|
|
1973.0: 2,
|
|
1974.0: 3,
|
|
1975.0: 4,
|
|
1976.0: 5,
|
|
1977.0: 6,
|
|
1978.0: 7,
|
|
1979.0: 8,
|
|
1980.0: 9,
|
|
1981.5: 10,
|
|
1982.5: 11,
|
|
1983.5: 12,
|
|
1985.5: 13,
|
|
1988.0: 14,
|
|
1990.0: 15,
|
|
1991.0: 16,
|
|
1992.5: 17,
|
|
1993.5: 18,
|
|
1994.5: 19,
|
|
1996.0: 20,
|
|
1997.5: 21,
|
|
1999.0: 22,
|
|
2006.0: 23,
|
|
2009.0: 24,
|
|
2012.5: 25,
|
|
2015.5: 26,
|
|
2017.0: 27,
|
|
}
|
|
"""This table represents the point in time FROM WHERE the given number of leap
|
|
seconds is valid. Given that leap seconds are (so far) always added at
|
|
June 30th or December 31st, a leap second added in 1997/06/30 is represented
|
|
here as '1997.5', while a leap second added in 2005/12/31 appears here as
|
|
'2006.0'."""
|
|
|
|
|
|
class Epoch(object):
|
|
"""
|
|
Class Epoch deals with the tasks related to time handling.
|
|
|
|
The constructor takes either a single JDE value, another Epoch object, or a
|
|
series of values representing year, month, day, hours, minutes, seconds.
|
|
This series of values are by default supposed to be in **Terrestial Time**
|
|
(TT).
|
|
|
|
This is not necesarily the truth, though. For instance, the time of a
|
|
current observation is tipically in UTC time (civil time), not in TT, and
|
|
there is some offset between those two time references.
|
|
|
|
When a UTC time is provided, the parameter **utc=True** must be given.
|
|
Then, the input is converted to International Atomic Time (TAI) using an
|
|
internal table of leap seconds, and from there, it is converted to (and
|
|
stored as) Terrestrial Time (TT).
|
|
|
|
Given that leap seconds are added or subtracted in a rather irregular
|
|
basis, it is not possible to predict them in advance, and the internal leap
|
|
seconds table will become outdated at some point in time. To counter this,
|
|
you have two options:
|
|
|
|
- Download an updated version of this Pymeeus package.
|
|
- Use the argument **leap_seconds** in the constructor or :meth:`set`
|
|
method to provide the correct number of leap seconds (w.r.t. TAI) to be
|
|
applied.
|
|
|
|
.. note:: Providing the **leap_seconds** argument will automatically set
|
|
the argument **utc** to True.
|
|
|
|
For instance, if at some time in the future the TAI-UTC difference is 43
|
|
seconds, you should set **leap_seconds=43** if you don't have an updated
|
|
version of this class.
|
|
|
|
In order to know which is the most updated leap second value stored in this
|
|
class, you may use the :meth:`get_last_leap_second()` method.
|
|
|
|
.. note:: The current version of UTC was implemented in January 1st, 1972.
|
|
Therefore, for dates before that date the correction is **NOT** carried
|
|
out, even if the **utc** argument is set to True, and it is supposed
|
|
that the input data is already in TT scale.
|
|
|
|
.. note:: For conversions between TT and Universal Time (UT), please use
|
|
the method :meth:`tt2ut`.
|
|
|
|
.. note:: Internally, time values are stored as a Julian Ephemeris Day
|
|
(JDE), based on the uniform scale of Dynamical Time, or more
|
|
specifically, Terrestial Time (TT) (itself the redefinition of
|
|
Terrestrial Dynamical Time, TDT).
|
|
|
|
.. note:: The UTC-TT conversion is composed of three corrections:
|
|
|
|
a. TT-TAI, comprising 32.184 s,
|
|
b. TAI-UTC(1972), 10 s, and
|
|
c. UTC(1972)-UTC(target)
|
|
|
|
item c. is the corresponding amount of leap seconds to the target Epoch.
|
|
When you do, for instance, **leap_seconds=43**, you modify the c. part.
|
|
|
|
.. note:: Given that this class stores the epoch as JDE, if the JDE value
|
|
is in the order of millions of days then, for a computer with 15-digit
|
|
accuracy, the final time resolution is about 10 milliseconds. That is
|
|
considered enough for most applications of this class.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Epoch constructor.
|
|
|
|
This constructor takes either a single JDE value, another Epoch object,
|
|
or a series of values representing year, month, day, hours, minutes,
|
|
seconds. This series of values are by default supposed to be in
|
|
**Terrestial Time** (TT).
|
|
|
|
It is also possible that the year, month, etc. arguments be provided in
|
|
a tuple or list. Moreover, it is also possible provide :class:`date` or
|
|
:class:`datetime` objects for initialization.
|
|
|
|
The **month** value can be provided as an integer (1 = January, 2 =
|
|
February, etc), or it can be provided with short (Jan, Feb,...) or long
|
|
(January, February,...) names. Also, hours, minutes, seconds can be
|
|
provided separately, or as decimals of the day value.
|
|
|
|
When a UTC time is provided, the parameter **utc=True** must be given.
|
|
Then, the input is converted to International Atomic Time (TAI) using
|
|
an internal table of leap seconds, and from there, it is converted to
|
|
(and stored as) Terrestrial Time (TT). If **utc** is not provided, it
|
|
is supposed that the input data is already in TT scale.
|
|
|
|
If a value is provided with the **leap_seconds** argument, then that
|
|
value will be used for the UTC->TAI conversion, and the internal leap
|
|
seconds table will be bypassed.
|
|
|
|
:param args: Either JDE, Epoch, date, datetime or year, month, day,
|
|
hours, minutes, seconds values, by themselves or inside a tuple or
|
|
list
|
|
:type args: int, float, :py:class:`Epoch`, tuple, list, date,
|
|
datetime
|
|
:param utc: Whether the provided epoch is a civil time (UTC)
|
|
:type utc: bool
|
|
:param leap_seconds: This is the value to be used in the UTC->TAI
|
|
conversion, instead of taking it from internal leap seconds table.
|
|
:type leap_seconds: int, float
|
|
|
|
:returns: Epoch object.
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: ValueError if input values are in the wrong range.
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> e = Epoch(1987, 6, 19.5)
|
|
>>> print(e)
|
|
2446966.0
|
|
"""
|
|
|
|
# Initialize field
|
|
self._jde = 0.0
|
|
self.set(*args, **kwargs) # Use 'set()' method to handle the setup
|
|
|
|
def set(self, *args, **kwargs):
|
|
"""Method used to set the value of this object.
|
|
|
|
This method takes either a single JDE value, or a series of values
|
|
representing year, month, day, hours, minutes, seconds. This series of
|
|
values are by default supposed to be in **Terrestial Time** (TT).
|
|
|
|
It is also possible to provide another Epoch object as input for the
|
|
:meth:`set` method, or the year, month, etc arguments can be provided
|
|
in a tuple or list. Moreover, it is also possible provide :class:`date`
|
|
or :class:`datetime` objects for initialization.
|
|
|
|
The **month** value can be provided as an integer (1 = January, 2 =
|
|
February, etc), or it can be provided as short (Jan, Feb, ...) or long
|
|
(January, February, ...) names. Also, hours, minutes, seconds can be
|
|
provided separately, or as decimals of the day value.
|
|
|
|
When a UTC time is provided, the parameter **utc=True** must be given.
|
|
Then, the input is converted to International Atomic Time (TAI) using
|
|
an internal table of leap seconds, and from there, it is converted to
|
|
(and stored as) Terrestrial Time (TT). If **utc** is not provided, it
|
|
is supposed that the input data is already in TT scale.
|
|
|
|
If a value is provided with the **leap_seconds** argument, then that
|
|
value will be used for the UTC->TAI conversion, and the internal leap
|
|
seconds table will be bypassed.
|
|
|
|
.. note:: The UTC to TT correction is only carried out for dates after
|
|
January 1st, 1972.
|
|
|
|
:param args: Either JDE, Epoch, date, datetime or year, month, day,
|
|
hours, minutes, seconds values, by themselves or inside a tuple or
|
|
list
|
|
:type args: int, float, :py:class:`Epoch`, tuple, list, date,
|
|
datetime
|
|
:param utc: Whether the provided epoch is a civil time (UTC)
|
|
:type utc: bool
|
|
:param leap_seconds: This is the value to be used in the UTC->TAI
|
|
conversion, instead of taking it from internal leap seconds table.
|
|
:type leap_seconds: int, float
|
|
|
|
:returns: None.
|
|
:rtype: None
|
|
:raises: ValueError if input values are in the wrong range.
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> e = Epoch()
|
|
>>> e.set(1987, 6, 19.5)
|
|
>>> print(e)
|
|
2446966.0
|
|
>>> e.set(1977, 'Apr', 26.4)
|
|
>>> print(e)
|
|
2443259.9
|
|
>>> e.set(1957, 'October', 4.81)
|
|
>>> print(e)
|
|
2436116.31
|
|
>>> e.set(333, 'Jan', 27, 12)
|
|
>>> print(e)
|
|
1842713.0
|
|
>>> e.set(1900, 'Jan', 1)
|
|
>>> print(e)
|
|
2415020.5
|
|
>>> e.set(-1001, 'august', 17.9)
|
|
>>> print(e)
|
|
1355671.4
|
|
>>> e.set(-4712, 1, 1.5)
|
|
>>> print(e)
|
|
0.0
|
|
>>> e.set((1600, 12, 31))
|
|
>>> print(e)
|
|
2305812.5
|
|
>>> e.set([1988, 'JUN', 19, 12])
|
|
>>> print(e)
|
|
2447332.0
|
|
>>> d = datetime.date(2000, 1, 1)
|
|
>>> e.set(d)
|
|
>>> print(e)
|
|
2451544.5
|
|
>>> e.set(837, 'Apr', 10, 7, 12)
|
|
>>> print(e)
|
|
2026871.8
|
|
>>> d = datetime.datetime(837, 4, 10, 7, 12, 0, 0)
|
|
>>> e.set(d)
|
|
>>> print(e)
|
|
2026871.8
|
|
"""
|
|
|
|
# Clean up the internal parameters
|
|
self._jde = 0.0
|
|
# If no arguments are given, return. Internal values are 0.0
|
|
if len(args) == 0:
|
|
return
|
|
# If we have only one argument, it can be a JDE or another Epoch object
|
|
elif len(args) == 1:
|
|
if isinstance(args[0], Epoch):
|
|
self._jde = args[0]._jde
|
|
return
|
|
elif isinstance(args[0], (int, float)):
|
|
self._jde = args[0]
|
|
return
|
|
elif isinstance(args[0], (tuple, list)):
|
|
year, month, day, hours, minutes, sec = \
|
|
self._check_values(*args[0])
|
|
elif isinstance(args[0], datetime.datetime):
|
|
d = args[0]
|
|
year, month, day, hours, minutes, sec = self._check_values(
|
|
d.year,
|
|
d.month,
|
|
d.day,
|
|
d.hour,
|
|
d.minute,
|
|
d.second + d.microsecond / 1e6,
|
|
)
|
|
elif isinstance(args[0], datetime.date):
|
|
d = args[0]
|
|
year, month, day, hours, minutes, sec = self._check_values(
|
|
d.year, d.month, d.day
|
|
)
|
|
else:
|
|
raise TypeError("Invalid input type")
|
|
elif len(args) == 2:
|
|
# Insuficient data to set the Epoch
|
|
raise ValueError("Invalid number of input values")
|
|
elif len(args) >= 3: # Year, month, day
|
|
year, month, day, hours, minutes, sec = self._check_values(*args)
|
|
day += hours / DAY2HOURS + minutes / DAY2MIN + sec / DAY2SEC
|
|
# Handle the 'leap_seconds' argument, if pressent
|
|
if "leap_seconds" in kwargs:
|
|
# Compute JDE
|
|
self._jde = self._compute_jde(year, month, day, utc2tt=False,
|
|
leap_seconds=kwargs["leap_seconds"])
|
|
elif "utc" in kwargs:
|
|
self._jde = self._compute_jde(year, month, day,
|
|
utc2tt=kwargs["utc"])
|
|
else:
|
|
self._jde = self._compute_jde(year, month, day, utc2tt=False)
|
|
|
|
def _compute_jde(self, y, m, d, utc2tt=True, leap_seconds=0.0):
|
|
"""Method to compute the Julian Ephemeris Day (JDE).
|
|
|
|
.. note:: The UTC to TT correction is only carried out for dates after
|
|
January 1st, 1972.
|
|
|
|
:param y: Year
|
|
:type y: int
|
|
:param m: Month
|
|
:type m: int
|
|
:param d: Day
|
|
:type d: float
|
|
:param utc2tt: Whether correction UTC to TT is done automatically.
|
|
:type utc2tt: bool
|
|
:param leap_seconds: Number of leap seconds to apply
|
|
:type leap_seconds: float
|
|
|
|
:returns: Julian Ephemeris Day (JDE)
|
|
:rtype: float
|
|
"""
|
|
|
|
# The best approach here is first convert to JDE, and then adjust secs
|
|
if m <= 2:
|
|
y -= 1
|
|
m += 12
|
|
a = iint(y / 100.0)
|
|
b = 0.0
|
|
if not Epoch.is_julian(y, m, iint(d)):
|
|
b = 2.0 - a + iint(a / 4.0)
|
|
jde = (iint(365.25 * (y + 4716.0)) +
|
|
iint(30.6001 * (m + 1.0)) + d + b - 1524.5)
|
|
# If enabled, let's convert from UTC to TT, adding the needed seconds
|
|
deltasec = 0.0
|
|
# In this case, UTC to TT correction is applied automatically
|
|
if utc2tt:
|
|
if y >= 1972:
|
|
deltasec = 32.184 # Difference between TT and TAI
|
|
deltasec += 10.0 # Difference between UTC and TAI in 1972
|
|
deltasec += Epoch.leap_seconds(y, m)
|
|
else: # Correction is NOT automatic
|
|
if leap_seconds != 0.0: # We apply provided leap seconds
|
|
if y >= 1972:
|
|
deltasec = 32.184 # Difference between TT and TAI
|
|
deltasec += 10.0 # Difference between UTC-TAI in 1972
|
|
deltasec += leap_seconds
|
|
return jde + deltasec / DAY2SEC
|
|
|
|
def _check_values(self, *args):
|
|
"""This method takes the input arguments to 'set()' method (year,
|
|
month, day, etc) and carries out some sanity checks on them.
|
|
|
|
It returns a tuple containing those values separately, assigning zeros
|
|
to those arguments which were not provided.
|
|
|
|
:param args: Year, month, day, hours, minutes, seconds values.
|
|
:type args: int, float
|
|
|
|
:returns: Tuple with year, month, day, hours, minutes, seconds values.
|
|
:rtype: tuple
|
|
:raises: ValueError if input values are in the wrong range, or too few
|
|
arguments given as input.
|
|
"""
|
|
|
|
# This list holds the maximum amount of days a given month can have
|
|
maxdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
# Initialize some variables
|
|
year = -9999
|
|
month = -9999
|
|
day = -9999
|
|
hours = 0.0
|
|
minutes = 0.0
|
|
sec = 0.0
|
|
|
|
# Carry out some basic checks
|
|
if len(args) < 3:
|
|
raise ValueError("Invalid number of input values")
|
|
elif len(args) >= 3: # Year, month, day
|
|
year = args[0]
|
|
month = args[1]
|
|
day = args[2]
|
|
if len(args) >= 4: # Year, month, day, hour
|
|
hours = args[3]
|
|
if len(args) >= 5: # Year, month, day, hour, minutes
|
|
minutes = args[4]
|
|
if len(args) >= 6: # Year, month, day, hour, minutes, seconds
|
|
sec = args[5]
|
|
if year < -4712: # No negative JDE will be allowed
|
|
raise ValueError("Invalid value for the input year")
|
|
if day < 1 or day >= 32:
|
|
raise ValueError("Invalid value for the input day")
|
|
if hours < 0 or hours >= 24:
|
|
raise ValueError("Invalid value for the input hours")
|
|
if minutes < 0 or minutes >= 60:
|
|
raise ValueError("Invalid value for the input minutes")
|
|
if sec < 0 or sec >= 60:
|
|
raise ValueError("Invalid value for the input seconds")
|
|
|
|
# Test the days according to the month
|
|
month = Epoch.get_month(month)
|
|
limit_day = maxdays[month - 1]
|
|
# We need extra tests if month is '2' (February)
|
|
if month == 2:
|
|
if Epoch.is_leap(year):
|
|
limit_day = 29
|
|
if day > limit_day:
|
|
raise ValueError("Invalid value for the input day")
|
|
|
|
# We are ready to return the parameters
|
|
return year, month, day, hours, minutes, sec
|
|
|
|
@staticmethod
|
|
def check_input_date(*args, **kwargs):
|
|
"""Method to check that the input is a proper date.
|
|
|
|
This method returns an Epoch object, and the **leap_seconds** argument
|
|
then controls the way the UTC->TT conversion is handled for that new
|
|
object. If **leap_seconds** argument is set to a value different than
|
|
zero, then that value will be used for the UTC->TAI conversion, and the
|
|
internal leap seconds table will be bypassed. On the other hand, if it
|
|
is set to zero, then the UTC to TT correction is disabled, and it is
|
|
supposed that the input data is already in TT scale.
|
|
|
|
:param args: Either Epoch, date, datetime or year, month, day values,
|
|
by themselves or inside a tuple or list
|
|
:type args: int, float, :py:class:`Epoch`, datetime, date, tuple,
|
|
list
|
|
:param leap_seconds: If different from zero, this is the value to be
|
|
used in the UTC->TAI conversion. If equals to zero, conversion is
|
|
disabled. If not given, UTC to TT conversion is carried out
|
|
(default).
|
|
:type leap_seconds: int, float
|
|
|
|
:returns: Epoch object corresponding to the input date
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: ValueError if input values are in the wrong range.
|
|
:raises: TypeError if input values are of wrong type.
|
|
"""
|
|
|
|
t = Epoch()
|
|
if len(args) == 0:
|
|
raise ValueError("Invalid input: No date given")
|
|
# If we have only one argument, it can be an Epoch, a date, a datetime
|
|
# or a tuple/list
|
|
elif len(args) == 1:
|
|
if isinstance(args[0], Epoch):
|
|
t = args[0]
|
|
elif isinstance(args[0], (tuple, list)):
|
|
if len(args[0]) >= 3:
|
|
t = Epoch(args[0][0], args[0][1], args[0][2], **kwargs)
|
|
else:
|
|
raise ValueError("Invalid input")
|
|
elif isinstance(args[0], datetime.datetime) or isinstance(
|
|
args[0], datetime.date
|
|
):
|
|
t = Epoch(args[0].year, args[0].month, args[0].day, **kwargs)
|
|
else:
|
|
raise TypeError("Invalid input type")
|
|
elif len(args) == 2:
|
|
raise ValueError("Invalid input: Date given is not valid")
|
|
elif len(args) >= 3:
|
|
# We will rely on Epoch capacity to handle improper input
|
|
t = Epoch(args[0], args[1], args[2], **kwargs)
|
|
return t
|
|
|
|
@staticmethod
|
|
def is_julian(year, month, day):
|
|
"""This method returns True if given date is in the Julian calendar.
|
|
|
|
:param year: Year
|
|
:type y: int
|
|
:param month: Month
|
|
:type m: int
|
|
:param day: Day
|
|
:type day: int
|
|
|
|
:returns: Whether the provided date belongs to Julian calendar or not.
|
|
:rtype: bool
|
|
|
|
>>> Epoch.is_julian(1997, 5, 27.1)
|
|
False
|
|
>>> Epoch.is_julian(1397, 7, 7.0)
|
|
True
|
|
"""
|
|
|
|
if (
|
|
(year < 1582)
|
|
or (year == 1582 and month < 10)
|
|
or (year == 1582 and month == 10 and day < 5.0)
|
|
):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def julian(self):
|
|
"""This method returns True if this Epoch object holds a date in the
|
|
Julian calendar.
|
|
|
|
:returns: Whether this Epoch object holds a date belonging to Julian
|
|
calendar or not.
|
|
:rtype: bool
|
|
|
|
>>> e = Epoch(1997, 5, 27.1)
|
|
>>> e.julian()
|
|
False
|
|
>>> e = Epoch(1397, 7, 7.0)
|
|
>>> e.julian()
|
|
True
|
|
"""
|
|
|
|
y, m, d = self.get_date()
|
|
return Epoch.is_julian(y, m, d)
|
|
|
|
@staticmethod
|
|
def get_month(month, as_string=False):
|
|
"""Method to get the month as a integer in the [1, 12] range, or as a
|
|
full name.
|
|
|
|
:param month: Month, in numeric, short name or long name format
|
|
:type month: int, float, str
|
|
:param as_string: Whether the output will be numeric, or a long name.
|
|
:type as_string: bool
|
|
|
|
:returns: Month as integer in the [1, 12] range, or as a long name.
|
|
:rtype: int, str
|
|
:raises: ValueError if input month value is invalid.
|
|
|
|
>>> Epoch.get_month(4.0)
|
|
4
|
|
>>> Epoch.get_month('Oct')
|
|
10
|
|
>>> Epoch.get_month('FEB')
|
|
2
|
|
>>> Epoch.get_month('August')
|
|
8
|
|
>>> Epoch.get_month('august')
|
|
8
|
|
>>> Epoch.get_month('NOVEMBER')
|
|
11
|
|
>>> Epoch.get_month(9.0, as_string=True)
|
|
'September'
|
|
>>> Epoch.get_month('Feb', as_string=True)
|
|
'February'
|
|
>>> Epoch.get_month('March', as_string=True)
|
|
'March'
|
|
"""
|
|
|
|
months_mmm = [
|
|
"Jan",
|
|
"Feb",
|
|
"Mar",
|
|
"Apr",
|
|
"May",
|
|
"Jun",
|
|
"Jul",
|
|
"Aug",
|
|
"Sep",
|
|
"Oct",
|
|
"Nov",
|
|
"Dec",
|
|
]
|
|
|
|
months_full = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December",
|
|
]
|
|
|
|
if isinstance(month, (int, float)):
|
|
month = int(month) # Truncate if it has decimals
|
|
if month >= 1 and month <= 12:
|
|
if not as_string:
|
|
return month
|
|
else:
|
|
return months_full[month - 1]
|
|
else:
|
|
raise ValueError("Invalid value for the input month")
|
|
elif isinstance(month, str):
|
|
month = month.strip().capitalize()
|
|
if len(month) == 3:
|
|
if month in months_mmm:
|
|
if not as_string:
|
|
return months_mmm.index(month) + 1
|
|
else:
|
|
return months_full[months_mmm.index(month)]
|
|
else:
|
|
raise ValueError("Invalid value for the input month")
|
|
else:
|
|
if month in months_full:
|
|
if not as_string:
|
|
return months_full.index(month) + 1
|
|
else:
|
|
return month
|
|
else:
|
|
raise ValueError("Invalid value for the input month")
|
|
|
|
@staticmethod
|
|
def is_leap(year):
|
|
"""Method to check if a given year is a leap year.
|
|
|
|
:param year: Year to be checked.
|
|
:type year: int, float
|
|
|
|
:returns: Whether or not year is a leap year.
|
|
:rtype: bool
|
|
:raises: ValueError if input year value is invalid.
|
|
|
|
>>> Epoch.is_leap(2003)
|
|
False
|
|
>>> Epoch.is_leap(2012)
|
|
True
|
|
>>> Epoch.is_leap(1900)
|
|
False
|
|
>>> Epoch.is_leap(-1000)
|
|
True
|
|
>>> Epoch.is_leap(1000)
|
|
True
|
|
"""
|
|
|
|
if isinstance(year, (int, float)):
|
|
# Mind the difference between Julian and Gregorian calendars
|
|
if year >= 1582:
|
|
year = iint(year)
|
|
return calendar.isleap(year)
|
|
else:
|
|
return (abs(year) % 4) == 0
|
|
else:
|
|
raise ValueError("Invalid value for the input year")
|
|
|
|
def leap(self):
|
|
"""This method checks if the current Epoch object holds a leap year.
|
|
|
|
:returns: Whether or the year in this Epoch object is a leap year.
|
|
:rtype: bool
|
|
|
|
>>> e = Epoch(2003, 1, 1)
|
|
>>> e.leap()
|
|
False
|
|
>>> e = Epoch(2012, 1, 1)
|
|
>>> e.leap()
|
|
True
|
|
>>> e = Epoch(1900, 1, 1)
|
|
>>> e.leap()
|
|
False
|
|
>>> e = Epoch(-1000, 1, 1)
|
|
>>> e.leap()
|
|
True
|
|
>>> e = Epoch(1000, 1, 1)
|
|
>>> e.leap()
|
|
True
|
|
"""
|
|
|
|
y, m, d = self.get_date()
|
|
return Epoch.is_leap(y)
|
|
|
|
@staticmethod
|
|
def get_doy(yyyy, mm, dd):
|
|
"""This method returns the Day Of Year (DOY) for the given date.
|
|
|
|
:param yyyy: Year, in four digits format
|
|
:type yyyy: int, float
|
|
:param mm: Month, in numeric format (1 = January, 2 = February, etc)
|
|
:type mm: int, float
|
|
:param dd: Day, in numeric format
|
|
:type dd: int, float
|
|
|
|
:returns: Day Of Year (DOY).
|
|
:rtype: float
|
|
:raises: ValueError if input values correspond to a wrong date.
|
|
|
|
>>> Epoch.get_doy(1999, 1, 29)
|
|
29.0
|
|
>>> Epoch.get_doy(1978, 11, 14)
|
|
318.0
|
|
>>> Epoch.get_doy(2017, 12, 31.7)
|
|
365.7
|
|
>>> Epoch.get_doy(2012, 3, 3.1)
|
|
63.1
|
|
>>> Epoch.get_doy(-400, 2, 29.9)
|
|
60.9
|
|
"""
|
|
|
|
# Let's carry out first some basic checks
|
|
if dd < 1 or dd >= 32 or mm < 1 or mm > 12:
|
|
raise ValueError("Invalid input data")
|
|
day = int(dd)
|
|
frac = dd % 1
|
|
if yyyy >= 1: # datetime's minimum year is 1
|
|
try:
|
|
d = datetime.date(yyyy, mm, day)
|
|
except ValueError:
|
|
raise ValueError("Invalid input date")
|
|
doy = d.timetuple().tm_yday
|
|
else:
|
|
k = 2 if Epoch.is_leap(yyyy) else 1
|
|
doy = (iint((275.0 * mm) / 9.0) -
|
|
k * iint((mm + 9.0) / 12.0) + day - 30.0)
|
|
return float(doy + frac)
|
|
|
|
@staticmethod
|
|
def doy2date(year, doy):
|
|
"""This method takes a year and a Day Of Year values, and returns the
|
|
corresponding date.
|
|
|
|
:param year: Year, in four digits format
|
|
:type year: int, float
|
|
:param doy: Day of Year number
|
|
:type doy: int, float
|
|
|
|
:returns: Year, month, day.
|
|
:rtype: tuple
|
|
:raises: ValueError if either input year or doy values are invalid.
|
|
|
|
>>> t = Epoch.doy2date(1999, 29)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
1999/1/29.0
|
|
>>> t = Epoch.doy2date(2017, 365.7)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
2017/12/31.7
|
|
>>> t = Epoch.doy2date(2012, 63.1)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
2012/3/3.1
|
|
>>> t = Epoch.doy2date(-1004, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-1004/2/29.0
|
|
>>> t = Epoch.doy2date(0, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
0/2/29.0
|
|
>>> t = Epoch.doy2date(1, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
1/3/1.0
|
|
>>> t = Epoch.doy2date(-1, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-1/3/1.0
|
|
>>> t = Epoch.doy2date(-2, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-2/3/1.0
|
|
>>> t = Epoch.doy2date(-3, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-3/3/1.0
|
|
>>> t = Epoch.doy2date(-4, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-4/2/29.0
|
|
>>> t = Epoch.doy2date(-5, 60)
|
|
>>> print("{}/{}/{}".format(t[0], t[1], round(t[2], 1)))
|
|
-5/3/1.0
|
|
"""
|
|
|
|
if isinstance(year, (int, float)) and isinstance(doy, (int, float)):
|
|
frac = float(doy % 1)
|
|
doy = int(doy)
|
|
if year >= 1: # datetime's minimum year is 1
|
|
ref = datetime.date(year, 1, 1)
|
|
mydate = datetime.date.fromordinal(ref.toordinal() + doy - 1)
|
|
return year, mydate.month, mydate.day + frac
|
|
else:
|
|
# The algorithm provided by Meeus doesn't work for years below
|
|
# +1. This little hack solves that problem (the 'if' result is
|
|
# inverted here).
|
|
k = 1 if Epoch.is_leap(year) else 2
|
|
if doy < 32:
|
|
m = 1
|
|
else:
|
|
m = iint((9.0 * (k + doy)) / 275.0 + 0.98)
|
|
d = (doy - iint((275.0 * m) / 9.0) +
|
|
k * iint((m + 9.0) / 12.0) + 30)
|
|
return year, int(m), d + frac
|
|
else:
|
|
raise ValueError("Invalid input values")
|
|
|
|
@staticmethod
|
|
def leap_seconds(year, month):
|
|
"""Returns the leap seconds accumulated for the given year and month.
|
|
|
|
:param year: Year
|
|
:type year: int
|
|
:param month: Month, in numeric format ([1:12] range)
|
|
:type month: int
|
|
|
|
:returns: Leap seconds accumulated for given year and month.
|
|
:rtype: int
|
|
|
|
>>> Epoch.leap_seconds(1972, 4)
|
|
0
|
|
>>> Epoch.leap_seconds(1972, 6)
|
|
0
|
|
>>> Epoch.leap_seconds(1972, 7)
|
|
1
|
|
>>> Epoch.leap_seconds(1983, 6)
|
|
11
|
|
>>> Epoch.leap_seconds(1983, 7)
|
|
12
|
|
>>> Epoch.leap_seconds(1985, 8)
|
|
13
|
|
>>> Epoch.leap_seconds(2016, 11)
|
|
26
|
|
>>> Epoch.leap_seconds(2017, 1)
|
|
27
|
|
>>> Epoch.leap_seconds(2018, 7)
|
|
27
|
|
"""
|
|
|
|
list_years = sorted(LEAP_TABLE.keys())
|
|
# First test the extremes of the table
|
|
if (year + month / 12.0) <= list_years[0]:
|
|
return 0
|
|
if (year + month / 12.0) >= list_years[-1]:
|
|
return LEAP_TABLE[list_years[-1]]
|
|
lyear = (year + 0.25) if month <= 6 else (year + 0.75)
|
|
idx = 0
|
|
while lyear > list_years[idx]:
|
|
idx += 1
|
|
return LEAP_TABLE[list_years[idx - 1]]
|
|
|
|
@staticmethod
|
|
def get_last_leap_second():
|
|
"""Method to get the date and value of the last leap second added to
|
|
the table
|
|
|
|
:returns: Tuple with year, month, day, leap second value.
|
|
:rtype: tuple
|
|
"""
|
|
|
|
list_years = sorted(LEAP_TABLE.keys())
|
|
lyear = list_years[-1]
|
|
lseconds = LEAP_TABLE[lyear]
|
|
year = iint(lyear)
|
|
# So far, leap seconds are added either on June 30th or December 31th
|
|
if lyear % 1 == 0.0:
|
|
year -= 1
|
|
month = 12
|
|
day = 31.0
|
|
else:
|
|
month = 6
|
|
day = 30.0
|
|
return year, month, day, lseconds
|
|
|
|
@staticmethod
|
|
def utc2local():
|
|
"""Method to return the difference between UTC and local time.
|
|
|
|
By default, dates in this Epoch class are handled in Coordinated
|
|
Universal Time (UTC). This method provides you the seconds that you
|
|
have to add or subtract to UTC time to convert to your local time.
|
|
|
|
Please bear in mind that, in order for this method to work, you
|
|
operative system must be correctly configured, with the right time and
|
|
corresponding time zone.
|
|
|
|
:returns: Difference in seconds between local and UTC time.
|
|
:rtype: float
|
|
"""
|
|
|
|
localhour = datetime.datetime.now().hour
|
|
utchour = datetime.datetime.utcnow().hour
|
|
localminute = datetime.datetime.now().minute
|
|
utcminute = datetime.datetime.utcnow().minute
|
|
return ((localhour - utchour) * 3600.0 +
|
|
(localminute - utcminute) * 60.0)
|
|
|
|
@staticmethod
|
|
def easter(year):
|
|
"""Method to return the Easter day for given year.
|
|
|
|
.. note:: This method is valid for both Gregorian and Julian years.
|
|
|
|
:param year: Year
|
|
:type year: int
|
|
|
|
:returns: Easter month and day, as a tuple
|
|
:rtype: tuple
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> Epoch.easter(1991)
|
|
(3, 31)
|
|
>>> Epoch.easter(1818)
|
|
(3, 22)
|
|
>>> Epoch.easter(1943)
|
|
(4, 25)
|
|
>>> Epoch.easter(2000)
|
|
(4, 23)
|
|
>>> Epoch.easter(1954)
|
|
(4, 18)
|
|
>>> Epoch.easter(179)
|
|
(4, 12)
|
|
>>> Epoch.easter(1243)
|
|
(4, 12)
|
|
"""
|
|
|
|
# This algorithm is describes in pages 67-69 of Meeus book
|
|
if not isinstance(year, (int, float)):
|
|
raise TypeError("Invalid input type")
|
|
year = int(year)
|
|
if year >= 1583:
|
|
# In this case, we are using the Gregorian calendar
|
|
a = year % 19
|
|
b = iint(year / 100.0)
|
|
c = year % 100
|
|
d = iint(b / 4.0)
|
|
e = b % 4
|
|
f = iint((b + 8.0) / 25.0)
|
|
g = iint((b - f + 1.0) / 3.0)
|
|
h = (19 * a + b - d - g + 15) % 30
|
|
i = iint(c / 4.0)
|
|
k = c % 4
|
|
ll = (32 + 2 * (e + i) - h - k) % 7
|
|
m = iint((a + 11 * h + 22 * ll) / 451.0)
|
|
n = iint((h + ll - 7 * m + 114) / 31.0)
|
|
p = (h + ll - 7 * m + 114) % 31
|
|
return (n, p + 1)
|
|
else:
|
|
# The Julian calendar is used here
|
|
a = year % 4
|
|
b = year % 7
|
|
c = year % 19
|
|
d = (19 * c + 15) % 30
|
|
e = (2 * a + 4 * b - d + 34) % 7
|
|
f = iint((d + e + 114) / 31.0)
|
|
g = (d + e + 114) % 31
|
|
return (f, g + 1)
|
|
|
|
@staticmethod
|
|
def jewish_pesach(year):
|
|
"""Method to return the Jewish Easter (Pesach) day for given year.
|
|
|
|
.. note:: This method is valid for both Gregorian and Julian years.
|
|
|
|
:param year: Year
|
|
:type year: int
|
|
|
|
:returns: Jewish Easter (Pesach) month and day, as a tuple
|
|
:rtype: tuple
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> Epoch.jewish_pesach(1990)
|
|
(4, 10)
|
|
"""
|
|
|
|
# This algorithm is described in pages 71-73 of Meeus book
|
|
if not isinstance(year, (int, float)):
|
|
raise TypeError("Invalid input type")
|
|
year = iint(year)
|
|
c = iint(year / 100.0)
|
|
s = 0 if year < 1583 else iint((3.0 * c - 5.0) / 4.0)
|
|
a = (12 * (year + 1)) % 19
|
|
b = year % 4
|
|
q = (-1.904412361576 + 1.554241796621 * a +
|
|
0.25 * b - 0.003177794022 * year + s)
|
|
j = (iint(q) + 3 * year + 5 * b + 2 + s) % 7
|
|
r = q - iint(q)
|
|
if j == 2 or j == 4 or j == 6:
|
|
d = iint(q) + 23
|
|
elif j == 1 and a > 6 and r > 0.632870370:
|
|
d = iint(q) + 24
|
|
elif j == 0 and a > 11 and r > 0.897723765:
|
|
d = iint(q) + 23
|
|
else:
|
|
d = iint(q) + 22
|
|
if d > 31:
|
|
return (4, d - 31)
|
|
else:
|
|
return (3, d)
|
|
|
|
@staticmethod
|
|
def moslem2gregorian(year, month, day):
|
|
"""Method to convert a date in the Moslen calendar to the Gregorian
|
|
(or Julian) calendar.
|
|
|
|
.. note:: This method is valid for both Gregorian and Julian years.
|
|
|
|
:param year: Year
|
|
:type year: int
|
|
:param month: Month
|
|
:type month: int
|
|
:param day: Day
|
|
:type day: int
|
|
|
|
:returns: Date in Gregorian (Julian) calendar: year, month and day, as
|
|
a tuple
|
|
:rtype: tuple
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> Epoch.moslem2gregorian(1421, 1, 1)
|
|
(2000, 4, 6)
|
|
"""
|
|
|
|
# First, check that input types are correct
|
|
if (
|
|
not isinstance(year, (int, float))
|
|
or not isinstance(month, (int, float))
|
|
or not isinstance(day, (int, float))
|
|
):
|
|
raise TypeError("Invalid input type")
|
|
if day < 1 or day > 30 or month < 1 or month > 12 or year < 1:
|
|
raise ValueError("Invalid input data")
|
|
# This algorithm is described in pages 73-75 of Meeus book
|
|
# Note: Ramadan is month Nr. 9
|
|
h = iint(year)
|
|
m = iint(month)
|
|
d = iint(day)
|
|
n = d + iint(29.5001 * (m - 1) + 0.99)
|
|
q = iint(h / 30.0)
|
|
r = h % 30
|
|
a = iint((11.0 * r + 3.0) / 30.0)
|
|
w = 404 * q + 354 * r + 208 + a
|
|
q1 = iint(w / 1461.0)
|
|
q2 = w % 1461
|
|
g = 621 + 4 * iint(7.0 * q + q1)
|
|
k = iint(q2 / 365.2422)
|
|
e = iint(365.2422 * k)
|
|
j = q2 - e + n - 1
|
|
x = g + k
|
|
if j > 366 and x % 4 == 0:
|
|
j -= 366
|
|
x += 1
|
|
elif j > 365 and x % 4 > 0:
|
|
j -= 365
|
|
x += 1
|
|
|
|
# Check if date is in Gregorian calendar. '277' is DOY of October 4th
|
|
if (x > 1583) or (x == 1582 and j > 277):
|
|
jd = iint(365.25 * (x - 1.0)) + 1721423 + j
|
|
alpha = iint((jd - 1867216.25) / 36524.25)
|
|
beta = jd if jd < 2299161 else (jd + 1 + alpha - iint(alpha / 4.0))
|
|
b = beta + 1524
|
|
c = iint((b - 122.1) / 365.25)
|
|
d = iint(365.25 * c)
|
|
e = iint((b - d) / 30.6001)
|
|
day = b - d - iint(30.6001 * e)
|
|
month = (e - 1) if e < 14 else (e - 13)
|
|
year = (c - 4716) if month > 2 else (c - 4715)
|
|
return year, month, day
|
|
else:
|
|
# It is a Julian date. We have year and DOY
|
|
return Epoch.doy2date(x, j)
|
|
|
|
@staticmethod
|
|
def gregorian2moslem(year, month, day):
|
|
"""Method to convert a date in the Gregorian (or Julian) calendar to
|
|
the Moslen calendar.
|
|
|
|
:param year: Year
|
|
:type year: int
|
|
:param month: Month
|
|
:type month: int
|
|
:param day: Day
|
|
:type day: int
|
|
|
|
:returns: Date in Moslem calendar: year, month and day, as a tuple
|
|
:rtype: tuple
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> Epoch.gregorian2moslem(1991, 8, 13)
|
|
(1412, 2, 2)
|
|
"""
|
|
|
|
# First, check that input types are correct
|
|
if (
|
|
not isinstance(year, (int, float))
|
|
or not isinstance(month, (int, float))
|
|
or not isinstance(day, (int, float))
|
|
):
|
|
raise TypeError("Invalid input type")
|
|
if day < 1 or day > 31 or month < 1 or month > 12 or year < -4712:
|
|
raise ValueError("Invalid input data")
|
|
# This algorithm is described in pages 75-76 of Meeus book
|
|
x = iint(year)
|
|
m = iint(month)
|
|
d = iint(day)
|
|
if m < 3:
|
|
x -= 1
|
|
m += 12
|
|
alpha = iint(x / 100.0)
|
|
beta = 2 - alpha + iint(alpha / 4.0)
|
|
b = iint(365.25 * x) + iint(30.6001 * (m + 1.0)) + d + 1722519 + beta
|
|
c = iint((b - 122.1) / 365.25)
|
|
d = iint(365.25 * c)
|
|
e = iint((b - d) / 30.6001)
|
|
d = b - d - iint(30.6001 * e)
|
|
m = (e - 1) if e < 14 else (e - 13)
|
|
x = (c - 4716) if month > 2 else (c - 4715)
|
|
w = 1 if x % 4 == 0 else 2
|
|
n = iint((275.0 * m) / 9.0) - w * iint((m + 9.0) / 12.0) + d - 30
|
|
a = x - 623
|
|
b = iint(a / 4.0)
|
|
c = a % 4
|
|
c1 = 365.2501 * c
|
|
c2 = iint(c1)
|
|
if c1 - c2 > 0.5:
|
|
c2 += 1
|
|
dp = 1461 * b + 170 + c2
|
|
q = iint(dp / 10631.0)
|
|
r = dp % 10631
|
|
j = iint(r / 354.0)
|
|
k = r % 354
|
|
o = iint((11.0 * j + 14.0) / 30.0)
|
|
h = 30 * q + j + 1
|
|
jj = k - o + n - 1
|
|
# jj is the number of the day in the moslem year h. If jj > 354 we need
|
|
# to know if h is a leap year
|
|
if jj > 354:
|
|
cl = h % 30
|
|
dl = (11 * cl + 3) % 30
|
|
if dl < 19:
|
|
jj -= 354
|
|
h += 1
|
|
else:
|
|
jj -= 355
|
|
h += 1
|
|
if jj == 0:
|
|
jj = 355
|
|
h -= 1
|
|
# Now, let's convert DOY jj to month and day
|
|
if jj == 355:
|
|
m = 12
|
|
d = 30
|
|
else:
|
|
s = iint((jj - 1.0) / 29.5)
|
|
m = 1 + s
|
|
d = iint(jj - 29.5 * s)
|
|
return h, m, d
|
|
|
|
def __str__(self):
|
|
"""Method used when trying to print the object.
|
|
|
|
:returns: Internal JDE value as a string.
|
|
:rtype: string
|
|
|
|
>>> e = Epoch(1987, 6, 19.5)
|
|
>>> print(e)
|
|
2446966.0
|
|
"""
|
|
|
|
return str(self._jde)
|
|
|
|
def __repr__(self):
|
|
"""Method providing the 'official' string representation of the object.
|
|
It provides a valid expression that could be used to recreate the
|
|
object.
|
|
|
|
:returns: As string with a valid expression to recreate the object
|
|
:rtype: string
|
|
|
|
>>> e = Epoch(1987, 6, 19.5)
|
|
>>> repr(e)
|
|
'Epoch(2446966.0)'
|
|
"""
|
|
|
|
return "{}({})".format(self.__class__.__name__, self._jde)
|
|
|
|
def get_date(self, **kwargs):
|
|
"""This method converts the internal JDE value back to a date.
|
|
|
|
Use **utc=True** to enable the TT to UTC conversion mechanism, or
|
|
provide a non zero value to **leap_seconds** to apply a specific leap
|
|
seconds value.
|
|
|
|
:param utc: Whether the TT to UTC conversion mechanism will be enabled
|
|
:type utc: bool
|
|
:param leap_seconds: Optional value for leap seconds.
|
|
:type leap_seconds: int, float
|
|
|
|
:returns: Year, month, day in a tuple
|
|
:rtype: tuple
|
|
|
|
>>> e = Epoch(2436116.31)
|
|
>>> y, m, d = e.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
1957/10/4.81
|
|
>>> e = Epoch(1988, 1, 27)
|
|
>>> y, m, d = e.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
1988/1/27.0
|
|
>>> e = Epoch(1842713.0)
|
|
>>> y, m, d = e.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
333/1/27.5
|
|
>>> e = Epoch(1507900.13)
|
|
>>> y, m, d = e.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
-584/5/28.63
|
|
"""
|
|
|
|
jd = self._jde + 0.5
|
|
z = iint(jd)
|
|
f = jd % 1
|
|
if z < 2299161:
|
|
a = z
|
|
else:
|
|
alpha = iint((z - 1867216.25) / 36524.25)
|
|
a = z + 1 + alpha - iint(alpha / 4.0)
|
|
b = a + 1524
|
|
c = iint((b - 122.1) / 365.25)
|
|
d = iint(365.25 * c)
|
|
e = iint((b - d) / 30.6001)
|
|
day = b - d - iint(30.6001 * e) + f
|
|
if e < 14:
|
|
month = e - 1
|
|
elif e == 14 or e == 15:
|
|
month = e - 13
|
|
if month > 2:
|
|
year = c - 4716
|
|
elif month == 1 or month == 2:
|
|
year = c - 4715
|
|
year = int(year)
|
|
month = int(month)
|
|
|
|
tt2utc = False
|
|
if "utc" in kwargs:
|
|
tt2utc = kwargs["utc"]
|
|
if "leap_seconds" in kwargs:
|
|
tt2utc = False
|
|
leap_seconds = kwargs["leap_seconds"]
|
|
else:
|
|
leap_seconds = 0.0
|
|
# If enabled, let's convert from TT to UTC, subtracting needed seconds
|
|
deltasec = 0.0
|
|
# In this case, TT to UTC correction is applied automatically, but only
|
|
# for dates after July 1st, 1972
|
|
if tt2utc:
|
|
if year > 1972 or (year == 1972 and month >= 7):
|
|
deltasec = 32.184 # Difference between TT and TAI
|
|
deltasec += 10.0 # Difference between UTC and TAI in 1972
|
|
deltasec += Epoch.leap_seconds(year, month)
|
|
else: # Correction is NOT automatic
|
|
if leap_seconds != 0.0: # We apply provided leap seconds
|
|
if year > 1972 or (year == 1972 and month >= 7):
|
|
deltasec = 32.184 # Difference between TT and TAI
|
|
deltasec += 10.0 # Difference between UTC-TAI in 1972
|
|
deltasec += leap_seconds
|
|
|
|
if deltasec != 0.0:
|
|
doy = Epoch.get_doy(year, month, day)
|
|
doy -= deltasec / DAY2SEC
|
|
# Check that we didn't change year
|
|
if doy < 1.0:
|
|
year -= 1
|
|
doy = 366.0 + doy if Epoch.is_leap(year) else 365.0 + doy
|
|
year, month, day = Epoch.doy2date(year, doy)
|
|
return year, month, day
|
|
|
|
def get_full_date(self, **kwargs):
|
|
"""This method converts the internal JDE value back to a full date.
|
|
|
|
Use **utc=True** to enable the TT to UTC conversion mechanism, or
|
|
provide a non zero value to **leap_seconds** to apply a specific leap
|
|
seconds value.
|
|
|
|
:param utc: Whether the TT to UTC conversion mechanism will be enabled
|
|
:type utc: bool
|
|
:param leap_seconds: Optional value for leap seconds.
|
|
:type leap_seconds: int, float
|
|
|
|
:returns: Year, month, day, hours, minutes, seconds in a tuple
|
|
:rtype: tuple
|
|
|
|
>>> e = Epoch(2436116.31)
|
|
>>> y, m, d, h, mi, s = e.get_full_date()
|
|
>>> print("{}/{}/{} {}:{}:{}".format(y, m, d, h, mi, round(s, 1)))
|
|
1957/10/4 19:26:24.0
|
|
>>> e = Epoch(1988, 1, 27)
|
|
>>> y, m, d, h, mi, s = e.get_full_date()
|
|
>>> print("{}/{}/{} {}:{}:{}".format(y, m, d, h, mi, round(s, 1)))
|
|
1988/1/27 0:0:0.0
|
|
>>> e = Epoch(1842713.0)
|
|
>>> y, m, d, h, mi, s = e.get_full_date()
|
|
>>> print("{}/{}/{} {}:{}:{}".format(y, m, d, h, mi, round(s, 1)))
|
|
333/1/27 12:0:0.0
|
|
>>> e = Epoch(1507900.13)
|
|
>>> y, m, d, h, mi, s = e.get_full_date()
|
|
>>> print("{}/{}/{} {}:{}:{}".format(y, m, d, h, mi, round(s, 1)))
|
|
-584/5/28 15:7:12.0
|
|
"""
|
|
|
|
y, m, d = self.get_date(**kwargs)
|
|
r = d % 1
|
|
d = int(d)
|
|
h = int(r * 24.0)
|
|
r = r * 24 - h
|
|
mi = int(r * 60.0)
|
|
s = 60.0 * (r * 60.0 - mi)
|
|
return y, m, d, h, mi, s
|
|
|
|
@staticmethod
|
|
def tt2ut(year, month):
|
|
"""This method provides an approximation of the difference, in seconds,
|
|
between Terrestrial Time and Universal Time, denoted **DeltaT**, where:
|
|
DeltaT = TT - UT.
|
|
|
|
Here we depart from Meeus book and use the polynomial expressions from:
|
|
|
|
https://eclipse.gsfc.nasa.gov/LEcat5/deltatpoly.html
|
|
|
|
Which are regarded as more elaborate and precise than Meeus'.
|
|
|
|
Please note that, by definition, the UTC time used internally in this
|
|
Epoch class by default is kept within 0.9 seconds from UT. Therefore,
|
|
UTC is in itself a quite good approximation to UT, arguably better than
|
|
some of the results provided by this method.
|
|
|
|
:param year: Year we want to compute DeltaT for.
|
|
:type year: int, float
|
|
:param month: Month we want to compute DeltaT for.
|
|
:type month: int, float
|
|
|
|
:returns: DeltaT, in seconds
|
|
:rtype: float
|
|
|
|
>>> round(Epoch.tt2ut(1642, 1), 1)
|
|
62.1
|
|
>>> round(Epoch.tt2ut(1680, 1), 1)
|
|
15.3
|
|
>>> round(Epoch.tt2ut(1700, 1), 1)
|
|
8.8
|
|
>>> round(Epoch.tt2ut(1726, 1), 1)
|
|
10.9
|
|
>>> round(Epoch.tt2ut(1750, 1), 1)
|
|
13.4
|
|
>>> round(Epoch.tt2ut(1774, 1), 1)
|
|
16.7
|
|
>>> round(Epoch.tt2ut(1800, 1), 1)
|
|
13.7
|
|
>>> round(Epoch.tt2ut(1820, 1), 1)
|
|
11.9
|
|
>>> round(Epoch.tt2ut(1890, 1), 1)
|
|
-6.1
|
|
>>> round(Epoch.tt2ut(1928, 2), 1)
|
|
24.2
|
|
>>> round(Epoch.tt2ut(1977, 2), 1)
|
|
47.7
|
|
>>> round(Epoch.tt2ut(1998, 1), 1)
|
|
63.0
|
|
>>> round(Epoch.tt2ut(2015, 7), 1)
|
|
69.3
|
|
"""
|
|
|
|
y = year + (month - 0.5) / 12.0
|
|
if year < -500:
|
|
u = (year - 1820.0) / 100.0
|
|
dt = -20.0 + 32.0 * u * u
|
|
elif year >= -500 and year < 500:
|
|
u = y / 100.0
|
|
dt = 10583.6 + u * (
|
|
-1014.41
|
|
+ u
|
|
* (
|
|
33.78311
|
|
+ u
|
|
* (
|
|
-5.952053
|
|
+ (u * (-0.1798452 +
|
|
u * (0.022174192 + 0.0090316521 * u)))
|
|
)
|
|
)
|
|
)
|
|
elif year >= 500 and year < 1600:
|
|
dt = 1574.2 + u * (
|
|
-556.01
|
|
+ u
|
|
* (
|
|
71.23472
|
|
+ u
|
|
* (
|
|
0.319781
|
|
+ (u * (-0.8503463 +
|
|
u * (-0.005050998 + 0.0083572073 * u)))
|
|
)
|
|
)
|
|
)
|
|
elif year >= 1600 and year < 1700:
|
|
t = y - 1600.0
|
|
dt = 120.0 + t * (-0.9808 + t * (-0.01532 + t / 7129.0))
|
|
elif year >= 1700 and year < 1800:
|
|
t = y - 1700.0
|
|
dt = 8.83 + t * (
|
|
0.1603 + t * (-0.0059285 + t * (0.00013336 - t / 1174000.0))
|
|
)
|
|
elif year >= 1800 and year < 1860:
|
|
t = y - 1800.0
|
|
dt = 13.72 + t * (
|
|
-0.332447
|
|
+ t
|
|
* (
|
|
0.0068612
|
|
+ t
|
|
* (
|
|
0.0041116
|
|
+ t
|
|
* (
|
|
-0.00037436
|
|
+ t
|
|
* (0.0000121272 + t * (-0.0000001699 +
|
|
0.000000000875 * t))
|
|
)
|
|
)
|
|
)
|
|
)
|
|
elif year >= 1860 and year < 1900:
|
|
t = y - 1860.0
|
|
dt = 7.62 + t * (
|
|
0.5737
|
|
+ t
|
|
* (-0.251754 + t * (0.01680668 +
|
|
t * (-0.0004473624 + t / 233174.0)))
|
|
)
|
|
elif year >= 1900 and year < 1920:
|
|
t = y - 1900.0
|
|
dt = -2.79 + t * (
|
|
1.494119 + t * (-0.0598939 + t * (0.0061966 - 0.000197 * t))
|
|
)
|
|
elif year >= 1920 and year < 1941:
|
|
t = y - 1920.0
|
|
dt = 21.20 + t * (0.84493 + t * (-0.076100 + 0.0020936 * t))
|
|
elif year >= 1941 and year < 1961:
|
|
t = y - 1950.0
|
|
dt = 29.07 + t * (0.407 + t * (-1.0 / 233.0 + t / 2547.0))
|
|
elif year >= 1961 and year < 1986:
|
|
t = y - 1975.0
|
|
dt = 45.45 + t * (1.067 + t * (-1.0 / 260.0 - t / 718.0))
|
|
elif year >= 1986 and year < 2005:
|
|
t = y - 2000.0
|
|
dt = 63.86 + t * (
|
|
0.3345
|
|
+ t
|
|
* (-0.060374 + t * (0.0017275 +
|
|
t * (0.000651814 + 0.00002373599 * t)))
|
|
)
|
|
elif year >= 2005 and year < 2050:
|
|
t = y - 2000.0
|
|
dt = 62.92 + t * (0.32217 + 0.005589 * t)
|
|
elif year >= 2050 and year < 2150:
|
|
dt = (-20.0 + 32.0 * ((y - 1820.0) / 100.0) ** 2 -
|
|
0.5628 * (2150.0 - y))
|
|
else:
|
|
u = (year - 1820.0) / 100.0
|
|
dt = -20.0 + 32.0 * u * u
|
|
return dt
|
|
|
|
def dow(self, as_string=False):
|
|
"""Method to return the day of week corresponding to this Epoch.
|
|
|
|
By default, this method returns an integer value: 0 for Sunday, 1 for
|
|
Monday, etc. However, when **as_string=True** is passed, the names of
|
|
the days are returned.
|
|
|
|
:param as_string: Whether result will be given as a integer or as a
|
|
string. False by default.
|
|
:type as_string: bool
|
|
|
|
:returns: Day of the week, as a integer or as a string.
|
|
:rtype: int, str
|
|
|
|
>>> e = Epoch(1954, 'June', 30)
|
|
>>> e.dow()
|
|
3
|
|
>>> e = Epoch(2018, 'Feb', 14.9)
|
|
>>> e.dow(as_string=True)
|
|
'Wednesday'
|
|
>>> e = Epoch(2018, 'Feb', 15)
|
|
>>> e.dow(as_string=True)
|
|
'Thursday'
|
|
>>> e = Epoch(2018, 'Feb', 15.99)
|
|
>>> e.dow(as_string=True)
|
|
'Thursday'
|
|
>>> e.set(2018, 'Jul', 15.4)
|
|
>>> e.dow(as_string=True)
|
|
'Sunday'
|
|
>>> e.set(2018, 'Jul', 15.9)
|
|
>>> e.dow(as_string=True)
|
|
'Sunday'
|
|
"""
|
|
|
|
jd = iint(self._jde - 0.5) + 2.0
|
|
doy = iint(jd % 7)
|
|
if not as_string:
|
|
return doy
|
|
else:
|
|
day_names = [
|
|
"Sunday",
|
|
"Monday",
|
|
"Tuesday",
|
|
"Wednesday",
|
|
"Thursday",
|
|
"Friday",
|
|
"Saturday",
|
|
]
|
|
return day_names[doy]
|
|
|
|
def mean_sidereal_time(self):
|
|
"""Method to compute the _mean_ sidereal time at Greenwich for the
|
|
epoch stored in this object. It represents the Greenwich hour angle of
|
|
the mean vernal equinox.
|
|
|
|
.. note:: If you require the result as an angle, you should convert the
|
|
result from this method to hours with decimals (with
|
|
:const:`DAY2HOURS`), and then multiply by 15 deg/hr. Alternatively,
|
|
you can convert the result to hours with decimals, and feed this
|
|
value to an :class:`Angle` object, setting **ra=True**, and making
|
|
use of :class:`Angle` facilities for further handling.
|
|
|
|
:returns: Mean sidereal time, in days
|
|
:rtype: float
|
|
|
|
>>> e = Epoch(1987, 4, 10)
|
|
>>> round(e.mean_sidereal_time(), 9)
|
|
0.549147764
|
|
>>> e = Epoch(1987, 4, 10, 19, 21, 0.0)
|
|
>>> round(e.mean_sidereal_time(), 9)
|
|
0.357605204
|
|
"""
|
|
|
|
jd0 = iint(self()) + 0.5 if self() % 1 >= 0.5 else iint(self()) - 0.5
|
|
t = (jd0 - 2451545.0) / 36525.0
|
|
theta0 = 6.0 / DAY2HOURS + 41.0 / DAY2MIN + 50.54841 / DAY2SEC
|
|
s = t * (8640184.812866 + t * (0.093104 - 0.0000062 * t))
|
|
theta0 += (s % DAY2SEC) / DAY2SEC
|
|
deltajd = self() - jd0
|
|
if abs(deltajd) < TOL: # In this case, we are done
|
|
return theta0 % 1
|
|
else:
|
|
deltajd *= 1.00273790935
|
|
return (theta0 + deltajd) % 1
|
|
|
|
def apparent_sidereal_time(self, true_obliquity, nutation_longitude):
|
|
"""Method to compute the _apparent_ sidereal time at Greenwich for the
|
|
epoch stored in this object. It represents the Greenwich hour angle of
|
|
the true vernal equinox.
|
|
|
|
.. note:: If you require the result as an angle, you should convert the
|
|
result from this method to hours with decimals (with
|
|
:const:`DAY2HOURS`), and then multiply by 15 deg/hr. Alternatively,
|
|
you can convert the result to hours with decimals, and feed this
|
|
value to an :class:`Angle` object, setting **ra=True**, and making
|
|
use of :class:`Angle` facilities for further handling.
|
|
|
|
:param true_obliquity: The true obliquity of the ecliptic as an int,
|
|
float or :class:`Angle`, in degrees. You can use the method
|
|
`Earth.true_obliquity()` to find it.
|
|
:type true_obliquity: int, float, :class:`Angle`
|
|
:param nutation_longitude: The nutation in longitude as an int, float
|
|
or :class:`Angle`, in degrees. You can use method
|
|
`Earth.nutation_longitude()` to find it.
|
|
:type nutation_longitude: int, float, :class:`Angle`
|
|
|
|
:returns: Apparent sidereal time, in days
|
|
:rtype: float
|
|
:raises: TypeError if input value is of wrong type.
|
|
|
|
>>> e = Epoch(1987, 4, 10)
|
|
>>> round(e.apparent_sidereal_time(23.44357, (-3.788)/3600.0), 8)
|
|
0.54914508
|
|
"""
|
|
|
|
if not (
|
|
isinstance(true_obliquity, (int, float, Angle))
|
|
and isinstance(nutation_longitude, (int, float, Angle))
|
|
):
|
|
raise TypeError("Invalid input value")
|
|
if isinstance(true_obliquity, Angle):
|
|
true_obliquity = float(true_obliquity) # Convert to a float
|
|
if isinstance(nutation_longitude, Angle):
|
|
nutation_longitude = float(nutation_longitude)
|
|
mean_stime = self.mean_sidereal_time()
|
|
epsilon = radians(true_obliquity) # Convert to radians
|
|
delta_psi = nutation_longitude * 3600.0 # From degrees to seconds
|
|
# Correction is in seconds of arc: It must be converted to seconds of
|
|
# time, and then to days (sidereal time is given here in days)
|
|
return mean_stime + ((delta_psi * cos(epsilon)) / 15.0) / DAY2SEC
|
|
|
|
def mjd(self):
|
|
"""This method returns the Modified Julian Day (MJD).
|
|
|
|
:returns: Modified Julian Day (MJD).
|
|
:rtype: float
|
|
|
|
>>> e = Epoch(1858, 'NOVEMBER', 17)
|
|
>>> e.mjd()
|
|
0.0
|
|
"""
|
|
|
|
return self._jde - 2400000.5
|
|
|
|
def jde(self):
|
|
"""Method to return the internal value of the Julian Ephemeris Day.
|
|
|
|
:returns: The internal value of the Julian Ephemeris Day.
|
|
:rtype: float
|
|
|
|
>>> a = Epoch(-1000, 2, 29.0)
|
|
>>> print(a.jde())
|
|
1355866.5
|
|
"""
|
|
|
|
return self._jde
|
|
|
|
def year(self):
|
|
"""This method returns the contents of this object as a year with
|
|
decimals.
|
|
|
|
:returns: Year with decimals.
|
|
:rtype: float
|
|
|
|
>>> e = Epoch(1993, 'October', 1)
|
|
>>> print(round(e.year(), 4))
|
|
1993.7479
|
|
"""
|
|
|
|
y, m, d = self.get_date()
|
|
doy = Epoch.get_doy(y, m, d)
|
|
# We must substract 1 from doy in order to compute correctly
|
|
doy -= 1
|
|
days_of_year = 365.0
|
|
if self.leap():
|
|
days_of_year = 366.0
|
|
return y + doy / days_of_year
|
|
|
|
def rise_set(self, latitude, longitude, altitude=0.0):
|
|
"""This method computes the times of rising and setting of the Sun.
|
|
|
|
.. note:: The algorithm used is the one explained in the article
|
|
"Sunrise equation" of the Wikipedia at:
|
|
https://en.wikipedia.org/wiki/Sunrise_equation
|
|
|
|
.. note:: This algorithm is only valid within the artic and antartic
|
|
circles (+/- 66d 33'). Outside that range this method returns
|
|
a ValueError exception
|
|
|
|
.. note:: The results are given in UTC time.
|
|
|
|
:param latitude: Latitude of the observer, as an Angle object. Positive
|
|
to the North
|
|
:type latitude: :py:class:`Angle`
|
|
:param longitude: Longitude of the observer, as an Angle object.
|
|
Positive to the East
|
|
:type longitude: :py:class:`Angle`
|
|
:param altitude: Altitude of the observer, as meters above sea level
|
|
:type altitude: int, float
|
|
|
|
:returns: Two :py:class:`Epoch` objects representing rising time and
|
|
setting time, in a tuple
|
|
:rtype: tuple
|
|
:raises: TypeError if input values are of wrong type.
|
|
:raises: ValueError if latitude outside the +/- 66d 33' range.
|
|
|
|
>>> e = Epoch(2019, 4, 2)
|
|
>>> latitude = Angle(48, 8, 0)
|
|
>>> longitude = Angle(11, 34, 0)
|
|
>>> altitude = 520.0
|
|
>>> rising, setting = e.rise_set(latitude, longitude, altitude)
|
|
>>> y, m, d, h, mi, s = rising.get_full_date()
|
|
>>> print("{}:{}".format(h, mi))
|
|
4:48
|
|
>>> y, m, d, h, mi, s = setting.get_full_date()
|
|
>>> print("{}:{}".format(h, mi))
|
|
17:48
|
|
"""
|
|
|
|
if not (isinstance(latitude, Angle) and isinstance(longitude, Angle)
|
|
and isinstance(altitude, (int, float))):
|
|
raise TypeError("Invalid input types")
|
|
# Check that latitude is within valid range
|
|
limit = Angle(66, 33, 0)
|
|
if latitude > limit or latitude < -limit:
|
|
raise ValueError("Latitude outside the +/- 66d 33' range")
|
|
# Let's start computing the number of days since 2000/1/1 12:00 (cjd)
|
|
# Compute fractional Julian Day for leap seconds and terrestrial time
|
|
# We need current epoch without hours, minutes and seconds
|
|
year, month, day = self.get_date()
|
|
e = Epoch(year, month, day)
|
|
frac = (10.0 + 32.184 + Epoch.leap_seconds(year, month)) / 86400.0
|
|
cjd = e.jde() - 2451545.0 + frac
|
|
# Compute mean solar noon
|
|
jstar = cjd - (float(longitude) / 360.0)
|
|
# Solar mean anomaly
|
|
m = (357.5291 + 0.98560028 * jstar) % 360
|
|
mr = radians(m)
|
|
# Equation of the center
|
|
c = 1.9148 * sin(mr) + 0.02 * sin(2.0 * mr) + 0.0003 * sin(3.0 * mr)
|
|
# Ecliptic longitude
|
|
lambd = (m + c + 180.0 + 102.9372) % 360
|
|
lr = radians(lambd)
|
|
# Solar transit
|
|
jtran = 2451545.5 + jstar + 0.0053 * sin(mr) - 0.0069 * sin(2.0 * lr)
|
|
# NOTE: The original algorithm indicates a value of 2451545.0, but that
|
|
# leads to transit times around midnight, which is an error
|
|
# Declination of the Sun
|
|
sin_delta = sin(lr) * sin(radians(23.44))
|
|
delta = asin(sin_delta)
|
|
cos_delta = cos(delta)
|
|
# Hour angle
|
|
# First, correct by elevation
|
|
corr = -0.83 - 2.076 * sqrt(altitude) / 60.0
|
|
cos_om = ((sin(radians(corr)) - sin(latitude.rad()) * sin_delta) /
|
|
(cos(latitude.rad()) * cos_delta))
|
|
# Finally, compute rising and setting times
|
|
omega = degrees(acos(cos_om))
|
|
jrise = Epoch(jtran - (omega / 360.0))
|
|
jsett = Epoch(jtran + (omega / 360.0))
|
|
return jrise, jsett
|
|
|
|
def __call__(self):
|
|
"""Method used when Epoch is called only with parenthesis.
|
|
|
|
:returns: The internal value of the Julian Ephemeris Day.
|
|
:rtype: float
|
|
|
|
>>> a = Epoch(-122, 1, 1.0)
|
|
>>> print(a())
|
|
1676497.5
|
|
"""
|
|
|
|
return self._jde
|
|
|
|
def __add__(self, b):
|
|
"""This method defines the addition between an Epoch and some days.
|
|
|
|
:param b: Value to be added, in days.
|
|
:type b: int, float
|
|
|
|
:returns: A new Epoch object.
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: TypeError if operand is of wrong type.
|
|
|
|
>>> a = Epoch(1991, 7, 11)
|
|
>>> b = a + 10000
|
|
>>> y, m, d = b.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
2018/11/26.0
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return Epoch(self._jde + float(b))
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __sub__(self, b):
|
|
"""This method defines the subtraction between Epochs or between an
|
|
Epoch and a given number of days.
|
|
|
|
:param b: Value to be subtracted, either an Epoch or days.
|
|
:type b: py:class:`Epoch`, int, float
|
|
|
|
:returns: A new Epoch object if parameter 'b' is in days, or the
|
|
difference between provided Epochs, in days.
|
|
:rtype: :py:class:`Epoch`, float
|
|
:raises: TypeError if operand is of wrong type.
|
|
|
|
>>> a = Epoch(1986, 2, 9.0)
|
|
>>> print(round(a(), 2))
|
|
2446470.5
|
|
>>> b = Epoch(1910, 4, 20.0)
|
|
>>> print(round(b(), 2))
|
|
2418781.5
|
|
>>> c = a - b
|
|
>>> print(round(c, 2))
|
|
27689.0
|
|
>>> a = Epoch(2003, 12, 31.0)
|
|
>>> b = a - 365.5
|
|
>>> y, m, d = b.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
2002/12/30.5
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return Epoch(self._jde - b)
|
|
elif isinstance(b, Epoch):
|
|
return float(self._jde - b._jde)
|
|
else:
|
|
raise TypeError("Invalid operand type")
|
|
|
|
def __iadd__(self, b):
|
|
"""This method defines the accumulative addition to this Epoch.
|
|
|
|
:param b: Value to be added, in days.
|
|
:type b: int, float
|
|
|
|
:returns: This Epoch.
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: TypeError if operand is of wrong type.
|
|
|
|
>>> a = Epoch(2003, 12, 31.0)
|
|
>>> a += 32.5
|
|
>>> y, m, d = a.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
2004/2/1.5
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
self = self + b
|
|
return self
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __isub__(self, b):
|
|
"""This method defines the accumulative subtraction to this Epoch.
|
|
|
|
:param b: Value to be subtracted, in days.
|
|
:type b: int, float
|
|
|
|
:returns: This Epoch.
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: TypeError if operand is of wrong type.
|
|
|
|
>>> a = Epoch(2001, 12, 31.0)
|
|
>>> a -= 2*365
|
|
>>> y, m, d = a.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
2000/1/1.0
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
self = self - b
|
|
return self
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __radd__(self, b):
|
|
"""This method defines the addition to a Epoch by the right
|
|
|
|
:param b: Value to be added, in days.
|
|
:type b: int, float
|
|
|
|
:returns: A new Epoch object.
|
|
:rtype: :py:class:`Epoch`
|
|
:raises: TypeError if operand is of wrong type.
|
|
|
|
>>> a = Epoch(2004, 2, 27.8)
|
|
>>> b = 2.2 + a
|
|
>>> y, m, d = b.get_date()
|
|
>>> print("{}/{}/{}".format(y, m, round(d, 2)))
|
|
2004/3/1.0
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return self.__add__(b) # It is the same as by the left
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __int__(self):
|
|
"""This method returns the internal JDE value as an int.
|
|
|
|
:returns: Internal JDE value as an int.
|
|
:rtype: int
|
|
|
|
>>> a = Epoch(2434923.85)
|
|
>>> int(a)
|
|
2434923
|
|
"""
|
|
|
|
return int(self._jde)
|
|
|
|
def __float__(self):
|
|
"""This method returns the internal JDE value as a float.
|
|
|
|
:returns: Internal JDE value as a float.
|
|
:rtype: float
|
|
|
|
>>> a = Epoch(2434923.85)
|
|
>>> float(a)
|
|
2434923.85
|
|
"""
|
|
|
|
return float(self._jde)
|
|
|
|
def __eq__(self, b):
|
|
"""This method defines the 'is equal' operator between Epochs.
|
|
|
|
.. note:: For the comparison, the **base.TOL** value is used.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> a = Epoch(2007, 5, 20.0)
|
|
>>> b = Epoch(2007, 5, 20.000001)
|
|
>>> a == b
|
|
False
|
|
>>> a = Epoch(2004, 10, 15.7)
|
|
>>> b = Epoch(a)
|
|
>>> a == b
|
|
True
|
|
>>> a = Epoch(2434923.85)
|
|
>>> a == 2434923.85
|
|
True
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return abs(self._jde - float(b)) < TOL
|
|
elif isinstance(b, Epoch):
|
|
return abs(self._jde - b._jde) < TOL
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __ne__(self, b):
|
|
"""This method defines the 'is not equal' operator between Epochs.
|
|
|
|
.. note:: For the comparison, the **base.TOL** value is used.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
|
|
>>> a = Epoch(2007, 5, 20.0)
|
|
>>> b = Epoch(2007, 5, 20.000001)
|
|
>>> a != b
|
|
True
|
|
>>> a = Epoch(2004, 10, 15.7)
|
|
>>> b = Epoch(a)
|
|
>>> a != b
|
|
False
|
|
>>> a = Epoch(2434923.85)
|
|
>>> a != 2434923.85
|
|
False
|
|
"""
|
|
|
|
return not self.__eq__(b) # '!=' == 'not(==)'
|
|
|
|
def __lt__(self, b):
|
|
"""This method defines the 'is less than' operator between Epochs.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> a = Epoch(2004, 10, 15.7)
|
|
>>> b = Epoch(2004, 10, 15.7)
|
|
>>> a < b
|
|
False
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return self._jde < float(b)
|
|
elif isinstance(b, Epoch):
|
|
return self._jde < b._jde
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __ge__(self, b):
|
|
"""This method defines 'is equal or greater' operator between Epochs.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> a = Epoch(2004, 10, 15.71)
|
|
>>> b = Epoch(2004, 10, 15.7)
|
|
>>> a >= b
|
|
True
|
|
"""
|
|
|
|
return not self.__lt__(b) # '>=' == 'not(<)'
|
|
|
|
def __gt__(self, b):
|
|
"""This method defines the 'is greater than' operator between Epochs.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> a = Epoch(2004, 10, 15.71)
|
|
>>> b = Epoch(2004, 10, 15.7)
|
|
>>> a > b
|
|
True
|
|
>>> a = Epoch(-207, 11, 5.2)
|
|
>>> b = Epoch(-207, 11, 5.2)
|
|
>>> a > b
|
|
False
|
|
"""
|
|
|
|
if isinstance(b, (int, float)):
|
|
return self._jde > float(b)
|
|
elif isinstance(b, Epoch):
|
|
return self._jde > b._jde
|
|
else:
|
|
raise TypeError("Wrong operand type")
|
|
|
|
def __le__(self, b):
|
|
"""This method defines 'is equal or less' operator between Epochs.
|
|
|
|
:returns: A boolean.
|
|
:rtype: bool
|
|
:raises: TypeError if input values are of wrong type.
|
|
|
|
>>> a = Epoch(2004, 10, 15.71)
|
|
>>> b = Epoch(2004, 10, 15.7)
|
|
>>> a <= b
|
|
False
|
|
>>> a = Epoch(-207, 11, 5.2)
|
|
>>> b = Epoch(-207, 11, 5.2)
|
|
>>> a <= b
|
|
True
|
|
"""
|
|
|
|
return not self.__gt__(b) # '<=' == 'not(>)'
|
|
|
|
|
|
JDE2000 = Epoch(2000, 1, 1.5)
|
|
"""Standard epoch for January 1st, 2000 at 12h corresponding to JDE2451545.0"""
|
|
|
|
|
|
def main():
|
|
|
|
# Let's define a small helper function
|
|
def print_me(msg, val):
|
|
print("{}: {}".format(msg, val))
|
|
|
|
# Let's do some work with the Epoch class
|
|
print("\n" + 35 * "*")
|
|
print("*** Use of Epoch class")
|
|
print(35 * "*" + "\n")
|
|
|
|
e = Epoch(1987, 6, 19.5)
|
|
print_me("JDE for 1987/6/19.5", e)
|
|
|
|
# Redefine the Epoch object
|
|
e.set(333, "Jan", 27, 12)
|
|
print_me("JDE for 333/1/27.5", e)
|
|
|
|
# We can create an Epoch from a 'date' or 'datetime' object
|
|
d = datetime.datetime(837, 4, 10, 7, 12, 0, 0)
|
|
f = Epoch(d)
|
|
print_me("JDE for 837/4/10.3", f)
|
|
|
|
print("")
|
|
|
|
# Check if a given date belong to the Julian or Gregorian calendar
|
|
print_me("Is 1590/4/21.4 a Julian date?", Epoch.is_julian(1590, 4, 21.4))
|
|
|
|
print("")
|
|
|
|
# We can also check if a given year is leap or not
|
|
print_me("Is -1000 a leap year?", Epoch.is_leap(-1000))
|
|
print_me("Is 1800 a leap year?", Epoch.is_leap(1800))
|
|
print_me("Is 2012 a leap year?", Epoch.is_leap(2012))
|
|
|
|
print("")
|
|
|
|
# Get the Day Of Year corresponding to a given date
|
|
print_me("Day Of Year (DOY) of 1978/11/14", Epoch.get_doy(1978, 11, 14))
|
|
print_me("Day Of Year (DOY) of -400/2/29.9", Epoch.get_doy(-400, 2, 29.9))
|
|
|
|
print("")
|
|
|
|
# Now the opposite: Get a date from a DOY
|
|
t = Epoch.doy2date(2017, 365.7)
|
|
s = str(t[0]) + "/" + str(t[1]) + "/" + str(round(t[2], 2))
|
|
print_me("Date from DOY 2017:365.7", s)
|
|
|
|
t = Epoch.doy2date(-4, 60)
|
|
s = str(t[0]) + "/" + str(t[1]) + "/" + str(round(t[2], 2))
|
|
print_me("Date from DOY -4:60", s)
|
|
|
|
print("")
|
|
|
|
# There is an internal table which we can use to get the leap seconds
|
|
print_me("Number of leap seconds applied up to July 1983",
|
|
Epoch.leap_seconds(1983, 7))
|
|
|
|
print("")
|
|
|
|
# We can convert the internal JDE value back to a date
|
|
e = Epoch(2436116.31)
|
|
y, m, d = e.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("Date from JDE 2436116.31", s)
|
|
|
|
print("")
|
|
|
|
# It is possible to get the day of the week corresponding to a given date
|
|
e = Epoch(2018, "Feb", 15)
|
|
print_me("The day of week of 2018/2/15 is", e.dow(as_string=True))
|
|
|
|
print("")
|
|
|
|
# In some cases it is useful to get the Modified Julian Day (MJD)
|
|
e = Epoch(1923, "August", 23)
|
|
print_me("Modified Julian Day for 1923/8/23", round(e.mjd(), 2))
|
|
|
|
print("")
|
|
|
|
# If your system is appropriately configured, you can get the difference in
|
|
# seconds between your local time and UTC
|
|
print_me(
|
|
"To convert from local system time to UTC you must add/subtract"
|
|
+ " this amount of seconds",
|
|
Epoch.utc2local(),
|
|
)
|
|
|
|
print("")
|
|
|
|
# Compute DeltaT = TT - UT differences for various dates
|
|
print_me("DeltaT (TT - UT) for Feb/333", round(Epoch.tt2ut(333, 2), 1))
|
|
print_me("DeltaT (TT - UT) for Jan/1642", round(Epoch.tt2ut(1642, 1), 1))
|
|
print_me("DeltaT (TT - UT) for Feb/1928", round(Epoch.tt2ut(1928, 1), 1))
|
|
print_me("DeltaT (TT - UT) for Feb/1977", round(Epoch.tt2ut(1977, 2), 1))
|
|
print_me("DeltaT (TT - UT) for Jan/1998", round(Epoch.tt2ut(1998, 1), 1))
|
|
|
|
print("")
|
|
|
|
# The difference between civil day and sidereal day is almost 4 minutes
|
|
e = Epoch(1987, 4, 10)
|
|
st1 = round(e.mean_sidereal_time(), 9)
|
|
e = Epoch(1987, 4, 11)
|
|
st2 = round(e.mean_sidereal_time(), 9)
|
|
ds = (st2 - st1) * DAY2MIN
|
|
msg = "{}m {}s".format(iint(ds), (ds % 1) * 60.0)
|
|
print_me("Difference between sidereal time 1987/4/11 and 1987/4/10", msg)
|
|
|
|
print("")
|
|
|
|
print(
|
|
"When correcting for nutation-related effects, we get the "
|
|
+ "'apparent' sidereal time:"
|
|
)
|
|
e = Epoch(1987, 4, 10)
|
|
print("e = Epoch(1987, 4, 10)")
|
|
print_me(
|
|
"e.apparent_sidereal_time(23.44357, (-3.788)/3600.0)",
|
|
e.apparent_sidereal_time(23.44357, (-3.788) / 3600.0),
|
|
)
|
|
# 0.549145082637
|
|
|
|
print("")
|
|
|
|
# Epoch class can also provide the date of Easter for a given year
|
|
# Let's spice up the output a little bit, calling dow() and get_month()
|
|
month, day = Epoch.easter(2019)
|
|
e = Epoch(2019, month, day)
|
|
s = (
|
|
e.dow(as_string=True)
|
|
+ ", "
|
|
+ str(day)
|
|
+ get_ordinal_suffix(day)
|
|
+ " of "
|
|
+ Epoch.get_month(month, as_string=True)
|
|
)
|
|
print_me("Easter day for 2019", s)
|
|
# I know Easter is always on Sunday, by the way... ;-)
|
|
|
|
print("")
|
|
|
|
# Compute the date of the Jewish Easter (Pesach) for a given year
|
|
month, day = Epoch.jewish_pesach(1990)
|
|
s = (
|
|
str(day)
|
|
+ get_ordinal_suffix(day)
|
|
+ " of "
|
|
+ Epoch.get_month(month, as_string=True)
|
|
)
|
|
print_me("Jewish Pesach day for 1990", s)
|
|
|
|
print("")
|
|
|
|
# Now, convert a date in the Moslem calendar to the Gregorian calendar
|
|
y, m, d = Epoch.moslem2gregorian(1421, 1, 1)
|
|
print_me("The date 1421/1/1 in the Moslem calendar is, in Gregorian " +
|
|
"calendar", "{}/{}/{}".format(y, m, d))
|
|
y, m, d = Epoch.moslem2gregorian(1439, 9, 1)
|
|
print_me(
|
|
"The start of Ramadan month (9/1) for Gregorian year 2018 is",
|
|
"{}/{}/{}".format(y, m, d),
|
|
)
|
|
# We can go from the Gregorian calendar back to the Moslem calendar too
|
|
print_me(
|
|
"Date 1991/8/13 in Gregorian calendar is, in Moslem calendar",
|
|
"{}/{}/{}".format(*Epoch.gregorian2moslem(1991, 8, 13)),
|
|
)
|
|
# Note: The '*' before 'Epoch' will _unpack_ the tuple into components
|
|
|
|
print("")
|
|
|
|
# It is possible to carry out some algebraic operations with Epochs
|
|
|
|
# Add 10000 days to a given date
|
|
a = Epoch(1991, 7, 11)
|
|
b = a + 10000
|
|
y, m, d = b.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("1991/7/11 plus 10000 days is", s)
|
|
|
|
# Subtract two Epochs to find the number of days between them
|
|
a = Epoch(1986, 2, 9.0)
|
|
b = Epoch(1910, 4, 20.0)
|
|
print_me("The number of days between 1986/2/9 and 1910/4/20 is",
|
|
round(a - b, 2))
|
|
|
|
# We can also subtract a given amount of days from an Epoch
|
|
a = Epoch(2003, 12, 31.0)
|
|
b = a - 365.5
|
|
y, m, d = b.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("2003/12/31 minus 365.5 days is", s)
|
|
|
|
# Accumulative addition and subtraction of days is also allowed
|
|
a = Epoch(2003, 12, 31.0)
|
|
a += 32.5
|
|
y, m, d = a.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("2003/12/31 plus 32.5 days is", s)
|
|
|
|
a = Epoch(2001, 12, 31.0)
|
|
a -= 2 * 365
|
|
y, m, d = a.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("2001/12/31 minus 2*365 days is", s)
|
|
|
|
# It is also possible to add days from the right
|
|
a = Epoch(2004, 2, 27.8)
|
|
b = 2.2 + a
|
|
y, m, d = b.get_date()
|
|
s = str(y) + "/" + str(m) + "/" + str(round(d, 2))
|
|
print_me("2004/2/27.8 plus 2.2 days is", s)
|
|
|
|
print("")
|
|
|
|
# Comparison operadors between epochs are also defined
|
|
a = Epoch(2007, 5, 20.0)
|
|
b = Epoch(2007, 5, 20.000001)
|
|
print_me("2007/5/20.0 == 2007/5/20.000001", a == b)
|
|
print_me("2007/5/20.0 != 2007/5/20.000001", a != b)
|
|
print_me("2007/5/20.0 > 2007/5/20.000001", a > b)
|
|
print_me("2007/5/20.0 <= 2007/5/20.000001", a <= b)
|
|
|
|
print("")
|
|
|
|
# Compute the time of rise and setting of the Sun in a given day
|
|
e = Epoch(2018, 5, 2)
|
|
print("On May 2nd, 2018, Sun rising/setting times in Munich were (UTC):")
|
|
latitude = Angle(48, 8, 0)
|
|
longitude = Angle(11, 34, 0)
|
|
altitude = 520.0
|
|
rising, setting = e.rise_set(latitude, longitude, altitude)
|
|
y, m, d, h, mi, s = rising.get_full_date()
|
|
print("Rising time: {}:{}".format(h, mi)) # 3:50
|
|
y, m, d, h, mi, s = setting.get_full_date()
|
|
print("Setting time: {}:{}".format(h, mi)) # 18:33
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|