471 lines
16 KiB
Python
471 lines
16 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/>.
|
||
|
|
||
|
|
||
|
from math import sin, cos, tan, acos, atan, atan2, sqrt
|
||
|
|
||
|
from pymeeus.base import TOL
|
||
|
from pymeeus.Angle import Angle
|
||
|
from pymeeus.Epoch import Epoch
|
||
|
from pymeeus.Coordinates import kepler_equation
|
||
|
from pymeeus.Sun import Sun
|
||
|
|
||
|
|
||
|
"""
|
||
|
.. module:: Minor
|
||
|
:synopsis: Class to model celestial bodies like comets and minor planets
|
||
|
:license: GNU Lesser General Public License v3 (LGPLv3)
|
||
|
|
||
|
.. moduleauthor:: Dagoberto Salazar
|
||
|
"""
|
||
|
|
||
|
|
||
|
class Minor(object):
|
||
|
"""
|
||
|
Class Minor models minor celestial bodies.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, q, e, i, omega, w, t):
|
||
|
"""Minor constructor.
|
||
|
|
||
|
The Minor object is initialized with this constructor, setting the
|
||
|
orbital values and computing some internal parameters. This constructor
|
||
|
is build upon the 'set()' method.
|
||
|
|
||
|
:param q: Perihelion distance, in Astronomical Units
|
||
|
:type q: float
|
||
|
:param e: Eccentricity of the orbit
|
||
|
:type e: float
|
||
|
:param i: Inclination of the orbit, as an Angle object
|
||
|
:type i: :py:class:`Angle`
|
||
|
:param omega: Longitude of the ascending node, as an Angle object
|
||
|
:type omega: :py:class:`Angle`
|
||
|
:param w: Argument of the perihelion, as an Angle object
|
||
|
:type w: :py:class:`Angle`
|
||
|
:param t: Epoch of passage by perihelion, as an Epoch object
|
||
|
:type t: :py:class:`Epoch`
|
||
|
|
||
|
:raises: TypeError if input value is of wrong type.
|
||
|
"""
|
||
|
|
||
|
self._tol = TOL
|
||
|
self.set(q, e, i, omega, w, t)
|
||
|
|
||
|
def set(self, q, e, i, omega, w, t):
|
||
|
"""Method used to set the orbital values and set some internal
|
||
|
parameters.
|
||
|
|
||
|
:param q: Perihelion distance, in Astronomical Units
|
||
|
:type q: float
|
||
|
:param e: Eccentricity of the orbit
|
||
|
:type e: float
|
||
|
:param i: Inclination of the orbit, as an Angle object
|
||
|
:type i: :py:class:`Angle`
|
||
|
:param omega: Longitude of the ascending node, as an Angle object
|
||
|
:type omega: :py:class:`Angle`
|
||
|
:param w: Argument of the perihelion, as an Angle object
|
||
|
:type w: :py:class:`Angle`
|
||
|
:param t: Epoch of passage by perihelion, as an Epoch object
|
||
|
:type t: :py:class:`Epoch`
|
||
|
|
||
|
:raises: TypeError if input value is of wrong type.
|
||
|
"""
|
||
|
|
||
|
# First check that input value is of correct types
|
||
|
if not (isinstance(t, Epoch) and isinstance(q, float) and
|
||
|
isinstance(e, float) and isinstance(i, Angle) and
|
||
|
isinstance(omega, Angle) and isinstance(w, Angle)):
|
||
|
raise TypeError("Invalid input types")
|
||
|
# Compute auxiliary quantities
|
||
|
se = 0.397777156
|
||
|
ce = 0.917482062
|
||
|
omer = omega.rad()
|
||
|
ir = i.rad()
|
||
|
f = cos(omer)
|
||
|
g = sin(omer) * ce
|
||
|
h = sin(omer) * se
|
||
|
p = -sin(omer) * cos(ir)
|
||
|
qq = cos(omer) * cos(ir) * ce - sin(ir) * se
|
||
|
r = cos(omer) * cos(ir) * se + sin(ir) * ce
|
||
|
self._aa = atan2(f, p)
|
||
|
self._bb = atan2(g, qq)
|
||
|
self._cc = atan2(h, r)
|
||
|
self._am = sqrt(f * f + p * p)
|
||
|
self._bm = sqrt(g * g + qq * qq)
|
||
|
self._cm = sqrt(h * h + r * r)
|
||
|
# Store some orbital parameters
|
||
|
if abs(e - 1.0) > self._tol:
|
||
|
self._a = abs(q / (1.0 - e))
|
||
|
else:
|
||
|
self._a = q
|
||
|
self._q = q
|
||
|
self._e = e
|
||
|
self._i = i
|
||
|
self._omega = omega
|
||
|
self._w = w
|
||
|
self._t = t
|
||
|
# Compute the mean motion from the semi-major axis (degrees/day)
|
||
|
self._n = 0.9856076686 / (self._a * sqrt(self._a))
|
||
|
return
|
||
|
|
||
|
def _near_parabolic(self, t):
|
||
|
"""This internal function handles the computation of the true anomaly
|
||
|
and the radius vector when the eccentricity is close to 1.
|
||
|
|
||
|
:param t: Days since perihelion
|
||
|
:type t: float
|
||
|
|
||
|
:returns: A tuple containing the true anomaly (as an Angle object) and
|
||
|
the radius vector (in Astronomical Units).
|
||
|
:rtype: tuple
|
||
|
:raises: TypeError if input value is of wrong type, and ValueError if
|
||
|
convergence is not possible
|
||
|
|
||
|
>>> q = 0.5871018
|
||
|
>>> e = 0.9672746
|
||
|
>>> t = 20.0
|
||
|
>>> i = Angle(0.0)
|
||
|
>>> omega = Angle(0.0)
|
||
|
>>> w = Angle(0.0)
|
||
|
>>> ep = Epoch(2000, 1, 1.5)
|
||
|
>>> minor = Minor(q, e, i, omega, w, ep)
|
||
|
>>> v, r = minor._near_parabolic(t)
|
||
|
>>> print(round(v, 5))
|
||
|
52.85331
|
||
|
>>> print(round(r, 6))
|
||
|
0.729116
|
||
|
>>> q = 3.363943
|
||
|
>>> e = 1.05731
|
||
|
>>> t = 1237.1
|
||
|
>>> minor = Minor(q, e, i, omega, w, ep)
|
||
|
>>> v, r = minor._near_parabolic(t)
|
||
|
>>> print(round(v, 5))
|
||
|
109.40598
|
||
|
>>> print(round(r, 6))
|
||
|
10.668551
|
||
|
"""
|
||
|
|
||
|
# First check that input value is of correct types
|
||
|
if not isinstance(t, float):
|
||
|
raise TypeError("Invalid input type")
|
||
|
# Let's start defining some constants and renaming some parameters
|
||
|
k = 0.01720209895
|
||
|
d1 = 10000
|
||
|
c = 1.0 / 3.0
|
||
|
d = self._tol
|
||
|
q = self._q
|
||
|
e = self._e
|
||
|
q1 = k * sqrt((1.0 + e) / q) / (2.0 * q)
|
||
|
g = (1.0 - e) / (1.0 + e)
|
||
|
# If t == 0, then r = q and v = 0
|
||
|
if abs(t) > d:
|
||
|
q2 = q1 * t
|
||
|
s = 2.0 / (3.0 * abs(q2))
|
||
|
s = 2.0 / tan(2.0 * atan(tan(atan(s) / 2) ** c))
|
||
|
if t < 0.0:
|
||
|
s = -s
|
||
|
# Parabolic case
|
||
|
if abs(e - 1.0) < d:
|
||
|
v = 2.0 * atan(s)
|
||
|
rr = q * (1.0 + e) / (1.0 + e * cos(v))
|
||
|
v = Angle(v, radians=True).to_positive()
|
||
|
return v, rr
|
||
|
ll = 0.0
|
||
|
s0 = s + 1.0
|
||
|
while abs(s - s0) > d:
|
||
|
s0 = s
|
||
|
z = 1
|
||
|
y = s * s
|
||
|
g1 = -y * s
|
||
|
q3 = q2 + 2.0 * g * s * y / 3.0
|
||
|
f = d + 1.0
|
||
|
while abs(f) > d:
|
||
|
z += 1
|
||
|
g1 = -g1 * g * y
|
||
|
z1 = (z - (z + 1.0) * g) / (2.0 * z + 1.0)
|
||
|
f = z1 * g1
|
||
|
q3 = q3 + f
|
||
|
if z > 50 or abs(f) > d1:
|
||
|
raise ValueError("No convergence")
|
||
|
ll += 1
|
||
|
if ll > 50:
|
||
|
raise ValueError("No convergence")
|
||
|
s1 = s + 1.0
|
||
|
while abs(s - s1) > d:
|
||
|
s1 = s
|
||
|
s = (2.0 * s * s * s / 3.0 + q3) / (s * s + 1.0)
|
||
|
v = 2.0 * atan(s)
|
||
|
rr = q * (1.0 + e) / (1.0 + e * cos(v))
|
||
|
v = Angle(v, radians=True).to_positive()
|
||
|
return v, rr
|
||
|
else:
|
||
|
rr = q
|
||
|
v = Angle(0.0)
|
||
|
return v, rr
|
||
|
|
||
|
def geocentric_position(self, epoch):
|
||
|
"""This method computes the geocentric position of a minor celestial
|
||
|
body (right ascension and declination) for the given epoch, and
|
||
|
referred to the standard equinox J2000.0. Additionally, it also
|
||
|
computes the elongation angle to the Sun.
|
||
|
|
||
|
:param epoch: Epoch to compute geocentric position, as an Epoch object
|
||
|
:type epoch: :py:class:`Epoch`
|
||
|
|
||
|
:returns: A tuple containing the right ascension, the declination and
|
||
|
the elongation angle to the Sun, as Angle objects
|
||
|
:rtype: tuple
|
||
|
:raises: TypeError if input value is of wrong type.
|
||
|
|
||
|
>>> a = 2.2091404
|
||
|
>>> e = 0.8502196
|
||
|
>>> q = a * (1.0 - e)
|
||
|
>>> i = Angle(11.94524)
|
||
|
>>> omega = Angle(334.75006)
|
||
|
>>> w = Angle(186.23352)
|
||
|
>>> t = Epoch(1990, 10, 28.54502)
|
||
|
>>> minor = Minor(q, e, i, omega, w, t)
|
||
|
>>> epoch = Epoch(1990, 10, 6.0)
|
||
|
>>> ra, dec, p = minor.geocentric_position(epoch)
|
||
|
>>> print(ra.ra_str(n_dec=1))
|
||
|
10h 34' 13.7''
|
||
|
>>> print(dec.dms_str(n_dec=0))
|
||
|
19d 9' 32.0''
|
||
|
>>> print(round(p, 2))
|
||
|
40.51
|
||
|
>>> t = Epoch(1998, 4, 14.4358)
|
||
|
>>> q = 1.487469
|
||
|
>>> e = 1.0
|
||
|
>>> i = Angle(0.0)
|
||
|
>>> omega = Angle(0.0)
|
||
|
>>> w = Angle(0.0)
|
||
|
>>> minor = Minor(q, e, i, omega, w, t)
|
||
|
>>> epoch = Epoch(1998, 8, 5.0)
|
||
|
>>> ra, dec, p = minor.geocentric_position(epoch)
|
||
|
>>> print(ra.ra_str(n_dec=1))
|
||
|
5h 45' 34.5''
|
||
|
>>> print(dec.dms_str(n_dec=0))
|
||
|
23d 23' 53.0''
|
||
|
>>> print(round(p, 2))
|
||
|
45.73
|
||
|
"""
|
||
|
|
||
|
# First check that input value is of correct types
|
||
|
if not isinstance(epoch, Epoch):
|
||
|
raise TypeError("Invalid input type")
|
||
|
# Get internal parameters
|
||
|
aa, bb, cc = self._aa, self._bb, self._cc
|
||
|
am, bm, cm = self._am, self._bm, self._cm
|
||
|
# Get the mean motion and other orbital parameters
|
||
|
n = self._n
|
||
|
a = self._a
|
||
|
e = self._e
|
||
|
w = self._w
|
||
|
t = self._t
|
||
|
# Time since perihelion
|
||
|
t_peri = epoch - t
|
||
|
# Now, compute the mean anomaly, in degrees
|
||
|
m = t_peri * n
|
||
|
m = Angle(m)
|
||
|
if e < 0.98:
|
||
|
# Elliptic case
|
||
|
# With the mean anomaly, use Kepler's equation to find E and v
|
||
|
ee, v = kepler_equation(e, m)
|
||
|
ee = Angle(ee).to_positive()
|
||
|
# Get r
|
||
|
er = ee.rad()
|
||
|
rr = a * (1.0 - e * cos(er))
|
||
|
elif abs(e - 1.0) < self._tol:
|
||
|
# Parabolic case
|
||
|
q = self._q
|
||
|
ww = (0.03649116245 * (epoch - self._t)) / (q * sqrt(q))
|
||
|
sp = ww / 3.0
|
||
|
iterate = True
|
||
|
while iterate:
|
||
|
s = (2.0 * sp * sp * sp + ww) / (3.0 * (sp * sp + 1.0))
|
||
|
iterate = abs(s - sp) > self._tol
|
||
|
sp = s
|
||
|
v = 2.0 * atan(s)
|
||
|
v = Angle(v, radians=True)
|
||
|
rr = q * (1.0 + s * s)
|
||
|
else:
|
||
|
# We are in the near-parabolic case
|
||
|
v, rr = self._near_parabolic(t_peri)
|
||
|
# Compute the heliocentric rectangular equatorial coordinates
|
||
|
wr = w.rad()
|
||
|
vr = Angle(v).rad()
|
||
|
x = rr * am * sin(aa + wr + vr)
|
||
|
y = rr * bm * sin(bb + wr + vr)
|
||
|
z = rr * cm * sin(cc + wr + vr)
|
||
|
# Now let's compute Sun's rectangular coordinates
|
||
|
xs, ys, zs = Sun.rectangular_coordinates_j2000(epoch)
|
||
|
xi = x + xs
|
||
|
eta = y + ys
|
||
|
zeta = z + zs
|
||
|
delta = sqrt(xi * xi + eta * eta + zeta * zeta)
|
||
|
# We need to correct for the effect of light-time. Compute delay tau
|
||
|
tau = 0.0057755183 * delta
|
||
|
# Recompute some critical parameters
|
||
|
t_peri = epoch - t - tau
|
||
|
# Now, compute the mean anomaly, in degrees
|
||
|
m = t_peri * n
|
||
|
m = Angle(m)
|
||
|
if e < 0.98:
|
||
|
# Elliptic case
|
||
|
# With the mean anomaly, use Kepler's equation to find E and v
|
||
|
ee, v = kepler_equation(e, m)
|
||
|
ee = Angle(ee).to_positive()
|
||
|
# Get r
|
||
|
er = ee.rad()
|
||
|
rr = a * (1.0 - e * cos(er))
|
||
|
elif abs(e - 1.0) < self._tol:
|
||
|
# Parabolic case
|
||
|
q = self._q
|
||
|
ww = (0.03649116245 * (epoch - self._t)) / (q * sqrt(q))
|
||
|
sp = ww / 3.0
|
||
|
iterate = True
|
||
|
while iterate:
|
||
|
s = (2.0 * sp * sp * sp + ww) / (3.0 * (sp * sp + 1.0))
|
||
|
iterate = abs(s - sp) > self._tol
|
||
|
sp = s
|
||
|
v = 2.0 * atan(s)
|
||
|
v = Angle(v, radians=True)
|
||
|
rr = q * (1.0 + s * s)
|
||
|
else:
|
||
|
# We are in the near-parabolic case
|
||
|
v, rr = self._near_parabolic(t_peri)
|
||
|
# Compute the heliocentric rectangular equatorial coordinates
|
||
|
wr = w.rad()
|
||
|
vr = Angle(v).rad()
|
||
|
x = rr * am * sin(aa + wr + vr)
|
||
|
y = rr * bm * sin(bb + wr + vr)
|
||
|
z = rr * cm * sin(cc + wr + vr)
|
||
|
xi = x + xs
|
||
|
eta = y + ys
|
||
|
zeta = z + zs
|
||
|
ra = Angle(atan2(eta, xi), radians=True)
|
||
|
dec = Angle(atan2(zeta, sqrt(xi * xi + eta * eta)), radians=True)
|
||
|
r_sun = sqrt(xs * xs + ys * ys + zs * zs)
|
||
|
psi = acos((xi * xs + eta * ys + zeta * zs) / (r_sun * delta))
|
||
|
psi = Angle(psi, radians=True)
|
||
|
return ra, dec, psi
|
||
|
|
||
|
def heliocentric_ecliptical_position(self, epoch):
|
||
|
"""This method computes the heliocentric position of a minor celestial
|
||
|
body, providing the result in ecliptical coordinates.
|
||
|
|
||
|
:param epoch: Epoch to compute geocentric position, as an Epoch object
|
||
|
:type epoch: :py:class:`Epoch`
|
||
|
|
||
|
:returns: A tuple containing longitude and latitude, as Angle objects
|
||
|
:rtype: tuple
|
||
|
:raises: TypeError if input value is of wrong type.
|
||
|
|
||
|
>>> a = 2.2091404
|
||
|
>>> e = 0.8502196
|
||
|
>>> q = a * (1.0 - e)
|
||
|
>>> i = Angle(11.94524)
|
||
|
>>> omega = Angle(334.75006)
|
||
|
>>> w = Angle(186.23352)
|
||
|
>>> t = Epoch(1990, 10, 28.54502)
|
||
|
>>> epoch = Epoch(1990, 10, 6.0)
|
||
|
>>> minor = Minor(q, e, i, omega, w, t)
|
||
|
>>> lon, lat = minor.heliocentric_ecliptical_position(epoch)
|
||
|
>>> print(lon.dms_str(n_dec=1))
|
||
|
66d 51' 57.8''
|
||
|
>>> print(lat.dms_str(n_dec=1))
|
||
|
11d 56' 14.4''
|
||
|
"""
|
||
|
|
||
|
# First check that input value is of correct types
|
||
|
if not isinstance(epoch, Epoch):
|
||
|
raise TypeError("Invalid input type")
|
||
|
# Get the mean motion and other orbital parameters
|
||
|
n = self._n
|
||
|
a = self._a
|
||
|
e = self._e
|
||
|
i = self._i
|
||
|
omega = self._omega
|
||
|
w = self._w
|
||
|
t = self._t
|
||
|
# Time since perihelion
|
||
|
t_peri = epoch - t
|
||
|
# Now, compute the mean anomaly, in degrees
|
||
|
m = t_peri * n
|
||
|
m = Angle(m)
|
||
|
# With the mean anomaly, use Kepler's equation to find E and v
|
||
|
ee, v = kepler_equation(e, m)
|
||
|
ee = Angle(ee).to_positive()
|
||
|
# Get r
|
||
|
er = ee.rad()
|
||
|
r = a * (1.0 - e * cos(er))
|
||
|
# Compute the heliocentric rectangular ecliptical coordinates
|
||
|
wr = w.rad()
|
||
|
vr = Angle(v).rad()
|
||
|
ur = wr + vr
|
||
|
omer = omega.rad()
|
||
|
ir = i.rad()
|
||
|
x = r * (cos(omer) * cos(ur) - sin(omer) * sin(ur) * cos(ir))
|
||
|
y = r * (sin(omer) * cos(ur) + cos(omer) * sin(ur) * cos(ir))
|
||
|
z = r * sin(ir) * sin(ur)
|
||
|
lon = atan2(y, x)
|
||
|
lat = atan2(z, sqrt(x * x + y * y))
|
||
|
return Angle(lon, radians=True), Angle(lat, radians=True)
|
||
|
|
||
|
|
||
|
def main():
|
||
|
|
||
|
# Let's define a small helper function
|
||
|
def print_me(msg, val):
|
||
|
print("{}: {}".format(msg, val))
|
||
|
|
||
|
# Let's show some uses of Minor class
|
||
|
print("\n" + 35 * "*")
|
||
|
print("*** Use of Minor class")
|
||
|
print(35 * "*" + "\n")
|
||
|
|
||
|
# Let's compute the equatorial coordinates of comet Encke
|
||
|
a = 2.2091404
|
||
|
e = 0.8502196
|
||
|
q = a * (1.0 - e)
|
||
|
i = Angle(11.94524)
|
||
|
omega = Angle(334.75006)
|
||
|
w = Angle(186.23352)
|
||
|
t = Epoch(1990, 10, 28.54502)
|
||
|
epoch = Epoch(1990, 10, 6.0)
|
||
|
minor = Minor(q, e, i, omega, w, t)
|
||
|
ra, dec, elong = minor.geocentric_position(epoch)
|
||
|
print_me("Right ascension", ra.ra_str(n_dec=1)) # 10h 34' 13.7''
|
||
|
print_me("Declination", dec.dms_str(n_dec=0)) # 19d 9' 32.0''
|
||
|
print_me("Elongation", round(elong, 2)) # 40.51
|
||
|
|
||
|
print("")
|
||
|
|
||
|
# Now compute the heliocentric ecliptical coordinates
|
||
|
lon, lat = minor.heliocentric_ecliptical_position(epoch)
|
||
|
print_me("Heliocentric ecliptical longitude", lon.dms_str(n_dec=1))
|
||
|
# 66d 51' 57.8''
|
||
|
print_me("Heliocentric ecliptical latitude", lat.dms_str(n_dec=1))
|
||
|
# 11d 56' 14.4''
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
|
||
|
main()
|