Opa_omroep-automatiseren/venv/lib/python3.8/site-packages/pymeeus/Epoch.py
Eljakim Herrewijnen f26bbbf103 initial
2020-12-27 21:00:11 +01:00

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()