Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions src/orbital/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,9 @@ def elements_from_state_vector(r, v, mu):
# Inclination is the angle between the angular
# momentum vector and its z component.
i = acos(h.z / norm(h))
i_small = abs(i) < SMALL_NUMBER or abs(i - pi) < SMALL_NUMBER

if abs(i) < SMALL_NUMBER:
if i_small:
# For non-inclined orbits, raan is undefined;
# set to zero by convention
raan = 0
Expand All @@ -284,40 +285,50 @@ def elements_from_state_vector(r, v, mu):
# Argument of periapsis is the angle between
# eccentricity vector and its x component.
arg_pe = acos(ev.x / norm(ev))
if ev.y < 0:
arg_pe = -arg_pe
# If plane is upside down, arg_pe needs to be inverted.
if abs(i) > pi * 0.5:
arg_pe = -arg_pe
arg_pe = mod(arg_pe, 2 * pi)
else:
# Right ascension of ascending node is the angle
# between the node vector and its x component.
raan = acos(n.x / norm(n))
if n.y < 0:
raan = 2 * pi - raan
raan = mod(-raan, 2 * pi)

# Argument of periapsis is angle between
# node and eccentricity vectors.
arg_pe = acos(dot(n, ev) / (norm(n) * norm(ev)))
if abs(e) < SMALL_NUMBER:
# For circular orbits, place periapsis
# at ascending node by convention
arg_pe = 0
else:
# Argument of periapsis is angle between
# node and eccentricity vectors.
arg_pe = vector_angle(n, ev)

if abs(e) < SMALL_NUMBER:
if abs(i) < SMALL_NUMBER:
if i_small:
# True anomaly is angle between position
# vector and its x component.
f = acos(r.x / norm(r))
if v.x > 0:
f = 2 * pi - f
f = mod(-f, 2 * pi)
else:
# True anomaly is angle between node
# vector and position vector.
f = acos(dot(n, r) / (norm(n) * norm(r)))
f = vector_angle(n, r)
if dot(n, v) > 0:
f = 2 * pi - f
f = mod(-f, 2 * pi)
else:
if ev.z < 0:
arg_pe = 2 * pi - arg_pe
arg_pe = mod(-arg_pe, 2 * pi)

# True anomaly is angle between eccentricity
# vector and position vector.
f = acos(dot(ev, r) / (norm(ev) * norm(r)))

f = vector_angle(ev, r)
if dot(r, v) < 0:
f = 2 * pi - f
f = mod(-f, 2 * pi)

return OrbitalElements(a=a, e=e, i=i, raan=raan, arg_pe=arg_pe, f=f)

Expand Down Expand Up @@ -373,6 +384,17 @@ def divmod(x, y):
return (floor(x / y), mod(x, y))


def vector_angle(v1, v2):
"""Return the unsigned minimum angle between two XyzVectors [rad].

The result will always be between 0 and pi inclusive.
"""
# The parameter to acos is clamped to the range [-1.0, 1.0]. If this is not
# done, rounding errors can make the value slightly below -1.0 or above 1.0,
# resulting in nan. These should simply result in pi and 0.0, respectively.
return acos(np.clip(dot(v1, v2) / (norm(v1) * norm(v2)), -1.0, 1.0))


# Objects for package


Expand Down
138 changes: 65 additions & 73 deletions tests/test_orbital.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,26 +672,19 @@ def test_from_state_vector_circular_retrograde(self):
R = Position(RADIUS, 0, 0)
V = Velocity(0, -sqrt(earth.mu / RADIUS), 0)

# XXX This erroneously generates warnings and raises an assertion, due
# to internal values being NaN.
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
self.assertRaises(
AssertionError, KeplerianElements.from_state_vector, R, V, body=earth
)
# The expected values, after this bug is fixed:
# numpy.testing.assert_almost_equal(orbit.r, R)
# numpy.testing.assert_almost_equal(orbit.v, V)
# self.assertAlmostEqual(orbit.a, RADIUS)
# self.assertAlmostEqual(orbit.e, 0.0)
# self.assertAlmostEqual(orbit.i, radians(180))
# self.assertAlmostEqual(orbit.raan, 0.0)
# self.assertAlmostEqual(orbit.arg_pe, 0.0)
# self.assertAlmostEqual(orbit.M0, 0.0)

# self.assertAlmostEqual(orbit.ref_epoch, J2000)
# self.assertEqual(orbit.body, earth)
# self.assertAlmostEqual(orbit.t, 0.0)
orbit = KeplerianElements.from_state_vector(R, V, body=earth)
numpy.testing.assert_almost_equal(orbit.r, R)
numpy.testing.assert_almost_equal(orbit.v, V)
self.assertAlmostEqual(orbit.a, RADIUS)
self.assertAlmostEqual(orbit.e, 0.0)
self.assertAlmostEqual(orbit.i, radians(180))
self.assertAlmostEqual(orbit.raan, 0.0)
self.assertAlmostEqual(orbit.arg_pe, 0.0)
self.assertAlmostEqual(orbit.M0, 0.0)

