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
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ monte-carlo = [
"contextily>=1.0.0; python_version < '3.14'",
]

all = ["rocketpy[env-analysis]", "rocketpy[monte-carlo]"]
animation = [
"vedo>=2024.5.1"
]

all = ["rocketpy[env-analysis]", "rocketpy[monte-carlo]", "rocketpy[animation]"]


[tool.coverage.report]
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ numpy>=1.13
scipy>=1.0
matplotlib>=3.9.0 # Released May 15th 2024
netCDF4>=1.6.4
requests
pytz
requests>=2.25.0
pytz>=2020.1
simplekml
dill
196 changes: 196 additions & 0 deletions rocketpy/simulation/flight.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new methods should be placed within the rocketpy/plots folder and - maybe- imported to be reused in this file. We usually avoid to define plots inside the simulation.py file.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=too-many-lines
import math
import time
import warnings
from copy import deepcopy
from functools import cached_property
Expand Down Expand Up @@ -4039,7 +4040,202 @@ def export_kml(
color=color,
altitude_mode=altitude_mode,
)
def animate_trajectory(self, file_name, start=0, stop=None, time_step=0.1, **kwargs):
"""
6-DOF Animation of the flight trajectory using Vedo.

Parameters
----------
file_name : str
3D object file representing your rocket, usually in .stl format.
Example: "rocket.stl"
start : int, float, optional
Time for starting animation, in seconds. Default is 0.
stop : int, float, optional
Time for ending animation, in seconds. If None, uses self.t_final.
Default is None.
time_step : float, optional
Time step for data interpolation in the animation. Default is 0.1.
**kwargs : dict, optional
Additional keyword arguments to be passed to vedo.Plotter.show().
Common arguments:
- azimuth (float): Rotation in degrees around the vertical axis.
- elevation (float): Rotation in degrees above the horizon.
- roll (float): Rotation in degrees around the view axis.
- zoom (float): Zoom level (default 1).

Returns
-------
None

Raises
------
ImportError
If the 'vedo' package is not installed.

Notes
-----
This feature requires the 'vedo' package. Install it with:
pip install rocketpy[animation]
"""
try:
from vedo import Box, Line, Mesh, Plotter, settings
except ImportError as e:
raise ImportError(
"The animation feature requires the 'vedo' package. "
"Install it with:\n"
" pip install rocketpy[animation]\n"
"Or directly:\n"
" pip install vedo>=2024.5.1"
) from e

# Enable interaction if needed
try:
settings.allow_interaction = True
except AttributeError:
pass # Not available in newer versions of vedo

# Handle stop time
if stop is None:
stop = self.t_final

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The method doesn't validate that start < stop or that these values are within the valid time range [0, self.t_final]. Add validation to raise a ValueError if start >= stop or if either value is outside the valid simulation time range.

Suggested change
# Validate start and stop times
if (
start < 0
or stop < 0
or start > self.t_final
or stop > self.t_final
or start >= stop
):
raise ValueError(
f"Invalid animation time range: start={start}, stop={stop}. "
f"Both must be within [0, {self.t_final}] and start < stop."
)

Copilot uses AI. Check for mistakes.
# Define the world bounds based on trajectory
max_x = max(self.x[:, 1])
max_y = max(self.y[:, 1])
# Use simple logic for bounds
world = Box(
pos=[self.x(start), self.y(start), self.apogee],
length=max_x * 2 if max_x != 0 else 1000,
width=max_y * 2 if max_y != 0 else 1000,
height=self.apogee,
).wireframe()

# Load rocket mesh
rocket = Mesh(file_name).c("green")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing file validation: The method doesn't check if the specified STL file exists before attempting to load it. Add a check to raise a FileNotFoundError with a helpful message if the file doesn't exist, similar to the pattern used elsewhere in the codebase.

Copilot uses AI. Check for mistakes.
rocket.pos(self.x(start), self.y(start), 0).add_trail(n=len(self.x[:, 1]))
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add_trail() method is called with n=len(self.x[:, 1]) but this appears to conflict with the manual trail creation at lines 4117-4119. The add_trail() call is typically used for automatic trailing in vedo, but here you're also manually creating a Line object for the trail. Either use vedo's built-in trailing mechanism or the manual Line approach, not both. The manual Line approach seems more appropriate given your control over the trail points.

Suggested change
rocket.pos(self.x(start), self.y(start), 0).add_trail(n=len(self.x[:, 1]))
rocket.pos(self.x(start), self.y(start), 0)

