# -*- coding: utf-8 -*-
#
# badidatetime/base_calendar.py
#
__docformat__ = "restructuredtext en"
import math
from badidatetime.julian_period import JulianPeriod
from badidatetime._astronomical_terms import AstronomicalTerms
[docs]
class BaseCalendar(AstronomicalTerms, JulianPeriod):
"""
Basic functionality used with all calendars.
U.T. = Mean solar time at Greenwich, England (0◦ meridian), reckoned from
midnight; sometimes GMT, Greenwich Mean Time
Transformations between Time Systems:
| https://gssc.esa.int/navipedia/index.php/Transformations_between_Time_Systems
"""
_HR = lambda self, x: x / 24
_MN = lambda self, x: x / 24 / 60
_SEC = lambda self, x: x / 24 / 60 / 60
_MINS = lambda self, x: x / 60
_SECS = lambda self, x: x / 3600
# Convert microseconds to a partial second.
_US = lambda self, x: x / 1e6
_ANGLE = lambda self, d, m, s: d + (m + s / 60) / 60 # 0 - 360
_AMOD = lambda self, x, y: y + x % -y
_MOD3 = lambda self, x, a, b: x if a == b else (
a + math.fmod((x - a), (b - a)))
_QUOTIENT = lambda self, m, n: math.floor(m / n)
# The inline functions below will assume that 0 is midnight, if
# converting from a Julian Period day add 0.5 to the value before
# calling the function.
_PARTIAL_DAY_TO_HOURS = lambda self, x: round((x % 1) * 24, 6)
_PARTIAL_HOUR_TO_MINUTE = lambda self, x: round((x % 1) * 60, 6)
_PARTIAL_MINUTE_TO_SECOND = _PARTIAL_HOUR_TO_MINUTE
_PARTIAL_SECOND_TO_MICROSECOND = lambda self, x: int(round(x % 1, 6) * 1e6)
_MEAN_TROPICAL_YEAR = 365.2421897
# MEAN_SIDEREAL_YEAR = 365.256363004
_MORNING = True
_EVENING = False
# Seasons set to degrees.
_SPRING = 0
_SUMMER = 90
_AUTUMN = 180
_WINTER = 270
_SUN_OFFSET = 0.8333
_MOON_OFFSET = 0.125
_STARS_PLANET_OFFSET = 0.5667
_ROUNDING_PLACES = 12
# This is using the proleptic astronomically algorithm.
# The Meeus value is 2440587.5
_POSIX_EPOCH = 2440585.5
_JULIAN_CAL_EPOCH = 1721423.5
_SECONDS_PER_DAY = 86400
def __init__(self, *args, **kwargs):
self._time = None
self._nutation = {'lon': (0, 0, False), 'obl': (0, 0, False)}
self._sun_tss = {'trn': (), 'rsn': (), 'stn': ()}
#
# Meeus Astronomical Algorithms
#
[docs]
def _delta_t(self, jd: float, *, seconds: bool=False) -> float:
"""
Calculate the value of ΔT = TD − UT, for TD = ΔT + UT, and for
UT = ΔT - TD. Only the year and month are considered, days, hours,
minutes, and seconds are ignored.
:param float jd: Julian day.
:param bool seconds: If True leave as seconds in a minute else convert
to seconds of a day (default).
:returns: The delta t.
:rtype: float
.. note::
See: http://eclipse.gsfc.nasa.gov/SEcat5/deltatpoly.html
"""
from .gregorian_calendar import GregorianCalendar
gc = GregorianCalendar()
g_date = gc.gregorian_date_from_jd(jd)
year = g_date[0] + (g_date[1] - 0.5) / 12
func = lambda year: -20 + 32 * ((year - 1820) / 100)**2
if year < -500:
dt = func(year)
elif year < 500:
u = year / 100
dt = self._poly(u, (10583.6, -1014.41, 33.78311, -5.952053,
-0.1798452, 0.022174192, 0.0090316521))
elif year < 1600:
u = (year - 1000) / 100
dt = self._poly(u, (1574.2, -556.01, 71.23472, 0.319781,
-0.8503463, -0.005050998, 0.0083572073))
elif year < 1700:
u = year - 1600
dt = self._poly(u, (120, -0.9808, -0.01532, 1 / 7129))
elif year < 1800:
u = year - 1700
dt = self._poly(u, (8.83, 0.1603, -0.0059285, 0.00013336,
-1 / 1174000))
elif year < 1860:
u = year - 1800
dt = self._poly(u, (13.72, -0.332447, 0.0068612, 0.0041116,
-0.00037436, 0.0000121272, -0.0000001699,
0.000000000875))
elif year < 1900:
u = year - 1860
dt = self._poly(u, (7.62, 0.5737, -0.251754, 0.01680668,
-0.0004473624, 1 / 233174))
elif year < 1920:
u = year - 1900
dt = self._poly(u, (-2.79, 1.494119, -0.0598939, 0.0061966,
-0.000197))
elif year < 1941:
u = year - 1920
dt = self._poly(u, (21.20, 0.84493, -0.0761, 0.0020936))
elif year < 1961:
u = year - 1950
dt = self._poly(u, (29.07, 0.407, -1 / 233, 1 / 2547))
elif year < 1986:
u = year - 1975
dt = self._poly(u, (45.45, 1.067, -1 / 260, -1 / 718))
elif year < 2005:
u = year - 2000
dt = self._poly(u, (63.86, 0.3345, -0.060374, 0.0017275,
0.000651814, 0.00002373599))
elif year < 2050:
u = year - 2000
dt = self._poly(u, (62.92, 0.32217, 0.005589))
elif year < 2150:
dt = -20 + 32 * ((year - 1820) / 100)**2 - 0.5628 * (2150 - year)
else: # 2150 >= year
dt = func(year)
# Convert to seconds of a day where 66.9 dt == 2010
# Seconds of a day are 0.0007743055555555556
return dt if seconds else dt / self._SECONDS_PER_DAY
[docs]
def _mean_sidereal_time_greenwich(self, tc: float) -> float:
"""
Mean sidereal time at Greenwich. (GMST) If the hour angle is
measured with respect to the mean equinox, mean sidereal time is
being measured.
:param float tc: Julian century.
:returns: The mean sidereal time at Greenwich .
:rtype: float
.. note::
Meeus--AA ch. 12 p. 88 Eq. 12.3
"""
return self._coterminal_angle(280.46061837 + 360.98564736629 *
(tc * 36525) + 0.000387933 *
tc**2 - tc**3 / 38710000)
[docs]
def _apparent_sidereal_time_greenwich(self, tc: float) -> float:
"""
The apparent sidereal time, or the Greenwich. (GAST) If the hour
angle is measured with respect to the true equinox, apparent
sidereal time is being measured.
:param float tc: Julian century.
:returns: The apparent sidereal time at Greenwich.
:rtype: float
.. note::
Meeus--AA ch. 12 p. 88
"""
t0 = self._mean_sidereal_time_greenwich(tc)
eps = self._true_obliquity_of_ecliptic(tc)
d_psi = self._nutation_longitude(tc)
return self._coterminal_angle(t0 + d_psi * self._cos_deg(eps))
[docs]
def _altitude(self, delta: float, lat: float, h: float) -> float:
"""
Altitude in degrees, positive above the horizon, negative below.
:param float delta: Declination in sidereal time.
:param float lat: Geographic latitude.
:param float h: Local hour angle.
:returns: Altitude in degrees.
:rtype: float
.. note::
Meeus--AA p. 93 Eq. 13.6
"""
return math.degrees(math.asin(
self._sin_deg(lat) * self._sin_deg(delta) + self._cos_deg(lat) *
self._cos_deg(delta) * self._cos_deg(h)))
[docs]
def _approx_local_hour_angle(self, tc: float, lat: float,
offset: float=_SUN_OFFSET) -> float:
"""
Approximate local hour angle, measured westwards from the south
in degrees.
Hour angle, in astronomy, the angle between an observer’s meridian
(a great circle passing over his head and through the celestial
poles) and the hour circle (any other great circle passing through
the poles) on which some celestial body lies. This angle, when
expressed in hours and minutes, is the time elapsed since the
celestial body’s last transit of the observer’s meridian.
:param float tc: Julian century.
:param float lat: Latitude in decimal
:param float offset: A constant “standard” altitude, i.e., the
geometric altitude of the center of the body at
the time of apparent rising or setting, namely,
h0 = -0°34’ = -0°5667 for stars and planets;
h0 = -0°50' = -0°8333 for the Sun.
Default is _SUN_OFFSET, _STARS_PLANET_OFFSET can
also be used.
:returns: The approximate local hour angle in degrees.
:rtype: float
.. note::
1. Meeus--AA p. 101, 102
2. If the result of the equation is negative then add 360°
(6.283185307179586 radians). If result is greater than
360° then subtract 360° (6.283185307179586 radians).
https://www.quora.com/How-do-I-calculate-the-hour-angle
"""
delta = self._sun_apparent_declination(tc)
cos_h0 = ((self._sin_deg(-offset) - self._sin_deg(lat) *
self._sin_deg(delta)) / (self._cos_deg(lat) *
self._cos_deg(delta)))
if cos_h0 < -1 or cos_h0 > 1:
cos_h0 -= math.floor(cos_h0)
return math.degrees(math.acos(cos_h0))
[docs]
def _sun_transit(self, jd: float, lon: float) -> float:
"""
The transit is when the body crosses the local meridian at upper
culmination.
:param float jd: Julian day in UT.
:param float lon: Geographic longitude positive east negative west.
:returns: The center point between sunrise and sunset.
:rtype: float
.. note::
Meeus-AA ch. 15 p. 102, 103 Eq. 15.1, 15.2
"""
func0 = lambda m: m + 1 if m <= 0 else m - 1 if m >= 1 else m
tc = self._julian_centuries(jd)
dt = self._delta_t(jd)
tc_td = dt / 36525 + tc # Compensate for the Julian Century
alpha = self._sun_apparent_right_ascension(tc_td)
ast = self._apparent_sidereal_time_greenwich(tc)
m = func0((alpha - lon - ast) / 360)
md = self._transit_correction(tc, ast, dt, lon, m)
return m + md
[docs]
def _transit_correction(self, tc: float, ast: float, dt: float, lon: float,
m: float) -> float:
"""
Find the correction to the transit.
:param float tc: The Julian Period century.
:param float ast: The apparent sidereal time at Greenwich.
:param float dt: The delta T of the JD.
:param float lon: The longitude.
:param float m: Times on day, expressed as fractions.
:returns: The correction to the transit.
:rtype: float
"""
srt = ast + 360.98564736629 * m
n = m + dt / self._SECONDS_PER_DAY
ra0 = self._sun_apparent_right_ascension(tc - (1 / 36525))
ra1 = self._sun_apparent_right_ascension(tc)
ra2 = self._sun_apparent_right_ascension(tc + (1 / 36525))
alpha = self._interpolation_from_three(ra0, ra1, ra2, n, True)
h = self._local_hour_angle(srt, lon, alpha)
return -h / 360
[docs]
def _sun_rising(self, jd: float, lat: float, lon: float, *,
offset: float=_SUN_OFFSET) -> float:
"""
Find the jd for sunrise of the given jd.
:param float jd: Julian day in UT.
:param float lat: Geographic latitude positive north negative south.
:param float lon: Geographic longitude positive east negative west.
:param bool offset: A constant “standard” altitude, i.e., the geometric
altitude of the center of the body at the time of
apparent rising or setting, namely,
h0 = -0°34’ = -0°5667 for stars and planets;
h0 = -0°50' = -0°8333 for the Sun.
Default is _SUN_OFFSET, _STARS_PLANET_OFFSET can
also be used.
:returns: The jd moment of the sunrise.
:rtype: float
.. note::
Meeus-AA ch. 15 p. 102, 103 Eq. 15.1, 15.2
"""
# We need to compensate for the longitude alignment in the JD for
# time zones, this has minimal effect in most locales, but fixes some.
lon_frac = lon / 360.0
jd_local = jd + lon_frac
jd0 = math.floor(jd_local + 0.5) - 0.5
jd0 -= lon_frac
m = self._rising_setting(jd0, lat, lon, offset=offset, sr_ss='RISE')
return round(jd0 + m, self._ROUNDING_PLACES)
[docs]
def _sun_setting(self, jd: float, lat: float, lon: float, *,
offset: float=_SUN_OFFSET) -> float:
"""
Find the jd for sunset of the given jd.
:param float jd: Julian day in UT.
:param float lat: Geographic latitude positive north negative south.
:param float lon: Geographic longitude positive east negative west.
:param bool offset: A constant “standard” altitude, i.e., the geometric
altitude of the center of the body at the time of
apparent rising or setting, namely,
h0 = -0°34’ = -0°5667 for stars and planets;
h0 = -0°50' = -0°8333 for the Sun.
Default is _SUN_OFFSET, _STARS_PLANET_OFFSET can
also be used.
:returns: The jd moment of the sunset.
:rtype: float
.. note::
Meeus-AA ch. 15 p. 102, 103 Eq. 15.1, 15.2
"""
# We need to compensate for the longitude alignment in the JD for
# time zones, this has minimal effect in most locales, but fixes some.
lon_frac = lon / 360.0
jd_local = jd + lon_frac
jd0 = math.floor(jd_local + 0.5) - 0.5
jd0 -= lon_frac
m = self._rising_setting(jd0, lat, lon, offset=offset, sr_ss='SET')
return round(jd0 + m, self._ROUNDING_PLACES)
[docs]
def _rising_setting(self, jd: float, lat: float, lon: float, *,
offset: float=_SUN_OFFSET, sr_ss: str='RISE') -> float:
"""
Find the jd difference for sunrise or sunset of the given jd.
:param float jd: Julian day in UT.
:param float lat: Geographic latitude positive north negative south.
:param float lon: Geographic longitude positive east negative west.
:param float offset: A constant “standard” altitude, i.e., the
geometric altitude of the center of the body at
the time of apparent rising or setting, namely,
h0 = -0°34’ = -0°5667 for stars and planets;
h0 = -0°50' = -0°8333 for the Sun.
Default is _SUN_OFFSET, _STARS_PLANET_OFFSET can
also be used.
:param str sr_ss: If 'RISE' return the sunrise else return sunset.
:returns: The offset that would be added to the currant date.
:rtype: float
.. note::
Meeus-AA ch. 15 p. 102, 103 Eq. 15.1, 15.2
"""
flags = ('RISE', 'SET')
sr_ss = sr_ss.upper()
assert sr_ss in flags, (
f"Invalid value, should be one of '{flags}' found '{sr_ss}'.")
tc = self._julian_centuries(jd)
dt = self._delta_t(jd)
tc_td = dt / 36525 + tc # Compensate for the Julian Century
alpha = self._sun_apparent_right_ascension(tc_td)
ast = self._apparent_sidereal_time_greenwich(tc)
h0 = self._approx_local_hour_angle(tc, lat, offset=offset)
# Normalize to [0,1]
m0 = ((alpha - lon - ast) / 360) % 1
m = m0 - h0 / 360 if sr_ss == 'RISE' else m0 + h0 / 360
for _ in range(3):
dm = self._rise_set_correction(tc, ast, dt, lat, lon, m, offset)
m += dm
if abs(dm) < 0.0001:
break
return m % 1
[docs]
def _rise_set_correction(self, tc: float, ast: float, dt: float,
lat: float, lon: float, m: float, offset: float
) -> float:
"""
Find the correction to the sunrise and sunset.
:param float tc: The Julian Period century.
:param float ast: The apparent sidereal time at Greenwich.
:param float dt: The delta T of the JD.
:param float lat: The latitude.
:param float lon: The longitude.
:param float m: Times on day, expressed as fractions.
:param float offset: Either `_SUN_OFFSET` or `_STARS_PLANET_OFFSET`.
:returns: The correction to the sunrise or sunset.
:rtype: float
"""
srt = ast + 360.98564736629 * m
n = m + dt / self._SECONDS_PER_DAY
ra0 = self._sun_apparent_right_ascension(tc - (1 / 36525))
ra1 = self._sun_apparent_right_ascension(tc)
ra2 = self._sun_apparent_right_ascension(tc + (1 / 36525))
de0 = self._sun_apparent_declination(tc - (1 / 36525))
de1 = self._sun_apparent_declination(tc)
de2 = self._sun_apparent_declination(tc + (1 / 36525))
alpha = self._interpolation_from_three(ra0, ra1, ra2, n, True)
delta = self._interpolation_from_three(de0, de1, de2, n)
h = self._local_hour_angle(srt, lon, alpha)
alt = self._altitude(delta, lat, h)
return (alt + offset) / (360 * self._cos_deg(delta) *
self._cos_deg(lat) * self._sin_deg(h))
[docs]
def _local_hour_angle(self, srt: float, lon: float, alpha: float) -> float:
"""
The LHA is the angle between the meridian of the observer and the
meridian of the geographical position of the celestial body.
:param float srt: Sidereal time.
:param float lon: The Geographic longitude of the observer in degrees.
:param float alpha: The apparent right ascensions.
:returns: The local hour angle.
:rtype: float
.. note::
| Meeus-AA p.103
| https://astronavigationdemystified.com/local-hour-angle-and-greenwich-hour-angle/
"""
h = self._coterminal_angle(srt + lon - alpha)
return h - 360 if h > 180 else h
[docs]
def _nutation_longitude(self, tc: float, *, degrees: bool=False) -> float:
"""
Nutation longitude of the Earth's axis around it's 'mean' position.
:param float tc: The Julian Period century.
:param bool degrees: If `False` (default) return radians else if
`True` return degrees.
:returns: Either the radians or degrees depending on the `degrees`
argument.
:rtype: float
"""
tc_day, lon_sum, deg = self._nutation['lon']
if tc != tc_day or degrees != deg:
lon_sum, obl_sum = self._nutation_obliquity_longitude(
tc, degrees=degrees)
self._nutation['lon'] = (tc, lon_sum, degrees)
self._nutation['obl'] = (tc, obl_sum, degrees)
return self._nutation['lon'][1]
[docs]
def _nutation_obliquity(self, tc: float, *, degrees: bool=False) -> float:
"""
Nutation obliquity of the Earth's equator around it's 'mean' position.
:param float tc: The Julian Period century.
:param bool degrees: If `False` (default) return radians else if
`True` return degrees.
:returns: The nutation of obliquity.
:rtype: float
"""
tc_day, obj_sum, deg = self._nutation['obl']
if tc != tc_day or degrees != deg:
lon_sum, obl_sum = self._nutation_obliquity_longitude(
tc, degrees=degrees)
self._nutation['lon'] = (tc, lon_sum, degrees)
self._nutation['obl'] = (tc, obl_sum, degrees)
return self._nutation['obl'][1]
[docs]
def _nutation_obliquity_longitude(self, tc: float, degrees: bool=False
) -> float:
"""
Nutation of the Earth's axis around it's 'mean' position.
:param float tc: Time in Julian centuries.
:param float degrees: If True units of degrees is returned, if False
units of radians is returned.
:returns: Moon latitude.
:rtype: float
.. note::
See: https://articles.adsabs.harvard.edu/full/seri/CeMec/0027/0000079.000.html
"""
lm = self._moon_mean_anomaly(tc)
ls = self._sun_earth_mean_anomaly(tc)
ff = self._moon_latitude(tc)
dd = self._mean_moon_elongation(tc)
om = self._moon_ascending_node_longitude(tc)
# W = LM*lm + LS*ls + F*ff + D*dd + OM*om
# T = tc
# Where LM, LS, F, D, and OM are from the nutation periodic terms.
# The nutation in longitude is a sum of terms of the form
# (psi_sin + sin * T) * sin(W).
# Where psi_sin and t_sin are from the nutation periodic terms
# and T is the Julian time from J2000.
# The obliquity in latitude is a sum of terms of the form
# (eps_cos + cos * T) * cos(W).
# Where eps_cos and t_cos are from the nutation periodic terms
# and T is the Julian time from J2000.
lon_sum = 0
obl_sum = 0
for LM, LS, F, D, OM, day, psi_sin, sin, eps_cos, cos in self._NUT:
w = LM*lm + LS*ls + F*ff + D*dd + OM*om
lon_sum += (psi_sin + sin * tc) * self._sin_deg(w)
obl_sum += (eps_cos + cos * tc) * self._cos_deg(w)
lon_sum /= 36000000
obl_sum /= 36000000
if degrees:
lon_sum = self._coterminal_angle(math.degrees(lon_sum))
obl_sum = self._coterminal_angle(math.degrees(obl_sum))
return lon_sum, obl_sum
[docs]
def _moon_mean_anomaly(self, tc: float) -> float:
"""
The position of the moon in its orbit around the Earth.
:param float tc: Time in Julian centuries.
:returns: Moon mean anomaly.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 144
| Referenced by lm (M').
"""
return self._coterminal_angle(self._poly(
tc, (134.96298, 477198.867398, 0.0086972, 1 / 56250)))
[docs]
def _sun_earth_mean_anomaly(self, tc: float) -> float:
"""
The position of the Earth in its orbit around the Sun.
:param float tc: Time in Julian centuries.
:returns: Sun and earth mean anomaly.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 144
| Referenced by ls (M).
"""
return self._coterminal_angle(self._poly(
tc, (357.52772, 35999.05034, -0.0001603, -1 / 300000)))
[docs]
def _moon_latitude(self, tc: float) -> float:
"""
The angle between the Moon’s ecliptic longitude and its mean
longitude.
:param float tc: Time in Julian centuries.
:returns: Moon latitude.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 144
| Referenced by ff (F).
"""
return self._coterminal_angle(self._poly(
tc, (93.27191, 483202.017538, -0.0036825, 1 / 327270)))
[docs]
def _mean_moon_elongation(self, tc: float) -> float:
"""
:param float tc: Time in Julian centuries.
:returns: Mean moon elongation.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 144
| Referenced by dd (D).
"""
return self._coterminal_angle(self._poly(
tc, (297.85036, 445267.11148, -0.0019142, 1 / 189474)))
[docs]
def _moon_ascending_node_longitude(self, tc: float) -> float:
"""
Longitude of the ascending node of the Moon’s mean orbit on the
ecliptic, measured from the mean equinox of the date:
:param float tc: Time in Julian centuries.
:returns: Moon ascending node longitude.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 144
| Referenced by om (omega).
"""
return self._coterminal_angle(self._poly(
tc, (125.04452, -1934.136261, 0.0020708, 1 / 450000)))
[docs]
def _true_obliquity_of_ecliptic(self, tc: float) -> float:
"""
The obliquity of the ecliptic, or inclination of the Earth’s axis
of rotation, is the angle between the equator and the ecliptic.
:param float jde: Julian century.
:returns: The obliquity of the ecliptic in degrees as a decimal.
:rtype: float
.. note::
| Meeus--AA ch. 22 p. 147 Eq. 22.3
| Convert lots of things:
| https://www.xconvert.com/unit-converter/arcseconds-to-degrees
"""
u = tc / 100
mean_ob = self._poly(u, (23.43929111111111, -1.3002583333333335,
-0.00043055555555555555, 0.5553472222222222,
-0.014272222222222223, -0.06935277777777778,
-0.010847222222222222, 0.001977777777777778,
0.007741666666666667, 0.0016083333333333334,
0.0006805555555555556))
return mean_ob + self._nutation_obliquity(tc)
[docs]
def _sun_mean_longitude(self, tc: float) -> float:
"""
The geometric mean longitude of the Sun, referred to the mean
equinox of the date.
:param float tc: Time in Julian centuries.
:returns: Mean longitude of the sun in degrees.
:rtype: float
.. note::
| Meeus--AA ch. 25 p. 163 Eq. 25.2
| References by L0
"""
return self._coterminal_angle(self._poly(
tc, (280.46646, 36000.76983, 0.0003032)))
[docs]
def _eccentricity_earth_orbit(self, tc: float) -> float:
"""
The eccentricity of the earth's orbit.
:param float tc: Time in Julian centuries.
:returns: The eccentricity of the earth's orbit.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 163 Eq. 25.4
"""
return self._poly(tc, (0.016708634, -0.000042037, -0.0000001267))
[docs]
def _sun_equation_of_center(self, tc: float) -> float:
"""
Describes the difference between the true anomaly (the actual
angular position of the Earth in its orbit around the Sun) and the
mean anomaly (the position the Earth would have if its orbit were
perfectly circular and uniform). This difference is caused by the
elliptical shape of Earth’s orbit and the resulting non-uniform
motion of the Earth around the Sun.
:param float tc: Time in Julian centuries.
:returns: The Sun's equation of it's center.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 164
"""
m = self._sun_earth_mean_anomaly(tc)
return ((1.914602 - 0.004817 * tc - 0.000014 * tc**2) *
self._sin_deg(m) + (0.019993 - 0.000101 * tc) *
self._sin_deg(2 * m) + 0.000290 * self._sin_deg(3 * m))
[docs]
def _sun_true_longitude(self, tc: float) -> float:
"""
The true geometric longitude referred to the mean equinox of the
date. This longitude is the quantity required for instance in the
calculation of geocentric planetary positions.
:param float tc: Time in Julian centuries.
:returns: The true geometric longitude.
:rtype: float
.. note::
Meeus--AA p. 164
"""
l0 = self._sun_mean_longitude(tc)
cen = self._sun_equation_of_center(tc)
return l0 + cen
[docs]
def _sun_apparent_longitude(self, tc: float) -> float:
"""
The Sun’s apparent longitude is the angle measured from the vernal
equinox to the Sun’s position on the ecliptic plane as seen from
Earth.
:param float tc: Time in Julian centuries.
:returns: The Sun’s apparent longitude.
:rtype: float
.. note::
Meeus--AA p. 164
This has a less accurate result. apparent_solar_longitude()
should be more accurate.
"""
sol = self._sun_true_longitude(tc)
om = self._moon_ascending_node_longitude(tc)
return sol - 0.00569 - 0.00478 * self._sin_deg(om)
[docs]
def _sun_apparent_right_ascension(self, tc: float) -> float:
"""
Right ascension is measured (from 0 to 24 hours, sometimes from 0°
to 360°) from the vernal equinox, positive to the east, along the
celestial equator.
:param float tc: Julian century.
:returns: The apparent declination of the sun in radians.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 165 Eq. 25.6
"""
om = self._moon_ascending_node_longitude(tc)
eps = (self._true_obliquity_of_ecliptic(tc) + 0.00256 *
self._cos_deg(om))
lam = self._sun_apparent_longitude(tc)
alpha = math.degrees(math.atan2(self._cos_deg(eps) *
self._sin_deg(lam),
self._cos_deg(lam)))
return self._coterminal_angle(alpha)
[docs]
def _sun_apparent_declination(self, tc: float) -> float:
"""
Declination is measured (from 0° to +90°) from the equator, positive
to the north, negative to the south.
:param float tc: Julian century.
:param bool app: If True the apparent declination is returned, if
:returns: The apparent declination of the sun in radians.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 165 Eq. 25.7
"""
om = self._moon_ascending_node_longitude(tc)
eps = (self._true_obliquity_of_ecliptic(tc) + 0.00256 *
self._cos_deg(om))
lam = self._sun_apparent_longitude(tc)
return math.degrees(math.asin(self._sin_deg(eps) * self._sin_deg(lam)))
[docs]
def _heliocentric_ecliptical_longitude(self, tm: float,
degrees: bool=False) -> float:
"""
Find the heliocentric elliptical longitude.
:param float tm: The moment in time referenced to J2000 millennia.
:param bool degrees: The results if False are radians, else True
are degrees. Default is False.
:returns: Longitude in degrees or radians.
:rtype: float
.. note::
| Meeus--AA ch. 25 p. 166
| Referenced by L
"""
func = lambda a, b, c: a * math.cos(b + c * tm)
l0 = self._sigma((self._L0_A, self._L0_B, self._L0_C), func)
l1 = self._sigma((self._L1_A, self._L1_B, self._L1_C), func)
l2 = self._sigma((self._L2_A, self._L2_B, self._L2_C), func)
l3 = self._sigma((self._L3_A, self._L3_B, self._L3_C), func)
l4 = self._sigma((self._L4_A, self._L4_B, self._L4_C), func)
l5 = self._sigma((self._L5_A, self._L5_B, self._L5_C), func)
l = self._poly(tm, (l0, l1, l2, l3, l4, l5)) / 10**8
return round(self._coterminal_angle(math.degrees(l)) if degrees else l,
self._ROUNDING_PLACES)
[docs]
def _heliocentric_ecliptical_latitude(self, tm: float,
degrees: bool=False) -> float:
"""
Find the heliocentric elliptical latitude.
:param float tm: The moment in time referenced to J2000 millennia.
:param bool degrees: The results if False are radians, else True
are degrees. Default is False.
:returns: Latitude in degrees or radians.
:rtype: float
.. note::
| Meeus--AA ch. 25 p. 166
| Referenced by B
"""
func = lambda a, b, c: a * math.cos(b + c * tm)
b0 = self._sigma((self._B0_A, self._B0_B, self._B0_C), func)
b1 = self._sigma((self._B1_A, self._B1_B, self._B1_C), func)
b = self._poly(tm, (b0, b1)) / 10**8
return round(self._coterminal_angle(math.degrees(b)) if degrees else b,
self._ROUNDING_PLACES)
[docs]
def _radius_vector(self, tm: float, degrees: bool=False) -> float:
"""
Find the distance of earth to the sun.
:param float tm: The moment in time referenced to J2000 millennia.
:param bool degrees: The results if False are radians, else True
are degrees. Default is False.
:returns: Radius vector in degrees or radians.
:rtype: float
.. note::
| Meeus--AA ch. 25 p. 166
| Referenced by R
"""
func = lambda a, b, c: a * math.cos(b + c * tm)
r0 = self._sigma((self._R0_A, self._R0_B, self._R0_C), func)
r1 = self._sigma((self._R1_A, self._R1_B, self._R1_C), func)
r2 = self._sigma((self._R2_A, self._R2_B, self._R2_C), func)
r3 = self._sigma((self._R3_A, self._R3_B, self._R3_C), func)
r4 = self._sigma((self._R4_A, self._R4_B, self._R4_C), func)
r = self._poly(tm, (r0, r1, r2, r3, r4)) / 10**8
return round(self._coterminal_angle(math.degrees(r)) if degrees else r,
self._ROUNDING_PLACES)
[docs]
def _apparent_solar_longitude(self, jde: float,
degrees: bool=True) -> float:
"""
Find the apparent solar longitude.
:param float jde: The Julian Period day.
:param bool degrees: The results if False are radians, else True
are degrees. Default is False.
:returns: The apparent solar longitude.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 166
"""
tm = self._julian_millennia(jde)
l = self._heliocentric_ecliptical_longitude(tm, degrees=False)
l += math.pi
# Convert to FK5 notation
l -= math.radians(2.5091666666666666e-05) # -0".09033
# Convert to centuries
l += self._nutation_longitude(tm * 10, degrees=False)
# eq 25.11
l += self._aberration(tm)
if degrees:
l = self._coterminal_angle(math.degrees(l))
return round(l, self._ROUNDING_PLACES)
[docs]
def _apparent_solar_latitude(self, jde: float,
degrees: bool=True) -> float:
"""
Find the apparent solar latitude.
:param float jde: The Julian Period day.
:param bool degrees: The results if False are radians, else True
are degrees. Default is False.
:returns: The apparent solar latitude.
:rtype: float
.. note::
Meeus--AA ch. 25 p. 166
"""
tm = self._julian_millennia(jde)
tc = tm * 10 # Convert millennia to centuries
b = self._heliocentric_ecliptical_latitude(tm)
b *= -1 # Invert the result
# Convert to FK5 notation
l = self._apparent_solar_longitude(jde, degrees=False)
b1 = self._poly(tc, (l, -1.397, -0.00031))
bd = (math.radians(0.000010877777777777778) *
(math.cos(b1) - math.sin(b1)))
b += bd
if degrees:
b = self._coterminal_angle(math.degrees(b))
return round(b, self._ROUNDING_PLACES)
[docs]
def _aberration(self, tm: float, fixed: bool=True) -> float:
"""
Find the aberration of a date with respect to a fixed reference
frame or to the mean equinox.
:param float tc: The moment in time referenced to J2000 millennia.
:param bool fixed: If `True` (default) the results is to a fixed
reference frame, if `False` the result is
referenced to the mean equinox.
:returns: The aberration of the date in degrees.
:rtype: float
.. note::
AA p.167 Eq. 25.11, p. 168
"""
if fixed:
aberration = 3548.193
else:
aberration = 3548.33
for a, b, c in self._ABER_A:
aberration += math.radians(a) * self._sin_deg(b + c * tm)
for a, b, c in self._ABER_B:
aberration += math.radians(a) * tm * self._sin_deg(b + c * tm)
for a, b, c in self._ABER_C:
aberration += math.radians(a) * tm**2 * self._sin_deg(b + c * tm)
r = self._radius_vector(tm, degrees=False)
return round(self._decimal_from_dms(
0, 0, -0.005775518 * r * aberration), self._ROUNDING_PLACES)
[docs]
def _approx_julian_day_for_equinoxes_or_solstices(self, g_year: int,
lam: int=_SPRING
) -> float:
"""
Find the approximate Julian day for the equinoxes or solstices.
:param int g_year: The Gregorian year.
:param int lam: The lambda, either `_SPRING` (default), `_SUMMER`,
`_AUTUMN`, or `_WINTER`.
:returns: The approximate Julian day for the equinoxes or solstices.
:rtype: float
.. note::
See: Meeus AA ch. 27 p. 177
"""
if g_year <= 1000:
y = g_year / 1000
if lam == self._SPRING:
jde = self._poly(y, (1721139.29189, 365242.1374, 0.06134,
0.00111, -0.00071))
elif lam == self._SUMMER:
jde = self._poly(y, (1721233.25401, 365241.72562, -0.05323,
0.00907, 0.00025))
elif lam == self._AUTUMN:
jde = self._poly(y, (1721325.70455, 365242.49558, -0.11677,
-0.00297, 0.00074))
else: # lam == self._WINTER:
jde = self._poly(y, (1721414.39987, 365242.88257, -0.00769,
-0.00933, -0.00006))
else:
y = (g_year - 2000) / 1000
if lam == self._SPRING:
jde = self._poly(y, (2451623.80984, 365242.37404, 0.05169,
-0.00411, -0.00057))
elif lam == self._SUMMER:
jde = self._poly(y, (2451716.56767, 365241.62603, 0.00325,
0.00888, -0.0003))
elif lam == self._AUTUMN:
jde = self._poly(y, (2451810.21715, 365242.01767, -0.11575,
0.00337, 0.00078))
else: # lam == self._WINTER:
jde = self._poly(y, (2451900.05952, 365242.74049, -0.06223,
-0.00823, 0.00032))
return jde
[docs]
def _find_moment_of_equinoxes_or_solstices(self, jd: float,
lam: int=_SPRING,
zone: float=0) -> float:
"""
With the jd and season of the year find the equinox or solstice at
Greenwich. If a time zone is provided modify the returned value.
:param float jd: Meeus algorithm Julian day.
:param int lam: The lambda, either `_SPRING` (0° default), `_SUMMER`
(90°), `_AUTUMN` (180°), or `_WINTER` (270°).
:param float zone: The time zone.
:returns: The Julian day of the equinox or solstice.
:rtype: float
.. note::
Meeus--AA ch. 27 p. 177
"""
from .gregorian_calendar import GregorianCalendar
gc = GregorianCalendar()
year = gc.gregorian_year_from_jd(jd)
jde0 = self._approx_julian_day_for_equinoxes_or_solstices(year, lam)
tc = self._julian_centuries(jde0)
w = 35999.373 * tc - 2.47
dl = 1 + 0.0334 * self._cos_deg(w + 0.0007) * self._cos_deg(2 * w)
s = self._sigma((self._EQ_SO_A, self._EQ_SO_B, self._EQ_SO_C),
lambda a, b, c: a * self._cos_deg(b + c * tc))
jde = jde0 + (0.00001 * s) / dl
# Must convert from Dynamical time (JDE) to Universal Time (UT).
delta_t_seconds = self._delta_t(jde)
jd_ut = jde - delta_t_seconds / self._SECONDS_PER_DAY
# Now add the timezone.
jd_local = jd_ut + self._HR(zone)
return round(jd_local, self._ROUNDING_PLACES)
[docs]
def _decimal_from_dms(self, degrees: int, minutes: int, seconds: float,
direction: str='N') -> float:
'''
Coordinates in degrees, minutes, and seconds.
| The Shrine of Baha’u’llah: 32°56’36.86″N, 35° 5’30.38″E
| The Shrine of The Bab: 32°48’52.49″N, 34°59’13.91″E
| The Guardian’s Resting Place (not 3D): 51°37’21.85″N, 0°08’35.57″W
.. rubric:: Process
Convert degrees, minutes, and seconds to a decimal.
Degrees, minutes, and seconds to a decimal coordinate:
1. Add the degrees to the minutes divided by 60
2. Add the seconds divided by (60 x 60), which is 3600
.. rubric:: Example
To convert 35° 20′ 35", the answer is
35 + (20/60) + (35/3600) = 35.34306 degrees.
:param int degrees: The degree part of the coordinates.
:param int minutes: The minute part of the coordinate.
:param float seconds: The second part of the coordinate.
:param str direction: The direction part of the coordinate which can
be any of the following N, S, E, W in upper or
lower case.
:returns: latitude and longitude
:rtype: tuple
'''
dirs = ('N', 'S', 'E', 'W')
assert direction.upper() in dirs, (
f"The 'direction' argument must be one of {dirs}")
# Remove the minus sign if it exists.
degrees = -degrees if degrees < 0 else degrees
decimal = degrees + (minutes / 60) + (seconds / 3600)
# Adjust the sign based on the direction.
return -decimal if direction.upper() in ('S', 'W') else decimal
[docs]
def _dms_from_decimal(self, coord: float, direction: str) -> tuple:
"""
Convert a decimal degree into degrees, minutes, and seconds.
:param float coord: The decimal coordinate.
:param str direction: The direction part of the coordinate which can
be any of the following N, S, E, W in upper or
lower case.
:returns: The degree, minute, second, and direction for of the
coordinate.
:rtype: tuple
"""
dirs = ('LATITUDE', 'LONGITUDE')
direction = direction.upper()
size = len(direction)
assert size >= 3 and any([d.startswith(direction) for d in dirs]), (
f"The direction argument must be one of {dirs}")
# degrees
degrees = math.floor(abs(coord))
# minutes
minutes = math.floor((abs(coord) - degrees) * 60)
# seconds
seconds = (abs(coord) - degrees - (minutes / 60)) * 3600
if coord < 0:
direc = 'S' if direction == 'LAT' else 'W'
else:
direc = 'N' if direction == 'LAT' else 'E'
return degrees, minutes, seconds, direc
[docs]
def _degrees_from_hms(self, h: int, m: int, s: float) -> float:
"""
Find the degrees from the hours, minutes, and seconds of 360 degrees.
Where as time zones are 15 degrees apart so 24 time zones times 15
degrees is 360 degrees.
The angle may be expressed as negative east of the meridian plane
and positive west of the meridian plane, or as positive westward
from 0° to 360°. The angle may be measured in degrees or in time,
with 24h = 360° exactly. So one hour is equal to (360/24)° = 15°.
:param int h: The hour.
:param int m: The minute.
:param float s: The second.
:returns: The degrees from the hours, minutes, and seconds of
360 degrees.
:rtype: float
"""
return 15 * h + 15 * m / 60 + 15 * s / 3600
[docs]
def _hms_from_degrees(self, deg: float) -> tuple:
"""
Find the hours, minutes, and seconds from 0 - 360 degrees. Where
as time zones are 15 degrees apart so 24 time zones times 15
degrees is 360 degrees.
The angle may be expressed as negative east of the meridian plane
and positive west of the meridian plane, or as positive westward
from 0° to 360°. The angle may be measured in degrees or in time,
with 24h = 360° exactly. So one hour is equal to (360/24)° = 15°.
:param float deg: The degrees of the 360 degree circumference of
the earth.
:returns: The hours, minutes, and seconds.
:rtype: tuple
"""
h = math.floor(deg / 15)
m = math.floor((deg / 15 - h) * 60)
s = (deg / 15 - h - m / 60) * 3600
return h, m, s
[docs]
def _seconds_from_dhms(self, days: int, hours: int, minutes: int,
seconds: float, microseconds: int=0,
zone: float=0) -> float:
"""
Convert days, hours, minutes, and seconds to seconds depending on
the timezone.
:param int days: Number of days.
:param int hours: Number of hours.
:param int minutes: The number of minutes.
:param float seconds: The number of seconds with possible fraction.
:param int microseconds: The number of microseconds if they exist.
:param float zone: The timezone in degrees, defaults to 0 or GMT.
:returns: The number of seconds.
:rtype: float
"""
if microseconds > 0:
assert seconds % 1 == 0, (
"If microseconds is greater that zero the second argument "
f"cannot have a decimal, found {seconds}.")
sec = seconds + microseconds / 1e6
else:
sec = seconds
return (days * self._SECONDS_PER_DAY + hours * 3600 + minutes
* 60 + sec + zone * 3600)
[docs]
def _dhms_from_seconds(self, seconds: float, zone: float=0,
us: bool=False) -> tuple:
"""
Convert seconds into days, hours, minutes, and seconds. Depending
on the timezone there could be an additional day added.
:param float seconds: The number of seconds with possible fraction.
:param float zone: The timezone in degrees, defaults to 0 or GMT.
:param bool us: If False (default) no separate field for microseconds
is returned else return microseconds.
:returns: The days, hours, minutes, and seconds.
:rtype: tuple
.. note::
See: https://www.timeanddate.com/time/map/
Time-zones can be from -12 to +14 based on the political times-zones
as of 2024-08-09.
"""
seconds += zone * 3600
hours = math.floor(seconds / 3600)
m = (seconds - hours * 3600) / 60
minutes = math.floor(m)
if hours < 24:
day = 0
else:
hours -= 24
day = 1
seconds -= self._SECONDS_PER_DAY
sec = (seconds - hours * 3600) - minutes * 60
if us:
sms = (math.floor(sec), int(round(sec % 1, 6) * 1e6))
else:
sms = (sec,)
return (day, hours, minutes) + sms
[docs]
def _tz_decimal_from_dhms(self, days: int, hours: int, minutes: int,
seconds: int) -> int:
"""
Convert days, hours, minutes, and seconds to a decimal number
representing percentage of one revolution around the Earth. Where
the number 1 indicates one revolution.
:param int days: Number of days.
:param int hours: Number of hours.
:param int minutes: The number of minutes.
:param float seconds: The number of seconds with possible fraction.
:returns: A decimal number.
:rtype: int
.. note::
This method is used in determining time zones.
"""
return (self._seconds_from_dhms(days, hours, minutes, seconds)
/ self._SECONDS_PER_DAY)
[docs]
def _tz_dhms_from_decimal(self, dec: float) -> tuple:
"""
Convert a decimal number into days, hours, minutes, and seconds
of a time zone. The decimal number represents the percentage of
one revolution around the Earth. Where the number 1 indicates
one revolution.
:param float dec: A decimal number.
:returns: The days, hours, minutes, and seconds of a time zone.
:rtype: tuple
.. note::
This method is used in determining time zones.
"""
return self._dhms_from_seconds(dec * self._SECONDS_PER_DAY)
[docs]
def _hms_from_decimal_day(self, dec: float, *, us: bool=False) -> tuple:
"""
Convert a decimal day to hours, minutes, and seconds. If this
method is used for a Julian Period day, 0.5 must be added to the
value before being passed in.
:param float dec: A decimal number.
:param bool us: If False (default) no separate field for microseconds
is returned else return microseconds.
:returns: A tuple representing the hour, minute, and seconds.
:rtype: tuple
.. note::
If a number as in 10.5 is passed in, the value to the left of
the decimal will be stripped off before calculations are done.
"""
h = self._PARTIAL_DAY_TO_HOURS(dec)
hour = math.floor(h)
m = self._PARTIAL_HOUR_TO_MINUTE(h)
minute = math.floor(m)
second = self._PARTIAL_MINUTE_TO_SECOND(m)
if us:
microsec = (self._PARTIAL_SECOND_TO_MICROSECOND(second),)
second = math.floor(second)
else:
microsec = ()
return (hour, minute, second) + microsec
[docs]
def _decimal_day_from_hms(self, h: int, m: int, s: float, us: int=0
) -> float:
"""
Convert hours, minutes, and seconds to a decimal day.
:param ine h: The hour.
:param int m: The minute.
:param float s: The second.
:param int us: The microseconds.
:returns: A decimal value representing the day with a partial that
indicates the hours, minutes, and seconds.
:rtype: float
"""
assert ((s % 1 and us == 0) or (s % 1 == 0 and us) or
(s % 1 == 0 and us == 0)), (
"Seconds cannot have a decimal value if microseconds are used.")
return (h * 3600 + m * 60 + s + self._US(us)) / self._SECONDS_PER_DAY
[docs]
def _sec_microsec_from_seconds(self, second: float) -> tuple:
"""
Split the second and microseconds.
:param float second: The second with a partial indicating the
microseconds.
:returns: The second split between the second and microseconds.
:rtype: tuple
"""
p = second % 1
s = abs(second) - p
s *= -1 if second < 0 else 1
us = self._PARTIAL_SECOND_TO_MICROSECOND(second)
return math.floor(s), us
[docs]
def _sin_deg(self, theta: float) -> float:
"""
Convert a value to sine in degrees.
:param float theta: The value to convert to degrees.
:returns: The degrees representing the value provided.
:rtype: float
.. rubric:: LISP code from Reingold & Dershowitz CC TUE p. 513
.. code:: lisp
(defun sin-degrees (theta)
;; TYPE angle -> amplitude
;; Sine of theta (given in degrees).
(sin (radians-from-degrees theta)))
"""
return math.sin(math.radians(theta))
[docs]
def _cos_deg(self, theta: float) -> float:
"""
Convert a value to the cosine in degrees.
:param float theta: The value to convert to degrees.
:returns: The degrees representing the value provided.
:rtype: float
.. rubric:: LISP code from Reingold & Dershowitz CC TUE p. 513
.. code:: lisp
(defun cos-degrees (theta)
;; TYPE angle -> amplitude
;; Cosine of theta (given in degrees).
(cos (radians-from-degrees theta)))
"""
return math.cos(math.radians(theta))
[docs]
def _sigma(self, lists: tuple, func: object) -> float:
"""
This gives a summation of a list based on the criteria in the
provided function.
:param tuple lists: The list of values to sum.
:param object func: The function that determines the summation
parameters.
:returns: The summation.
:rtype: float
.. rubric:: LISP code from Reingold & Dershowitz CC TUE p. 473
.. code:: lisp
(defmacro sigma (list body)
;; TYPE (list-of-pairs (list-of-reals->real))
;; TYPE -> real
;; list is of the form ((i1 l1)...(in ln)).
;; Sum of body for indices i1...in
;; running simultaneously thru lists l1...ln.
`(apply `+ (mapcar (function (lambda
,(mapcar `car list)
,body))
,@(mapcar `cadr list))))
"""
# Ensure all lists have the same length
assert len(set(len(lst) for lst in lists)) == 1, (
"Lists must have the same length")
return sum(func(*e) for e in zip(*lists))
[docs]
def _poly(self, x: float, a: list) -> float:
"""
This is the Horner method of polynomial used to eliminate the use
of powers.
.. note::
| Instead of:
| y = A + B * x + C * x^2 + D * x^3 + E * x^4
| do this
| y = A + x * (B + x * (C + x * (D + x * E)))
:param float x: Power of number.
:param list a: The list of numbers in polynomial.
:returns: The polynomial result.
:rtype: float
.. rubric:: LISP code from Reingold & Dershowitz CC TUE p. 473
.. code:: lisp
(defun poly (x a)
;; TYPE (real list-of-reals) -> real
;; Sum powers of x with coefficients (from order 0 up) in list a.
(if (equal a nil)
0
(+ (first a) (* x (poly x (rest a))))))
"""
return 0 if not a else a[0] + (x * self._poly(x, a[1:]))
[docs]
def _days_in_years(self, y: int, *, alt: bool=False) -> int:
"""
Find the number of days up to the provided year.
:param int y: The year to count to.
:param bool alt: If True use the 4|128 rule else if False (default)
use the 4|100|400 rule
:returns: The count of days including year one to the given year.
:rtype: int
.. note::
This method starts the count from year 1 of the Proleptic Gregorian
Calendar, however, it uses one of the two leap rules described
above instead of the usual Julian Calendar leap year rule of every
4 year.
"""
n_4 = y // 4
if alt:
n_128 = y // 128
n_leap_years = n_4 - n_128
else:
n_100 = y // 100
n_400 = y // 400
n_leap_years = n_4 - n_100 + n_400
a = y - n_leap_years # Non-leap years
b = y - a # Leap years
return a * 365 + b * 366
[docs]
def _meeus_from_exact(self, jd: float) -> float:
"""
Returns the Meeus algorithm jd converted from the exact algorithm jd.
:param float jd: Exact Julian Period day.
:returns: The difference added to an astronomically correct jd.
:rtype: float
.. note::
The JDs below are the proleptic (astronomically correct) JDs.
"""
if 2299148.5 <= jd < 2299158.5:
raise ValueError(f"Astronomical JD {jd} lies in the Gregorian "
"reform gap (1582-10-05 through 1582-10-14); "
"no Meeus JD exists.")
jd_diff = (
(1757641.5, 0), (1794165.5, 1), (1830689.5, 2), (1903738.5, 3),
(1940262.5, 4), (1976786.5, 5), (2049835.5, 6), (2086359.5, 7),
(2122883.5, 8), (2195932.5, 9), (2232456.5, 10), (2268980.5, 11),
(2299148.5, 12),
)
diff = 2
for j, df in jd_diff:
if jd < j:
diff = df
break
return jd + diff
[docs]
def _exact_from_meeus(self, jd: float) -> float:
"""
The returned difference value to convert a Meeus algorithm jd to
an exact algorithm jd. This is subtracted from the meeus jd.
:param float jd: Meeus Julian Period day.
:returns: The difference subtracted from an historically correct jd.
:rtype: float
.. note::
1. The JDs below are the historically correct (Meeus) JDs.
2. This library uses a continuous proleptic solar day count for
the Badí' Calendar.
3. The exact JD numbering is uninterrupted across 1582-10-05 …
1582-10-14.
4. Invalid days checked with https://aa.usno.navy.mil/data/JulianDate
The table below indicates that dates less than the ones shown are
legal dates. The dates shown except the last two rows indicate
invalid days in the historic (Meeus) algorithm. The second to the
last indicates the end of the Gregorian reform and the last dates
indicates that from then to forever the proleptic algorithm is 2
days below the historic algorithm.
+--------------+--------------+--------+--------------+
| Historic JD | Proleptic JD | Offset | Gregorian DT |
+==============+==============+========+==============+
| < 1757641.5 | < 1757641.5 | 0 | 0100-02-29 |
+--------------+--------------+--------+--------------+
| < 1794166.5 | < 1794165.5 | 1 | 0200-02-29 |
+--------------+--------------+--------+--------------+
| < 1830691.5 | < 1830689.5 | 2 | 0300-02-29 |
+--------------+--------------+--------+--------------+
| < 1903741.5 | < 1903738.5 | 3 | 0500-02-29 |
+--------------+--------------+--------+--------------+
| < 1940266.5 | < 1940262.5 | 4 | 0600-02-29 |
+--------------+--------------+--------+--------------+
| < 1976791.5 | < 1976786.5 | 5 | 0700-02-29 |
+--------------+--------------+--------+--------------+
| < 2049841.5 | < 2049835.5 | 6 | 0900-02-29 |
+--------------+--------------+--------+--------------+
| < 2086366.5 | < 2086359.5 | 7 | 1000-02-29 |
+--------------+--------------+--------+--------------+
| < 2122891.5 | < 2122883.5 | 8 | 1100-02-29 |
+--------------+--------------+--------+--------------+
| < 2195941.5 | < 2195932.5 | 9 | 1300-02-29 |
+--------------+--------------+--------+--------------+
| < 2232466.5 | < 2232456.5 | 10 | 1400-02-29 |
+--------------+--------------+--------+--------------+
| < 2268991.5 | < 2268980.5 | 11 | 1500-02-29 |
+--------------+--------------+--------+--------------+
| < 2299160.5 | < 2299148.5 | 12 | 1582-10-15 |
+--------------+--------------+--------+--------------+
| >= 2299160.5 | >= 2299158.5 | 2 | 1582-10-15 |
+--------------+--------------+--------+--------------+
"""
invalid_days = ((1757641.5, 1757642.5), (1794166.5, 1794167.5),
(1830691.5, 1830692.5), (1903741.5, 1903742.5),
(1940266.5, 1940267.5), (1976791.5, 1976792.5),
(2049841.5, 2049842.5), (2086366.5, 2086367.5),
(2122891.5, 2122892.5), (2195941.5, 2195942.5),
(2232466.5, 2232467.5), (2268991.5, 2268992.5))
for s, e in invalid_days:
if s <= jd < e:
raise ValueError(f"Invalid historic (Meeus) JD {jd}.")
jd_diff = (
(1757641.5, 0), (1794166.5, 1), (1830691.5, 2), (1903741.5, 3),
(1940266.5, 4), (1976791.5, 5), (2049841.5, 6), (2086366.5, 7),
(2122891.5, 8), (2195941.5, 9), (2232466.5, 10), (2268991.5, 11),
(2299160.5, 12),
)
diff = 2
for j, df in jd_diff:
if jd < j:
diff = df
break
return jd - diff
[docs]
def _coterminal_angle(self, value: float) -> float:
"""
Find the Coterminal Angle from a value that is either more than
360 or less than 0.
:param float value: The value that is more than 360 or less than 0.
:returns: The adjusted angle to be between 0 and 360.
:rtype: float
"""
value = math.fmod(value, 360)
return value + 360 if value < 0 else value
[docs]
def _interpolation_from_three(self, y1: float, y2: float, y3: float,
n: float, normalize: bool=False) -> float:
"""
Interpolate from three terms with a factor.
:param float y1: 1st of the three parameters.
:param float y2: 2nd of the three parameters.
:param float y3: 3rd of the three parameters.
:param float n: The factor.
:param bool normalize: If `False' (default) no normalization is done
else if `True` normalize.
:returns: The three factor interpolation.
:rtype: float
"""
a = y2 - y1
b = y3 - y2
if normalize:
a += 360 if a < 0 else 0
b += 360 if b < 0 else 0
c = b - a
return y2 + (n / 2) * (a + b + n * c)
[docs]
def _truncate_decimal(self, n: int, places: int) -> int:
"""
Truncate a decimal to a number of places.
.. note::
This is somewhat like rounding, but is used in places where
rounding gives an invalid results.
:param int n: Number to truncate.
:param int places: The number of places to truncate to.
:returns: The truncated number.
:rtype: int
"""
p = int('1' + '0' * places)
return math.floor(n * p) / p
[docs]
def _xor_boolean(self, booleans: tuple) -> bool:
"""
Test that any number of booleans can all be False or only one True.
:param tuple booleans: A tuple of booleans that cannot be used
together.
:returns: True if only one of the booleans are True or if None are True
else False.
:rtype: bool
"""
count = sum(booleans)
return (count % 2 == 1) or (count == 0)
[docs]
def _local_zone_correction(self, jd_ut: float, zone: float=None,
lon: float=None, *, inverse: bool=False,
mod_jd: bool=False) -> float:
"""
Convert the UT time to local time.
.. note::
1. If the full JD is passed as jd_ut then the result will be the
correct value.
2. If jd_ut is the fractional part of the JD the result of this
method replaces the fractional part of the JD. DO NOT add it
to the fractional part.
3. If the zone is None the zone will be calculated from the
longitude, a value other than None will be regarded as a
political time zone.
:param float jd_ut: UT time as a fractional part of a JD.
:param float zone: The time zone in hours.
:param float lon: The longitude.
:param bool inverse: Subtract the zone instead of adding it.
:param bool mod_jd: If `False` (default) a correction value is returned
else if `True` the jd_ut is updated to local time.
:returns: The local time corrected from the UT time as a fraction.
:rtype: float
"""
assert zone is None or zone is not None and (-180 <= zone <= 180), (
"If the zone is not None, the zone value must be between -180 and "
f"180, found zone: {zone}.")
assert (zone, lon).count(None) in (0, 1), (
"Both the time zone and longitude cannot be None.")
zone = lon / 360 if zone is None else zone
dec_hr = self._HR(zone)
modified_jd = jd_ut - dec_hr if inverse else jd_ut + dec_hr
if mod_jd:
result = modified_jd
else:
result = round(modified_jd % 1, self._ROUNDING_PLACES)
return result