Skip to content

Commit 62912d7

Browse files
GuilhermeAsuraGui-FernandesBR
authored andcommitted
test: add modular verification suite for 3D animation methods
1 parent 5161699 commit 62912d7

4 files changed

Lines changed: 373 additions & 0 deletions

File tree

tests/animation_verification/__init__.py

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import os
2+
import traceback
3+
from rocketpy import Environment, Flight
4+
from rocket_stl import create_rocket_stl
5+
from rocket_setup import get_calisto_rocket
6+
7+
def run_simulation_and_test_animation():
8+
print("🚀 Setting up simulation (Calisto Example)...")
9+
10+
# 1. Setup Environment
11+
env = Environment(latitude=32.990254, longitude=-106.974998, elevation=1400)
12+
env.set_date((2025, 12, 5, 12))
13+
env.set_atmospheric_model(type="standard_atmosphere")
14+
15+
# 2. Get Rocket
16+
try:
17+
calisto = get_calisto_rocket()
18+
except Exception as e:
19+
print(f"❌ Failed to configure rocket: {e}")
20+
return
21+
22+
# 3. Simulate Flight
23+
test_flight = Flight(
24+
rocket=calisto, environment=env, rail_length=5.2, inclination=85, heading=0
25+
)
26+
27+
print(f"✅ Flight simulated successfully! Apogee: {test_flight.apogee:.2f} m")
28+
29+
# 4. Test Animation Methods
30+
stl_file = "rocket_model.stl"
31+
# Note: Depending on where you run this, you might need to adjust imports
32+
# or ensure create_rocket_stl is available in scope.
33+
create_rocket_stl(stl_file, length=300, radius=50)
34+
35+
print("\n🎥 Testing animate_trajectory()...")
36+
37+
try:
38+
test_flight.animate_trajectory(
39+
file_name=stl_file,
40+
stop=15.0,
41+
time_step=0.05,
42+
azimuth=-45, # Rotates view 45 degrees left
43+
elevation=30, # Tilts view 30 degrees up
44+
zoom=1.2
45+
)
46+
print("✅ animate_trajectory() executed successfully.")
47+
except Exception as e:
48+
print(f"❌ animate_trajectory() Failed: {e}")
49+
traceback.print_exc()
50+
51+
print("\n🔄 Testing animate_rotate()...")
52+
53+
try:
54+
test_flight.animate_rotate(
55+
file_name=stl_file,
56+
time_step=1.0,
57+
azimuth=-45, # Rotates view 45 degrees left
58+
elevation=30, # Tilts view 30 degrees up
59+
zoom=1.2
60+
)
61+
print("✅ animate_rotate() executed successfully.")
62+
except Exception as e:
63+
print(f"❌ animate_rotate() Failed: {e}")
64+
traceback.print_exc()
65+
66+
# Cleanup
67+
if os.path.exists(stl_file):
68+
os.remove(stl_file)
69+
70+
if __name__ == "__main__":
71+
run_simulation_and_test_animation()
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import os
2+
from rocketpy import SolidMotor, Rocket
3+
4+
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
5+
ROOT_DIR = os.path.dirname(os.path.dirname(CURRENT_DIR))
6+
DATA_DIR = os.path.join(ROOT_DIR, "data")
7+
8+
OFF_DRAG_PATH = os.path.join(DATA_DIR, "rockets/calisto/powerOffDragCurve.csv")
9+
ON_DRAG_PATH = os.path.join(DATA_DIR, "rockets/calisto/powerOnDragCurve.csv")
10+
AIRFOIL_PATH = os.path.join(DATA_DIR, "airfoils/NACA0012-radians.txt")
11+
MOTOR_PATH = os.path.join(DATA_DIR, "motors/cesaroni/Cesaroni_M1670.eng")
12+
13+
def get_motor():
14+
"""Locates the motor file and returns a configured SolidMotor object."""
15+
# We can now point directly to the file without searching
16+
if not os.path.exists(MOTOR_PATH):
17+
raise FileNotFoundError(f"Could not find Cesaroni_M1670.eng at: {MOTOR_PATH}")
18+
19+
return SolidMotor(
20+
thrust_source=MOTOR_PATH,
21+
dry_mass=1.815,
22+
dry_inertia=(0.125, 0.125, 0.002),
23+
nozzle_radius=33 / 1000,
24+
grain_number=5,
25+
grain_density=1815,
26+
grain_outer_radius=33 / 1000,
27+
grain_initial_inner_radius=15 / 1000,
28+
grain_initial_height=120 / 1000,
29+
grain_separation=5 / 1000,
30+
grains_center_of_mass_position=0.397,
31+
center_of_dry_mass_position=0.317,
32+
nozzle_position=0,
33+
burn_time=3.9,
34+
throat_radius=11 / 1000,
35+
coordinate_system_orientation="nozzle_to_combustion_chamber",
36+
)
37+
38+
def get_calisto_rocket():
39+
"""Configures and returns the Calisto Rocket object."""
40+
motor = get_motor()
41+
42+
calisto = Rocket(
43+
radius=127 / 2000,
44+
mass=14.426,
45+
inertia=(6.321, 6.321, 0.034),
46+
power_off_drag=OFF_DRAG_PATH,
47+
power_on_drag=ON_DRAG_PATH,
48+
center_of_mass_without_motor=0,
49+
coordinate_system_orientation="tail_to_nose",
50+
)
51+
52+
calisto.add_motor(motor, position=-1.255)
53+
calisto.set_rail_buttons(
54+
upper_button_position=0.0818,
55+
lower_button_position=-0.618,
56+
angular_position=45,
57+
)
58+
59+
# Aerodynamic surfaces
60+
calisto.add_nose(length=0.55829, kind="vonKarman", position=1.27)
61+
calisto.add_trapezoidal_fins(
62+
n=4,
63+
root_chord=0.120,
64+
tip_chord=0.060,
65+
span=0.110,
66+
position=-1.04956,
67+
cant_angle=0,
68+
airfoil=(AIRFOIL_PATH, "radians"),
69+
)
70+
calisto.add_tail(
71+
top_radius=0.0635, bottom_radius=0.0435, length=0.060, position=-1.194656
72+
)
73+
74+
# Parachutes
75+
calisto.add_parachute(
76+
name="Main", cd_s=10.0, trigger=800, sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5)
77+
)
78+
calisto.add_parachute(
79+
name="Drogue", cd_s=1.0, trigger="apogee", sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5)
80+
)
81+
82+
return calisto
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import math
2+
import numpy as np
3+
from stl import mesh # Requires numpy-stl package: pip install numpy-stl
4+
import struct
5+
6+
def create_rocket_stl(filename="rocket_model.stl", length=100, radius=10):
7+
"""
8+
Creates a detailed rocket-shaped STL file with proper scale.
9+
10+
Parameters
11+
----------
12+
filename : str
13+
Output filename
14+
length : float
15+
Rocket length in meters
16+
radius : float
17+
Rocket base radius in meters
18+
"""
19+
20+
# More realistic proportions
21+
nose_length = length * 0.25
22+
body_length = length * 0.65
23+
engine_length = length * 0.1
24+
25+
# Fin parameters
26+
fin_height = radius * 3
27+
fin_width = radius * 2
28+
fin_thickness = radius * 0.2
29+
num_fins = 4
30+
31+
# Calculate number of vertices for smooth curves
32+
num_segments = 36
33+
vertices = []
34+
faces = []
35+
36+
# Helper function to add triangle
37+
def add_triangle(v1, v2, v3):
38+
nonlocal faces
39+
idx = len(vertices)
40+
vertices.extend([v1, v2, v3])
41+
faces.append([idx, idx+1, idx+2])
42+
43+
# Helper function to add quadrilateral as two triangles
44+
def add_quad(v1, v2, v3, v4):
45+
add_triangle(v1, v2, v3)
46+
add_triangle(v1, v3, v4)
47+
48+
# 1. Create nose cone (pointed ogive)
49+
nose_vertices = []
50+
for i in range(num_segments + 1):
51+
angle = 2 * math.pi * i / num_segments
52+
x = math.cos(angle) * radius
53+
y = math.sin(angle) * radius
54+
z = length # Tip at full length
55+
nose_vertices.append([x, y, z])
56+
57+
# Create nose cone triangles
58+
nose_tip = [0, 0, length]
59+
for i in range(num_segments):
60+
v1 = nose_tip
61+
v2 = nose_vertices[i]
62+
v3 = nose_vertices[(i + 1) % num_segments]
63+
add_triangle(v1, v2, v3)
64+
65+
# 2. Create main body cylinder
66+
body_z_start = length - nose_length
67+
body_z_end = engine_length
68+
69+
# Create vertices for top and bottom circles of body
70+
top_circle = []
71+
bottom_circle = []
72+
73+
for i in range(num_segments):
74+
angle = 2 * math.pi * i / num_segments
75+
x = math.cos(angle) * radius
76+
y = math.sin(angle) * radius
77+
78+
top_circle.append([x, y, body_z_start])
79+
bottom_circle.append([x, y, body_z_end])
80+
81+
# Create body cylinder triangles
82+
for i in range(num_segments):
83+
next_i = (i + 1) % num_segments
84+
85+
# Side quad
86+
v1 = top_circle[i]
87+
v2 = top_circle[next_i]
88+
v3 = bottom_circle[next_i]
89+
v4 = bottom_circle[i]
90+
add_quad(v1, v2, v3, v4)
91+
92+
# Top circle (connecting to nose)
93+
v1 = top_circle[i]
94+
v2 = top_circle[next_i]
95+
v3 = nose_vertices[i]
96+
add_triangle(v1, v2, v3)
97+
98+
# 3. Create engine nozzle (truncated cone)
99+
engine_radius = radius * 0.7
100+
engine_z_end = 0
101+
102+
# Create vertices for engine circles
103+
engine_top_circle = []
104+
engine_bottom_circle = []
105+
106+
for i in range(num_segments):
107+
angle = 2 * math.pi * i / num_segments
108+
x_top = math.cos(angle) * radius
109+
y_top = math.sin(angle) * radius
110+
x_bottom = math.cos(angle) * engine_radius
111+
y_bottom = math.sin(angle) * engine_radius
112+
113+
engine_top_circle.append([x_top, y_top, body_z_end])
114+
engine_bottom_circle.append([x_bottom, y_bottom, engine_z_end])
115+
116+
# Create engine nozzle triangles
117+
for i in range(num_segments):
118+
next_i = (i + 1) % num_segments
119+
120+
# Side quad
121+
v1 = engine_top_circle[i]
122+
v2 = engine_top_circle[next_i]
123+
v3 = engine_bottom_circle[next_i]
124+
v4 = engine_bottom_circle[i]
125+
add_quad(v1, v2, v3, v4)
126+
127+
# Connect to body
128+
v1 = engine_top_circle[i]
129+
v2 = engine_top_circle[next_i]
130+
v3 = bottom_circle[i]
131+
add_triangle(v1, v2, v3)
132+
133+
# 4. Create fins
134+
for fin_num in range(num_fins):
135+
fin_angle = 2 * math.pi * fin_num / num_fins
136+
137+
# Fin vertices
138+
fin_base_center = [0, 0, body_z_end * 0.5]
139+
140+
# Inner fin edge (attached to rocket)
141+
inner_x = math.cos(fin_angle) * radius
142+
inner_y = math.sin(fin_angle) * radius
143+
inner_top = [inner_x, inner_y, body_z_end + fin_height * 0.3]
144+
inner_bottom = [inner_x, inner_y, body_z_end - fin_height * 0.7]
145+
146+
# Outer fin edge
147+
outer_x = math.cos(fin_angle) * (radius + fin_width)
148+
outer_y = math.sin(fin_angle) * (radius + fin_width)
149+
outer_top = [outer_x, outer_y, body_z_end + fin_height * 0.3]
150+
outer_bottom = [outer_x, outer_y, body_z_end - fin_height * 0.7]
151+
152+
# Fin side that attaches to rocket
153+
add_quad(inner_top, inner_bottom, bottom_circle[fin_num * num_segments // num_fins],
154+
bottom_circle[(fin_num * num_segments // num_fins + 1) % num_segments])
155+
156+
# Fin surfaces
157+
# Front face
158+
add_quad(inner_top, outer_top, outer_bottom, inner_bottom)
159+
160+
# Top face
161+
add_triangle(inner_top, outer_top,
162+
[inner_top[0] + math.cos(fin_angle + math.pi/2) * fin_thickness * 0.5,
163+
inner_top[1] + math.sin(fin_angle + math.pi/2) * fin_thickness * 0.5,
164+
inner_top[2]])
165+
166+
# Bottom face
167+
add_triangle(inner_bottom, outer_bottom,
168+
[inner_bottom[0] + math.cos(fin_angle + math.pi/2) * fin_thickness * 0.5,
169+
inner_bottom[1] + math.sin(fin_angle + math.pi/2) * fin_thickness * 0.5,
170+
inner_bottom[2]])
171+
172+
# 5. Create bottom cap
173+
center_bottom = [0, 0, engine_z_end]
174+
for i in range(num_segments):
175+
next_i = (i + 1) % num_segments
176+
add_triangle(center_bottom, engine_bottom_circle[i], engine_bottom_circle[next_i])
177+
178+
# Convert to numpy arrays
179+
vertices_array = np.array(vertices)
180+
faces_array = np.array(faces)
181+
182+
# Create the mesh
183+
rocket_mesh = mesh.Mesh(np.zeros(faces_array.shape[0], dtype=mesh.Mesh.dtype))
184+
for i, face in enumerate(faces_array):
185+
for j in range(3):
186+
rocket_mesh.vectors[i][j] = vertices_array[face[j]]
187+
188+
# Write the mesh to file
189+
rocket_mesh.save(filename)
190+
print(f"Rocket STL saved to {filename}")
191+
print(f"Total triangles: {len(faces)}")
192+
193+
# Alternative version using pure Python STL generation
194+
def create_rocket_stl_simple(filename="rocket_simple.stl", length=100, radius=10):
195+
"""Simpler version using pure Python without numpy-stl dependency"""
196+
197+
def write_stl(faces, filename):
198+
with open(filename, 'wb') as f:
199+
# Write 80 byte header
200+
f.write(b'\x00' * 80)
201+
# Write number of faces
202+
f.write(struct.pack('<I', len(faces)))
203+
204+
for face in faces:
205+
# Write normal (dummy, will be recalculated)
206+
f.write(struct.pack('<fff', 0.0, 0.0, 0.0))
207+
# Write three vertices
208+
for vertex in face:
209+
f.write(struct.pack('<fff', vertex[0], vertex[1], vertex[2]))
210+
# Write attribute byte count
211+
f.write(struct.pack('<H', 0))
212+
213+
# Simplified geometry - create a more detailed rocket than original but simpler than full version
214+
num_segments = 16
215+
faces = []
216+
217+
# Add more triangles for better shape...
218+
# (You can combine the face generation logic from above with this write_stl function)
219+
220+
write_stl(faces, filename)

0 commit comments

Comments
 (0)