Files
skyview.astronomiemuseum.de/public/py/vendor_linux/skyfield/tests/test_elementslib.py
T
2026-07-13 20:00:07 +02:00

610 lines
23 KiB
Python

from skyfield.api import load, Time, load_file
from skyfield.data.spice import inertial_frames
from skyfield.data.gravitational_parameters import GM_dict
from skyfield.units import Distance, Angle, Velocity
from skyfield.constants import DAY_S
from skyfield.elementslib import (
OsculatingElements,
normpi,
osculating_elements_of,
)
from skyfield.keplerlib import ele_to_vec
from numpy import array, pi, ndarray, float64, repeat, seterr, inf, linspace
import os
def ts():
yield load.timescale()
def _data_path(filename):
return os.path.join(os.path.dirname(__file__), 'data', filename)
ECLIPTIC = inertial_frames['ECLIPJ2000']
ephem = load_file(_data_path('de430-2015-03-02.bsp'))
jup_ephem = load_file(_data_path('jup310-2015-03-02.bsp'))
io = jup_ephem['io']
moon = ephem['moon']
earth = ephem['earth']
sun = ephem['sun']
seterr(all='raise')
def compare(value, expected_value, epsilon, mod=False):
"""Compares value to expected value, and works if one or both are arrays.
Also allows epsilon to be an array.
If mod==True, then compare(0, tau, 0) is True.
"""
if mod:
diff = normpi(value - expected_value)
else:
diff = value - expected_value
if hasattr(value, '__len__') or hasattr(expected_value, '__len__'):
if hasattr(epsilon, '__len__'):
assert (abs(diff) <= epsilon).all()
else:
assert max(abs(diff)) <= epsilon
else:
assert abs(diff) <= epsilon
def check_types(elements, length):
"""Raise an assertion error for any problems with orbital `elements`.
Checks that all of the attributes in the OsculatingElements object
`elements` are present, have the correct type, and have the expected
size.
"""
for item in ['inclination', 'longitude_of_ascending_node',
'argument_of_periapsis', 'true_anomaly',
'argument_of_latitude', 'eccentric_anomaly', 'mean_anomaly',
'mean_longitude', 'true_longitude', 'longitude_of_periapsis',
'mean_motion_per_day']:
element = getattr(elements, item)
assert isinstance(element, Angle)
assert element.radians.size == length
for item in ['eccentricity', 'period_in_days']:
element = getattr(elements, item)
assert isinstance(element, (ndarray, float64))
assert element.size == length
for item in ['_n_vec', '_e_vec', '_h_vec']:
element = getattr(elements, item)
assert isinstance(element, (ndarray, float64))
assert element[0].size == length
for item in ['semi_latus_rectum', 'apoapsis_distance',
'periapsis_distance', 'semi_major_axis', 'semi_minor_axis']:
element = getattr(elements, item)
assert isinstance(element, Distance)
assert element.au.size == length
assert isinstance(elements.periapsis_time, Time)
assert elements.periapsis_time.tt.size == length
def horizons_dict(elem, units='km_d', ):
"""Return a dictionary with keys that match labels used by Horizons.
The data from this method is exactly the same as the data in the
elements object and differs from it only in units.
Returns an dictionary whose keys match the labels from Horizons:
EC = eccentricity
QR = periapsis distance
IN = inclination
OM = longitude of ascending node
W = argument of periapsis
Tp = time of closest periapsis
N = mean motion
MA = mean anomaly
TA = true anomaly
A = semi-major axis
AD = apoapsis distance
PR = period
Just like in Horizons, all angle values are in degrees, but the
distance and time units can be specified with the `units` keyword,
and must be one of the following:
'km_s' for kilometers and seconds
'km_d' for kilometers and days
'au_d' for au and days
"""
data = {}
data['EC'] = elem.eccentricity
if units=='km_s' or units=='km_d':
data['QR'] = elem.periapsis_distance.km
elif units=='au_d':
data['QR'] = elem.periapsis_distance.au
data['IN'] = elem.inclination.degrees
data['OM'] = elem.longitude_of_ascending_node.degrees
data['W'] = elem.argument_of_periapsis.degrees
data['Tp'] = elem.periapsis_time.tdb
if units=='km_d' or units =='au_d':
data['N'] = elem.mean_motion_per_day.degrees
elif units=='km_s':
data['N'] = elem.mean_motion_per_day.degrees/DAY_S
data['MA'] = elem.mean_anomaly.degrees
data['TA'] = elem.true_anomaly.degrees
if units=='km_s' or units=='km_d':
data['A'] = elem.semi_major_axis.km
data['AD'] = elem.apoapsis_distance.km
elif units=='au_d':
data['A'] = elem.semi_major_axis.au
data['AD'] = elem.apoapsis_distance.au
if units=='km_d' or units =='au_d':
data['PR'] = elem.period_in_days
elif units=='km_s':
data['PR'] = elem.period_in_days*DAY_S
return data
def horizons_array(elem, units='km_d', ):
"""Return a numpy array containing data in the same order as horizons.
The data from this method is exactly the same as the data in the
elements object and differs from it only in units.
Just like in Horizons, all angle values are in degrees, but the
distance and time units can be specified with the `units` keyword,
and must be one of the following:
'km_s' for kilometers and seconds
'km_d' for kilometers and days
'au_d' for au and days
The shape of the array is ``(12,)`` if the time used to construct
the position is a float, and ``(12, n)`` if the time is an array of
length n.
"""
dict_ = horizons_dict(elem, units=units)
array_ = array([dict_['EC'],
dict_['QR'],
dict_['IN'],
dict_['OM'],
dict_['W'],
dict_['Tp'],
dict_['N'],
dict_['MA'],
dict_['TA'],
dict_['A'],
dict_['AD'],
dict_['PR']])
return array_
def test_repr(ts):
elements = osculating_elements_of((moon-earth).at(ts.utc(2015, 3, 2, 12)))
assert repr(elements) == '<Elements 1 sets>'
def test_single_time(ts):
"""Tests creation of an OsculatingElements object with a single set of elements
"""
elements = osculating_elements_of((moon-earth).at(ts.utc(2015, 3, 2, 12)))
check_types(elements, 1)
def test_multiple_times(ts):
"""Tests creation of an OsculatingElements object with multiple sets of elements
"""
time = ts.utc(2015, 3, 2, [12, 13, 14, 15])
elements = osculating_elements_of((moon-earth).at(time))
check_types(elements, len(time))
def test_equatorial_km_d(ts):
"""Tests against data from Horizons in km and days, with equatorial reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
calc_data = horizons_array(osculating_elements_of(geocentric_pos))
horizons_data = array([5.569337304355707E-02,
3.628019705296879E+05,
1.832507608006020E+01,
3.570946187642240E+02,
3.318968921151215E+02,
2457072.329517839011,
1.320459197700415E+01,
1.486020417866582E+02,
1.517384963232830E+02,
3.841993269696947E+05,
4.055966834097015E+05,
2.726324301628867E+01])
epsilon = array([1e-10, 1e-4, 1e-7, 1e-6, 1e-6, 1e-8, 1e-9, 1e-6, 1e-7, 1e-4, 1e-4, 1e-8])
compare(calc_data, horizons_data, epsilon)
def test_equatorial_km_s(ts):
"""Tests against data from Horizons in km and seconds, with equatorial reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
calc_data = horizons_array(osculating_elements_of(geocentric_pos), units='km_s')
horizons_data = array([5.569337304355707E-02,
3.628019705296879E+05,
1.832507608006020E+01,
3.570946187642240E+02,
3.318968921151215E+02,
2457072.329517839011,
1.528309256597703E-04,
1.486020417866582E+02,
1.517384963232830E+02,
3.841993269696947E+05,
4.055966834097015E+05,
2.355544196607342E+06])
epsilon = array([1e-10, 1e-4, 1e-7, 1e-6, 1e-6, 1e-8, 1e-14, 1e-6, 1e-7, 1e-4, 1e-4, 1e-3])
compare(calc_data, horizons_data, epsilon)
def test_equatorial_au_d(ts):
"""Tests against data from Horizons in au and days, with equatorial reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
calc_data = horizons_array(osculating_elements_of(geocentric_pos), units='au_d')
horizons_data = array([5.569337304355707E-02,
2.425181380136368E-03,
1.832507608006020E+01,
3.570946187642240E+02,
3.318968921151215E+02,
2457072.329517839011,
1.320459197700415E+01,
1.486020417866582E+02,
1.517384963232830E+02,
2.568213873445825E-03,
2.711246366755283E-03,
2.726324301628867E+01])
epsilon = array([1e-10, 1e-12, 1e-7, 1e-6, 1e-6, 1e-8, 1e-9, 1e-6, 1e-7, 1e-13, 1e-12, 1e-8])
compare(calc_data, horizons_data, epsilon)
def test_ecliptic_km_d(ts):
"""Tests against data from Horizons in km and days, with ecliptic reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
calc_data = horizons_array(osculating_elements_of(geocentric_pos, ECLIPTIC), units='km_d')
horizons_data = array([5.569337304355707E-02,
3.628019705296879E+05,
5.216521657765558E+00,
1.900948892867905E+02,
1.390846818575352E+02,
2457072.329517839011,
1.320459197700415E+01,
1.486020417866582E+02,
1.517384963232830E+02,
3.841993269696947E+05,
4.055966834097015E+05,
2.726324301628867E+01])
epsilon = array([1e-10, 1e-4, 1e-8, 1e-6, 1e-6, 1e-8, 1e-9, 1e-6, 1e-7, 1e-4, 1e-4, 1e-8])
compare(calc_data, horizons_data, epsilon)
def test_ecliptic_km_s(ts):
"""Tests against data from Horizons in km and seconds, with ecliptic reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
calc_data = horizons_array(osculating_elements_of(geocentric_pos, ECLIPTIC), units='km_s')
horizons_data = array([5.569337304355707E-02,
3.628019705296879E+05,
5.216521657765558E+00,
1.900948892867905E+02,
1.390846818575352E+02,
2457072.329517839011,
1.528309256597703E-04,
1.486020417866582E+02,
1.517384963232830E+02,
3.841993269696947E+05,
4.055966834097015E+05,
2.355544196607342E+06])
epsilon = array([1e-10, 1e-4, 1e-8, 1e-6, 1e-6, 1e-8, 1e-14, 1e-6, 1e-7, 1e-4, 1e-4, 1e-3])
compare(calc_data, horizons_data, epsilon)
def test_ecliptic_au_d(ts):
"""Tests against data from Horizons in au and days, with ecliptic reference plane
"""
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
elem = osculating_elements_of(geocentric_pos, ECLIPTIC)
calc_data = horizons_array(elem, units='au_d')
horizons_data = array([5.569337304355707E-02,
2.425181380136368E-03,
5.216521657765558E+00,
1.900948892867905E+02,
1.390846818575352E+02,
2457072.329517839011,
1.320459197700415E+01,
1.486020417866582E+02,
1.517384963232830E+02,
2.568213873445825E-03,
2.711246366755283E-03,
2.726324301628867E+01])
epsilon = array([1e-10, 1e-12, 1e-8, 1e-6, 1e-6, 1e-8, 1e-9, 1e-6, 1e-7, 1e-13, 1e-12, 1e-8])
compare(calc_data, horizons_data, epsilon)
def test_extreme_ellipse(ts):
"""Tests against data from Horizons for an orbit with eccentricity just less than 1
"""
geocentric_pos = (io - sun).at(ts.tdb(2015, 3, 2, 17, 26))
calc_data = horizons_array(osculating_elements_of(geocentric_pos), units='km_s')
horizons_data = array([9.993434925710607E-01,
1.163126430217223E+08,
2.164979337400508E+01,
7.503612948248414E+00,
3.571949928607168E+02,
2456680.438054572791,
2.798965463884538E-10,
9.764838165348996E-03,
1.351769989470609E+02,
1.771688146920545E+11,
3.542213167410872E+11,
1.286189503390209E+12])
epsilon = array([1e-10, 1e-1, 1e-8, 1e-8, 1e-7, 1e-8, 1e-16, 1e-8, 1e-7, 1e5, 1e5, 1e6])
compare(calc_data, horizons_data, epsilon)
def test_slightly_hyperbolic(ts):
"""Tests against data from Horizons for an orbit with eccentricity just over 1
"""
geocentric_pos = (io - sun).at(ts.tdb(2015, 3, 2, 17, 27))
calc_data = horizons_array(osculating_elements_of(geocentric_pos), units='km_s')
horizons_data = array([1.000249165282725E+00,
1.176022222580809E+08,
2.167088597088522E+01,
7.441033056578390E+00,
3.575776020085432E+02,
2456680.266815741546,
6.437043762782399E-11,
2.246667771669457E-03,
1.348525808471548E+02,
-4.719847844449893E+11])
epsilon = array([1e-11, 1e-2, 1e-11, 1e-11, 1e-9, 1e-8, 1e-17, 1e-10, 1e-9, 1e4])
compare(calc_data[:-2], horizons_data, epsilon)
assert (calc_data[-2:] == array([inf, inf])).all()
def test_periapsis_time(ts):
"""This tests the moment when the eccentricity of Io orbiting the sun
transitions from just below 1 to just above 1. Periapsis time is
calculated using M/n, and both M and n go to 0 as e goes to 1.
Periapsis time of Io around the sun should change linearly over a very
small time interval interval (a fraction of one second) during this
transition, but instead periapsis time deviated from linear within .005s
of e=1.
One source of the deviation was the fact that M was being remapped to
-pi to pi when e>1. Using M without remapping reduced the deviation when
e>1. The second thing that reduced the deviation was widening the
tolerance for the use of the parabolic periapsis time equation.
This test makes sure these fixes don't regress by checking that the
periapsis times don't deviate from linear by more than 1e-5 days within
.005s of e=1.
"""
t = ts.tdb(jd=linspace(2457084.226893796, 2457084.226893912, 500))
elem = osculating_elements_of((io - sun).at(t))
Tp = elem.periapsis_time.tdb
line = -240.61044176706827*t.tdb + 593656801.6052344
compare(Tp, line, 1e-5)
def check_orbit(p, e, i, Om, w, v, ts):
"""Checks that the given set of elements are calculated properly by
elementslib.py
Converts the given elements to state vectors using ele_to_vec, then uses
those state vectors to create an OsculatingElements object, and then
checks that the data in the OsculatingElements object matches the input
elements.
"""
length = 1
for item in p, e, i, Om, w, v:
if isinstance(item, (list, ndarray)):
length = len(item)
mu = 403503.2355022598
pos_vec, vel_vec = ele_to_vec(p, e, i, Om, w, v, mu)
time_tt = ts.utc(2018).tt
time = ts.tt(jd=repeat(time_tt, pos_vec[0].size))
elements = OsculatingElements(Distance(km=pos_vec),
Velocity(km_per_s=vel_vec),
time, mu)
check_types(elements, length)
compare(time, elements.time, 1e-9)
compare(elements.semi_latus_rectum.km, p, 1e-8)
compare(elements.eccentricity, e, 1e-14)
compare(elements.inclination.radians, i, 1e-14, mod=True)
compare(elements.longitude_of_ascending_node.radians, Om, 1e-14, mod=True)
compare(elements.argument_of_periapsis.radians, w, 1e-14, mod=True)
compare(elements.true_anomaly.radians, v, 1e-14, mod=True)
# The remaining tests check that certain edge case orbits are being handled
# correctly. Each element is checked in the middle and at the edges of all
# quadrants.
angles1 = array([0, .25, .5, .75, 1, 1.25, 1.5, 1.75])*pi
angles2 = array([-.75, -.5, -.25, 0, .25, .5, .75])*pi
def test_circular(ts):
e = 0
w = 0
check_orbit(300000, e, .5, angles1, w, 1, ts)
check_orbit(300000, e, .5, 1, w, angles1, ts)
for angle in angles1:
check_orbit(300000, e, .5, angle, w, 1, ts)
check_orbit(300000, e, .5, 1, w, angle, ts)
def test_circular_equatorial(ts):
e = 0
i = 0
w = 0
Om = 0
check_orbit(300000, e, i, Om, w, angles1, ts)
for angle in angles1:
check_orbit(300000, e, i, Om, w, angle, ts)
def test_circular_polar(ts):
e = 0
w = 0
i = pi/2
check_orbit(300000, e, i, angles1, w, 3, ts)
check_orbit(300000, e, i, 1, w, angles1, ts)
for angle in angles1:
check_orbit(300000, e, i, angle, w, 3, ts)
check_orbit(300000, e, i, 1, w, angle, ts)
def test_elliptical(ts):
check_orbit(300000, .3, angles1[:4], 0, 4, 5, ts)
check_orbit(300000, .3, .1, angles1, 4, 5, ts)
check_orbit(300000, .3, .1, 2, angles1, 5, ts)
check_orbit(300000, .3, .1, 2, 4, angles1, ts)
for angle in angles1[:4]:
check_orbit(300000, .3, angle, 0, 4, 5, ts)
for angle in angles1:
check_orbit(300000, .3, .1, angle, 4, 5, ts)
check_orbit(300000, .3, .1, 2, angle, 5, ts)
check_orbit(300000, .3, .1, 2, 4, angle, ts)
def test_elliptical_equatorial(ts):
i = 0
Om = 0
check_orbit(300000, .3, i, Om, angles1, 5, ts)
check_orbit(300000, .3, i, Om, 4, angles1, ts)
for angle in angles1:
check_orbit(300000, .3, i, Om, angle, 5, ts)
check_orbit(300000, .3, i, Om, 4, angle, ts)
def test_elliptical_polar(ts):
i = pi/2
check_orbit(300000, .2, i, angles1, 2, 3, ts)
check_orbit(300000, .2, i, 1, angles1, 3, ts)
check_orbit(300000, .2, i, 1, 2, angles1, ts)
for angle in angles1:
check_orbit(300000, .2, i, angle, 2, 3, ts)
check_orbit(300000, .2, i, 1, angle, 3, ts)
check_orbit(300000, .2, i, 1, 2, angle, ts)
def test_parabolic(ts):
e = 1
check_orbit(300000, e, angles1[:4], 0, 4, 3, ts)
check_orbit(300000, e, 2, angles1, 4, 3, ts)
check_orbit(300000, e, 2, 3, angles1, 3, ts)
check_orbit(300000, e, 2, 3, 4, angles2, ts)
for angle in angles1[:4]:
check_orbit(300000, e, angle, 0, 4, 3, ts)
for angle in angles1:
check_orbit(300000, e, 2, angle, 4, 3, ts)
check_orbit(300000, e, 2, 3, angle, 3, ts)
for angle in angles2:
check_orbit(300000, e, 2, 3, 4, angle, ts)
def test_parabolic_equatorial(ts):
e = 1
i = 0
Om = 0
check_orbit(300000, e, i, Om, angles1, 2, ts)
check_orbit(300000, e, i, Om, 4, angles2, ts)
for angle in angles1:
check_orbit(300000, e, i, Om, angle, 2, ts)
for angle in angles2:
check_orbit(300000, e, i, Om, 4, angle, ts)
def test_parabolic_polar(ts):
e = 1
i = pi/2
check_orbit(300000, e, i, angles1, 2, 3, ts)
check_orbit(300000, e, i, 1, angles1, 3, ts)
check_orbit(300000, e, i, 1, 2, angles2, ts)
for angle in angles1:
check_orbit(300000, e, i, angle, 2, 3, ts)
check_orbit(300000, e, i, 1, angle, 3, ts)
for angle in angles2:
check_orbit(300000, e, i, 1, 2, angle, ts)
def test_hyperbolic(ts):
e = 1.3
check_orbit(300000, e, angles1[:4], 0, 4, .5, ts)
check_orbit(300000, e, 2, angles1, 4, .5, ts)
check_orbit(300000, e, 2, 3, angles1, .5, ts)
check_orbit(300000, e, 2, 3, 4, angles2, ts)
for angle in angles1[:4]:
check_orbit(300000, e, angle, 0, 4, .5, ts)
for angle in angles1:
check_orbit(300000, e, 2, angle, 4, .5, ts)
check_orbit(300000, e, 2, 3, angle, .5, ts)
for angle in angles2:
check_orbit(300000, e, 2, 3, 4, angle, ts)
def test_hyperbolic_equatorial(ts):
e = 1.3
i = 0
Om = 0
check_orbit(300000, e, i, Om, angles1, .5, ts)
check_orbit(300000, e, i, Om, 4, angles2, ts)
for angle in angles1:
check_orbit(300000, e, i, Om, angle, .5, ts)
for angle in angles2:
check_orbit(300000, e, i, Om, 4, angle, ts)
def test_hyperbolic_polar(ts):
e = 1.3
i = pi/2
check_orbit(300000, e, i, angles1, 2, .5, ts)
check_orbit(300000, e, i, 1, angles1, .5, ts)
check_orbit(300000, e, i, 1, 2, angles2, ts)
for angle in angles1:
check_orbit(300000, e, i, angle, 2, .5, ts)
check_orbit(300000, e, i, 1, angle, .5, ts)
for angle in angles2:
check_orbit(300000, e, i, 1, 2, angle, ts)
def test_all_types_at_once(ts):
check_orbit(p=array([300000]*12),
e=array([ 0, 0, 0, .3, .3, .2, 1, 1, 1, 1.3, 1.3, 1.3]),
i=array([.5, 0, pi/2, .1, 0, pi/2, 2, 0, pi/2, 2, 0, pi/2]),
Om=array([ 1, 0, 1, 2, 0, 1, 3, 0, 1, 3, 0, 1]),
w=array([ 0, 0, 0, 4, 4, 2, 4, 4, 2, 4, 4, 2]),
v=array([ 1, 5, 3, 5, 5, 3, 5, 5, 3, .5, .5, .5]),
ts=ts)
def test_gm_calculation(ts):
geocentric_pos = (moon - earth).at(ts.tdb(2015, 3, 2, 2))
geocentric_elements = osculating_elements_of(geocentric_pos, ECLIPTIC)
assert geocentric_elements._mu == GM_dict[3]
barycentric_pos = earth.at(ts.tdb(2015, 3, 2, 2))
barycentric_elements = osculating_elements_of(barycentric_pos, ECLIPTIC)
assert barycentric_elements._mu == GM_dict[0]
assert osculating_elements_of(barycentric_pos, ECLIPTIC, GM_dict[0])._mu == barycentric_elements._mu