self.assertAlmostEqual(orbit.ref_epoch, J2000)
self.assertEqual(orbit.body, earth)
self.assertAlmostEqual(orbit.t, 0.0)

def test_from_state_vector_inclined(self):
# Inclined circular orbit, 1/4 of the way around.
Expand All @@ -700,20 +693,14 @@ def test_from_state_vector_inclined(self):
R = Position(-RADIUS * 0.5 * sqrt(2), 0, RADIUS * 0.5 * sqrt(2))
V = Velocity(0, -sqrt(earth.mu / RADIUS), 0)

with warnings.catch_warnings():
# XXX This has a warning for dividing by zero.
warnings.simplefilter("ignore", category=RuntimeWarning)
orbit = KeplerianElements.from_state_vector(R, V, body=earth)
# XXX: r, v and arg_pe are nan due to a bug in from_state_vector.
# This happens for a perfect circle, but doesn't happen in
# test_from_state_vector_circular for some reason.
# numpy.testing.assert_almost_equal(orbit.r, R)
# numpy.testing.assert_almost_equal(orbit.v, V)
orbit = KeplerianElements.from_state_vector(R, V, body=earth)
numpy.testing.assert_almost_equal(orbit.r, R)
numpy.testing.assert_almost_equal(orbit.v, V)
self.assertAlmostEqual(orbit.a, RADIUS)
self.assertAlmostEqual(orbit.e, 0.0)
self.assertAlmostEqual(orbit.i, radians(45))
self.assertAlmostEqual(orbit.raan, radians(90))
# self.assertAlmostEqual(orbit.arg_pe, 0.0)
self.assertAlmostEqual(orbit.arg_pe, 0.0)
self.assertAlmostEqual(orbit.M0, radians(90))

self.assertAlmostEqual(orbit.ref_epoch, J2000)
Expand Down Expand Up @@ -761,15 +748,13 @@ def test_from_state_vector_elliptical_arg_pe_gt_180(self):
V = Velocity(16703.9010129, 0, 0)

orbit = KeplerianElements.from_state_vector(R, V, body=earth)
# XXX These do not match (they are 180° out, due to arg_pe).
# numpy.testing.assert_almost_equal(orbit.r, R)
# numpy.testing.assert_almost_equal(orbit.v, V)
numpy.testing.assert_almost_equal(orbit.r, R)
numpy.testing.assert_almost_equal(orbit.v, V)
self.assertAlmostEqual(orbit.a, 10000000.0, places=3)
self.assertAlmostEqual(orbit.e, 0.75)
self.assertAlmostEqual(orbit.i, 0.0)
self.assertAlmostEqual(orbit.raan, 0.0)
# XXX This is incorrectly calculated as 90°.
# self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
self.assertAlmostEqual(orbit.M0, 0.0)

self.assertAlmostEqual(orbit.ref_epoch, J2000)
Expand All @@ -784,24 +769,19 @@ def test_from_state_vector_f_at_periapsis(self):
R = Position(0, -1767766.952966369, -1767766.952966369)
V = Velocity(16703.901013, 0, 0)

# XXX Currently crashes due to the above bug.
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=RuntimeWarning)
self.assertRaises(
AssertionError, KeplerianElements.from_state_vector, R, V, body=earth
)
# numpy.testing.assert_almost_equal(orbit.r, R)
# numpy.testing.assert_almost_equal(orbit.v, V)
# self.assertAlmostEqual(orbit.a, 10000000.0, places=2)
# self.assertAlmostEqual(orbit.e, 0.75)
# self.assertAlmostEqual(orbit.i, radians(45))
# self.assertAlmostEqual(orbit.raan, 0.0)
# self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
# self.assertAlmostEqual(orbit.M0, 0.0)

# self.assertAlmostEqual(orbit.ref_epoch, J2000)
# self.assertEqual(orbit.body, earth)
# self.assertAlmostEqual(orbit.t, 0.0)
orbit = KeplerianElements.from_state_vector(R, V, body=earth)
numpy.testing.assert_almost_equal(orbit.r, R)
numpy.testing.assert_almost_equal(orbit.v, V)
self.assertAlmostEqual(orbit.a, 10000000.0, places=2)
self.assertAlmostEqual(orbit.e, 0.75)
self.assertAlmostEqual(orbit.i, radians(45))
self.assertAlmostEqual(orbit.raan, 0.0)
self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
self.assertAlmostEqual(orbit.M0, 0.0)

