import numpy as np
import pytest
from astropy import units as u
try:
from astrora.plotting.animation import animate_orbit, animate_orbit_3d
HAS_ANIMATION = True
except ImportError:
HAS_ANIMATION = False
try:
import matplotlib
matplotlib.use("Agg") import matplotlib.animation as mpl_animation
import matplotlib.pyplot as plt
HAS_MATPLOTLIB = True
except ImportError:
HAS_MATPLOTLIB = False
try:
import plotly.graph_objects as go
HAS_PLOTLY = True
except ImportError:
HAS_PLOTLY = False
from astrora.bodies import Earth, Mars
from astrora.twobody import Orbit
pytestmark = pytest.mark.skipif(not HAS_ANIMATION, reason="Animation module not available")
class TestAnimateOrbit:
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_single_orbit_basic(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, num_frames=10, fps=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
assert anim is not None
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_single_orbit_with_units(self):
r = [7000, 0, 0] << u.km
v = [0, 7.546, 0] << u.km / u.s
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_multiple_orbits(self):
orbit1 = Orbit.from_classical(
Earth,
a=7000 << u.km,
ecc=0.01 << u.one,
inc=0 << u.deg,
raan=0 << u.deg,
argp=0 << u.deg,
nu=0 << u.deg,
)
orbit2 = Orbit.from_classical(
Earth,
a=8000 << u.km,
ecc=0.05 << u.one,
inc=10 << u.deg,
raan=0 << u.deg,
argp=0 << u.deg,
nu=0 << u.deg,
)
anim = animate_orbit([orbit1, orbit2], num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_custom_duration(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
period = orbit.period.value if hasattr(orbit.period, "value") else orbit.period
anim = animate_orbit(orbit, duration=period / 2, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_without_trail(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, trail=False, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_dark_mode(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, dark=True, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_without_time_display(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, show_time=False, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_custom_fps(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
anim = animate_orbit(orbit, num_frames=20, fps=30)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_custom_axes(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
fig, ax = plt.subplots()
anim = animate_orbit(orbit, ax=ax, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
assert ax in fig.get_axes()
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_elliptical_orbit(self):
orbit = Orbit.from_classical(
Earth,
a=10000 << u.km,
ecc=0.3 << u.one,
inc=30 << u.deg,
raan=45 << u.deg,
argp=60 << u.deg,
nu=0 << u.deg,
)
anim = animate_orbit(orbit, num_frames=15)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animate_different_attractor(self):
orbit = Orbit.from_classical(
Mars,
a=5000 << u.km,
ecc=0.01 << u.one,
inc=0 << u.deg,
raan=0 << u.deg,
argp=0 << u.deg,
nu=0 << u.deg,
)
anim = animate_orbit(orbit, num_frames=10)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
class TestAnimateOrbit3D:
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_single_orbit_basic(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=10)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_with_units(self):
r = [7000, 0, 0] << u.km
v = [0, 0, 7.546] << u.km / u.s
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=10)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_multiple_orbits(self):
orbit1 = Orbit.from_classical(
Earth,
a=7000 << u.km,
ecc=0.01 << u.one,
inc=0 << u.deg,
raan=0 << u.deg,
argp=0 << u.deg,
nu=0 << u.deg,
)
orbit2 = Orbit.from_classical(
Earth,
a=8000 << u.km,
ecc=0.05 << u.one,
inc=20 << u.deg,
raan=30 << u.deg,
argp=0 << u.deg,
nu=0 << u.deg,
)
fig = animate_orbit_3d([orbit1, orbit2], num_frames=10)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_custom_duration(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
period = orbit.period.value if hasattr(orbit.period, "value") else orbit.period
fig = animate_orbit_3d(orbit, duration=period / 2, num_frames=10)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_without_trail(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, trail=False, num_frames=10)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_dark_mode(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, dark=True, num_frames=10)
assert isinstance(fig, go.Figure)
assert hasattr(fig.layout, "template")
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_controls(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=10)
assert len(fig.layout.updatemenus) > 0
assert any(
"Play" in str(button) for menu in fig.layout.updatemenus for button in menu.buttons
)
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_slider(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=10)
assert len(fig.layout.sliders) > 0
slider = fig.layout.sliders[0]
assert len(slider.steps) == 10
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_inclined_orbit(self):
r = np.array([7000e3, 3000e3, 2000e3]) v = np.array([-2000, 6000, 4000])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=15)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 15
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_animate_3d_custom_fps(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
fig = animate_orbit_3d(orbit, num_frames=20, fps=30)
assert isinstance(fig, go.Figure)
assert len(fig.frames) == 20
class TestAnimationIntegration:
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animation_preserves_orbit_properties(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
original_a = orbit.a
original_ecc = orbit.ecc
anim = animate_orbit(orbit, num_frames=10)
assert orbit.a == original_a
assert orbit.ecc == original_ecc
plt.close("all")
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_3d_animation_preserves_orbit_properties(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
original_a = orbit.a
original_inc = orbit.inc
fig = animate_orbit_3d(orbit, num_frames=10)
assert orbit.a == original_a
assert orbit.inc == original_inc
@pytest.mark.skipif(not HAS_MATPLOTLIB, reason="matplotlib not available")
def test_animation_num_frames_parameter(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
for num_frames in [5, 10, 20, 50]:
anim = animate_orbit(orbit, num_frames=num_frames)
assert isinstance(anim, mpl_animation.FuncAnimation)
plt.close("all")
@pytest.mark.skipif(not HAS_PLOTLY, reason="plotly not available")
def test_3d_animation_num_frames_parameter(self):
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
for num_frames in [5, 10, 20, 30]:
fig = animate_orbit_3d(orbit, num_frames=num_frames)
assert len(fig.frames) == num_frames
class TestAnimationErrors:
def test_animate_orbit_no_matplotlib(self, monkeypatch):
if not HAS_MATPLOTLIB:
pytest.skip("matplotlib not installed")
import astrora.plotting.animation as anim_module
monkeypatch.setattr(anim_module, "HAS_MATPLOTLIB", False)
r = np.array([7000e3, 0, 0])
v = np.array([0, 7546, 0])
orbit = Orbit.from_vectors(Earth, r, v)
with pytest.raises(ImportError, match="Matplotlib is required"):
animate_orbit(orbit, num_frames=10)
def test_animate_orbit_3d_no_plotly(self, monkeypatch):
if not HAS_PLOTLY:
pytest.skip("plotly not installed")
import astrora.plotting.animation as anim_module
monkeypatch.setattr(anim_module, "HAS_PLOTLY", False)
r = np.array([7000e3, 0, 0])
v = np.array([0, 0, 7546])
orbit = Orbit.from_vectors(Earth, r, v)
with pytest.raises(ImportError, match="Plotly is required"):
animate_orbit_3d(orbit, num_frames=10)
if __name__ == "__main__":
pytest.main([__file__, "-v"])