Copilot uses AI. Check for mistakes.
# Create trail
trail_points = [[self.x(t), self.y(t), self.z(t) - self.env.elevation]
for t in np.arange(start, stop, time_step)]
trail = Line(trail_points, c="k", alpha=0.5)
Comment on lines +4117 to +4119
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trail is created using all trajectory points from start to stop before the animation loop, but it's only added to the scene once at line 4143. This means the trail shows the entire future path before the rocket animates, which may not be the desired behavior. Consider either:

  1. Building the trail incrementally during the animation loop, or
  2. Removing the trail entirely if it's meant to show only the past trajectory, or
  3. Clarifying in documentation that the trail shows the full trajectory path upfront.

Copilot uses AI. Check for mistakes.
# Setup Plotter
plt = Plotter(axes=1, interactive=False)
plt.show(world, rocket, __doc__, viewup="z", **kwargs)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of __doc__ as a display string is incorrect. __doc__ refers to the module/function's docstring and will display a large block of text. If you want to show a title or message, pass a proper string like "Flight Trajectory Animation" instead.

Suggested change
plt.show(world, rocket, __doc__, viewup="z", **kwargs)
plt.show(world, rocket, "Flight Trajectory Animation", viewup="z", **kwargs)

Copilot uses AI. Check for mistakes.

# Animation Loop
for t in np.arange(start, stop, time_step):
# Calculate rotation angle and vector from quaternions
# Note: This simple rotation logic mimics the old branch.
# Ideally, vedo handles orientation via matrix, but we stick
# to the provided logic for now.

# e0 is the scalar part of the quaternion
angle = np.arccos(2 * self.e0(t)**2 - 1)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential numerical instability: np.arccos(2 * self.e0(t)**2 - 1) can fail if the argument is slightly outside [-1, 1] due to numerical precision. Use np.clip() to ensure the argument is within valid range: np.arccos(np.clip(2 * self.e0(t)**2 - 1, -1, 1)).

Suggested change
angle = np.arccos(2 * self.e0(t)**2 - 1)
angle = np.arccos(np.clip(2 * self.e0(t)**2 - 1, -1, 1))

Copilot uses AI. Check for mistakes.
k = np.sin(angle / 2) if np.sin(angle / 2) != 0 else 1

# Update position and rotation
# Adjusting for ground elevation
rocket.pos(self.x(t), self.y(t), self.z(t) - self.env.elevation)
rocket.rotate_x(self.e1(t) / k)
rocket.rotate_y(self.e2(t) / k)
rocket.rotate_z(self.e3(t) / k)
Comment on lines +4132 to +4140
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quaternion-to-rotation conversion logic has several issues:

  1. The rotation is being applied incrementally in each animation frame without resetting the mesh orientation. This will cause cumulative rotation errors as rotate_x/y/z methods apply rotations relative to the current orientation, not the initial state.

  2. The quaternion-to-axis-angle conversion formula appears incorrect. For a unit quaternion (e0, e1, e2, e3), the angle should be 2 * arccos(e0), not arccos(2*e0^2 - 1).

  3. The rotation axis components should be normalized by sin(angle/2), which equals sqrt(e1^2 + e2^2 + e3^2), not the computed k value.

To fix this, you should either:

  • Reset the mesh orientation before each frame and apply the absolute rotation, or
  • Use vedo's orientation() method with a rotation matrix derived from the quaternion, or
  • Store the initial mesh orientation and compute relative rotations properly.

The same issue exists in the animate_rotate method at lines 4222-4229.

Copilot uses AI. Check for mistakes.

Comment on lines +4132 to +4141
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by zero protection is incomplete. When np.sin(angle / 2) == 0, setting k = 1 still leads to division by potentially zero values in the quaternion components (e1, e2, e3). If the angle is 0 (identity quaternion), all quaternion vector components should also be 0, making e1/k, e2/k, e3/k undefined. Consider handling the identity quaternion case explicitly by skipping rotation when angle is close to zero.

Suggested change
angle = np.arccos(2 * self.e0(t)**2 - 1)
k = np.sin(angle / 2) if np.sin(angle / 2) != 0 else 1
# Update position and rotation
# Adjusting for ground elevation
rocket.pos(self.x(t), self.y(t), self.z(t) - self.env.elevation)
rocket.rotate_x(self.e1(t) / k)
rocket.rotate_y(self.e2(t) / k)
rocket.rotate_z(self.e3(t) / k)
angle = np.arccos(2 * self.e0(t)**2 - 1)
# If angle is very small, skip rotation (identity quaternion)
if np.isclose(angle, 0.0, atol=1e-8):
# Update position only
rocket.pos(self.x(t), self.y(t), self.z(t) - self.env.elevation)
else:
k = np.sin(angle / 2)
# Update position and rotation
rocket.pos(self.x(t), self.y(t), self.z(t) - self.env.elevation)
rocket.rotate_x(self.e1(t) / k)
rocket.rotate_y(self.e2(t) / k)
rocket.rotate_z(self.e3(t) / k)

Copilot uses AI. Check for mistakes.
# update the scene
plt.show(world, rocket, trail)