self.assertAlmostEqual(orbit.ref_epoch, J2000)
self.assertEqual(orbit.body, earth)
self.assertAlmostEqual(orbit.t, 0.0)

def test_from_state_vector_iss(self):
# ISS (Zarya) from 2008-09-20 12:25:40
Expand Down Expand Up @@ -832,23 +812,39 @@ def test_from_state_vector_roundtrips(self):
# Circular flat, f > 180°.
(Position(0, -10000000, 0), Velocity(6313.4811435530555, 0, 0)),
# Circular flat, retrograde.
# (Position(10000000, 0, 0), Velocity(0, -6313.4811435530555, 0)),
(Position(10000000, 0, 0), Velocity(0, -6313.4811435530555, 0)),
# Circular inclined, prograde.
# (Position(10000000, 0, 0), Velocity(0, 4464.305329499764, 4464.305329499764)),
(
Position(10000000, 0, 0),
Velocity(0, 4464.305329499764, 4464.305329499764),
),
# Circular inclined, prograde (raan 90°, M0 90°).
# (Position(-7071067.811865476, 0, 7071067.811865476), Velocity(0, -6313.4811435530555, 0)),
(
Position(-7071067.811865476, 0, 7071067.811865476),
Velocity(0, -6313.4811435530555, 0),
),
# Circular inclined, raan > 180°.
# (Position(0, -10000000, 0), Velocity(4464.305329499764, 0, 4464.305329499764)),
(
Position(0, -10000000, 0),
Velocity(4464.305329499764, 0, 4464.305329499764),
),
# Circular inclined, f > 180°.
# (Position(0, -7071067.811865476, -7071067.811865476), Velocity(6313.4811435530555, 0, 0)),
(
Position(0, -7071067.811865476, -7071067.811865476),
Velocity(6313.4811435530555, 0, 0),
),
# Circular polar.
# (Position(10000000, 0, 0), Velocity(0, 0, 6313.4811435530555)),
(Position(10000000, 0, 0), Velocity(0, 0, 6313.4811435530555)),
# Elliptical flat (e=0.75), prograde.
(Position(2500000, 0, 0), Velocity(0, 16703.901013, 0)),
# Elliptical flat (e=0.75), arg_pe > 180°.
# (Position(0, -2500000, 0), Velocity(16703.901013, 0, 0)),
(Position(0, -2500000, 0), Velocity(16703.901013, 0, 0)),
# Elliptical flat (e=0.75), retrograde.
# (Position(2500000, 0, 0), Velocity(0, -16703.901013, 0)),
(Position(2500000, 0, 0), Velocity(0, -16703.901013, 0)),
# Elliptical flat (e=0.75), retrograde, arg_pe = 90°, at periapsis.
(Position(0, -2500000, 0), Velocity(-16703.901013, 0, 0)),
# Elliptical flat, retrograde, arg_pe = 90°, at apoapsis.
(Position(0, -2500000, 0), Velocity(-10000.0, 0, 0)),
# Elliptical flat (e=0.75), f > 180°.
(
Position(-13255776.4031414, -5408888.899183, 0),
Expand All @@ -860,7 +856,10 @@ def test_from_state_vector_roundtrips(self):
Velocity(0, 11811.441678561141, 11811.441678561141),
),
# Elliptical inclined, arg_pe > 180°.
# (Position(0, -1767766.952966369, -1767766.952966369), Velocity(16703.901013, 0, 0)),
(
Position(0, -1767766.952966369, -1767766.952966369),
Velocity(16703.901013, 0, 0),
),
]

for i, (r, v) in enumerate(CASES):
Expand Down Expand Up @@ -1077,19 +1076,12 @@ def test_set_v_arg_pe_gt_180(self):
# arg_pe = 270°.
V = Velocity(-10000, 0, 0)

def set_v(value):
orbit.v = value

# XXX The 'r and v changed' detection logic is triggered in this case,
# causing a RuntimeError to be raised. If this was not raised, the
# following asserts would be wildly off.
self.assertRaises(RuntimeError, set_v, V)
# numpy.testing.assert_almost_equal(orbit.r, R)
# numpy.testing.assert_almost_equal(orbit.v, V)
orbit.v = V
numpy.testing.assert_almost_equal(orbit.r, R)
numpy.testing.assert_almost_equal(orbit.v, V)
# arg_pe should have rotated around 180°, and M0 to match (so r is in
# the same spot as it was before).
# XXX This is incorrectly calculated as 90°.
# self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
self.assertAlmostEqual(orbit.arg_pe, radians(270.0))
self.assertAlmostEqual(orbit.M0, radians(180.0))

def test_set_n(self):
Expand Down
Loading