# slow down to make animation visible
start_pause = time.time()
while time.time() - start_pause < time_step:
plt.render()
Comment on lines +4146 to +4148
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timing control using time.sleep() in a while loop is inefficient and may cause performance issues. Consider using a simpler approach like time.sleep(time_step) directly, or better yet, let vedo handle the animation timing if it provides such functionality. The current implementation will consume CPU cycles unnecessarily during the sleep period.

Suggested change
start_pause = time.time()
while time.time() - start_pause < time_step:
plt.render()
time.sleep(time_step)

Copilot uses AI. Check for mistakes.

if getattr(plt, 'escaped', False):
break

plt.interactive().close()
return None
Comment on lines +4043 to +4154
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new animation methods lack unit test coverage. While a verification script exists in tests/animation_verification/, there are no pytest-based unit tests that can be run as part of the CI pipeline. Consider adding tests to tests/unit/simulation/test_flight.py that:

  1. Mock the vedo import and verify the ImportError is raised when vedo is not installed
  2. Test parameter validation (e.g., invalid time ranges, missing files)
  3. Verify the methods can be called with vedo installed (even if just checking they don't raise exceptions)

This follows the testing pattern used for other Flight methods and ensures the feature doesn't break in future refactorings.

Copilot uses AI. Check for mistakes.

def animate_rotate(self, file_name, start=0, stop=None, time_step=0.1, **kwargs):
"""
Animation of rocket attitude (rotation) during the flight.

Parameters
----------
file_name : str
3D object file representing your rocket, usually in .stl format.
start : int, float, optional
Time for starting animation, in seconds. Default is 0.
stop : int, float, optional
Time for ending animation, in seconds. If None, uses self.t_final.
Default is None.
time_step : float, optional
Time step for data interpolation. Default is 0.1.
**kwargs : dict, optional
Additional keyword arguments to be passed to vedo.Plotter.show().
Common arguments:
- azimuth (float): Rotation in degrees around the vertical axis.
- elevation (float): Rotation in degrees above the horizon.
- roll (float): Rotation in degrees around the view axis.
- zoom (float): Zoom level (default 1).

Returns
-------
None

Raises
------
ImportError
If the 'vedo' package is not installed.
"""
try:
from vedo import Box, Mesh, Plotter, settings
except ImportError as e:
raise ImportError(
"The animation feature requires the 'vedo' package. "
"Install it with:\n"
" pip install rocketpy[animation]\n"
) from e

# Enable interaction if needed
try:
settings.allow_interaction = True
except AttributeError:
pass # Not available in newer versions of vedo

if stop is None:
stop = self.t_final

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The method doesn't validate that start < stop or that these values are within the valid time range [0, self.t_final]. Add validation to raise a ValueError if start >= stop or if either value is outside the valid simulation time range.

Suggested change
# Validate start and stop times
if not (0 <= start < stop <= self.t_final):
raise ValueError(
f"Invalid animation time range: start={start}, stop={stop}. "
f"Both must satisfy 0 <= start < stop <= {self.t_final}."
)

Copilot uses AI. Check for mistakes.
# Smaller box for rotation view
world = Box(
pos=[self.x(start), self.y(start), self.apogee],
length=max(self.x[:, 1]) * 0.2,
width=max(self.y[:, 1]) * 0.2,
Comment on lines +4207 to +4210
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue with Box dimensions: When trajectory has small or zero displacement in x or y directions, the world box can have zero or very small dimensions (e.g., max_x * 0.2 when max_x is small). This could result in invisible or improperly sized bounding boxes. Add minimum dimension checks to ensure the box remains visible, e.g., max(self.x[:, 1]) * 0.2 if max(self.x[:, 1]) > 10 else 10.

Suggested change
world = Box(
pos=[self.x(start), self.y(start), self.apogee],
length=max(self.x[:, 1]) * 0.2,
width=max(self.y[:, 1]) * 0.2,
min_box_dim = 10 # meters, minimum box dimension for visibility
world = Box(
pos=[self.x(start), self.y(start), self.apogee],
length=max(max(self.x[:, 1]) * 0.2, min_box_dim),
width=max(max(self.y[:, 1]) * 0.2, min_box_dim),

Copilot uses AI. Check for mistakes.
height=self.apogee * 0.1,
).wireframe()

rocket = Mesh(file_name).c("green")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing file validation: The method doesn't check if the specified STL file exists before attempting to load it. Add a check to raise a FileNotFoundError with a helpful message if the file doesn't exist.

Copilot uses AI. Check for mistakes.
# Initialize at origin relative to view
rocket.pos(self.x(start), self.y(start), 0).add_trail(n=len(self.x[:, 1]))
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The add_trail() method is called with n=len(self.x[:, 1]) but in the animate_rotate method, the trail doesn't make sense since the position is static (kept at start). Remove the add_trail() call from this method as it serves no purpose when the rocket doesn't translate.

Suggested change
rocket.pos(self.x(start), self.y(start), 0).add_trail(n=len(self.x[:, 1]))
rocket.pos(self.x(start), self.y(start), 0)

Copilot uses AI. Check for mistakes.

plt = Plotter(axes=1, interactive=False)
plt.show(world, rocket, __doc__, viewup="z", **kwargs)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of __doc__ as a display string is incorrect. __doc__ refers to the module/function's docstring and will display a large block of text. If you want to show a title or message, pass a proper string like "Rocket Rotation Animation" instead.

Suggested change
plt.show(world, rocket, __doc__, viewup="z", **kwargs)
plt.show(world, rocket, "Rocket Rotation Animation", viewup="z", **kwargs)

Copilot uses AI. Check for mistakes.

for t in np.arange(start, stop, time_step):
angle = np.arccos(2 * self.e0(t)**2 - 1)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential numerical instability: Same issue as in animate_trajectory. Use np.clip() to ensure the arccos argument is within valid range: np.arccos(np.clip(2 * self.e0(t)**2 - 1, -1, 1)).

Suggested change
angle = np.arccos(2 * self.e0(t)**2 - 1)
angle = np.arccos(np.clip(2 * self.e0(t)**2 - 1, -1, 1))

Copilot uses AI. Check for mistakes.
k = np.sin(angle / 2) if np.sin(angle / 2) != 0 else 1

# Keep position static (relative start) to observe only rotation
rocket.pos(self.x(start), self.y(start), 0)
rocket.rotate_x(self.e1(t) / k)
rocket.rotate_y(self.e2(t) / k)
rocket.rotate_z(self.e3(t) / k)

plt.show(world, rocket)

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a typo: "slow down to make animation visible" should better explain what's happening. However, more importantly, there's no actual pause mechanism in the animate_rotate method (unlike animate_trajectory), which means the rotation animation will run at maximum speed without frame-rate control. Add a similar timing control as in animate_trajectory to maintain consistent animation speed.

Suggested change
# Pause to maintain consistent animation speed and make each frame visible
time.sleep(time_step)

Copilot uses AI. Check for mistakes.
if getattr(plt, 'escaped', False):
break

plt.interactive().close()
return None

def info(self):
"""Prints out a summary of the data available about the Flight."""
self.prints.all()
Expand Down
Empty file.
73 changes: 73 additions & 0 deletions tests/animation_verification/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import os
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docstring for module. According to RocketPy's coding guidelines (NumPy style docstrings for all public modules), add a module-level docstring explaining the purpose of this verification script and how to run it.

Copilot uses AI. Check for mistakes.
import traceback
from rocketpy import Environment, Flight
from rocket_stl import create_rocket_stl
from rocket_setup import get_calisto_rocket


def run_simulation_and_test_animation():
print("🚀 Setting up simulation (Calisto Example)...")

# 1. Setup Environment
env = Environment(latitude=32.990254, longitude=-106.974998, elevation=1400)
env.set_date((2025, 12, 5, 12))
env.set_atmospheric_model(type="standard_atmosphere")

# 2. Get Rocket
try:
calisto = get_calisto_rocket()
except Exception as e:
print(f"❌ Failed to configure rocket: {e}")
return

# 3. Simulate Flight
test_flight = Flight(
rocket=calisto, environment=env, rail_length=5.2, inclination=85, heading=0
)

print(f"✅ Flight simulated successfully! Apogee: {test_flight.apogee:.2f} m")

# 4. Test Animation Methods
stl_file = "rocket_model.stl"
# Note: Depending on where you run this, you might need to adjust imports
# or ensure create_rocket_stl is available in scope.
create_rocket_stl(stl_file, length=300, radius=50)

print("\n🎥 Testing animate_trajectory()...")

try:
test_flight.animate_trajectory(
file_name=stl_file,
stop=15.0,
time_step=0.05,
azimuth=-45, # Rotates view 45 degrees left
elevation=30, # Tilts view 30 degrees up
zoom=1.2,
)
print("✅ animate_trajectory() executed successfully.")
except Exception as e:
print(f"❌ animate_trajectory() Failed: {e}")
traceback.print_exc()

print("\n🔄 Testing animate_rotate()...")

try:
test_flight.animate_rotate(
file_name=stl_file,
time_step=1.0,
azimuth=-45, # Rotates view 45 degrees left
elevation=30, # Tilts view 30 degrees up
zoom=1.2,
)
print("✅ animate_rotate() executed successfully.")
except Exception as e:
print(f"❌ animate_rotate() Failed: {e}")
traceback.print_exc()

# Cleanup
if os.path.exists(stl_file):
os.remove(stl_file)


if __name__ == "__main__":
run_simulation_and_test_animation()
Loading
Loading