Skip to content

Commit 9805868

Browse files
authored
Merge branch 'develop' into copilot/enhance-drag-curve-functionality
2 parents 05ebb10 + 607af52 commit 9805868

23 files changed

+2826
-25
lines changed

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,25 @@ Attention: The newest changes should be on top -->
3434

3535
- ENH: Add multi-dimensional drag coefficient support (Cd as function of M, Re, α) [#875](https://github.com/RocketPy-Team/RocketPy/pull/875)
3636
- ENH: Add save functionality to `_MonteCarloPlots.all` method [#848](https://github.com/RocketPy-Team/RocketPy/pull/848)
37+
- ENH: add animations for motor propellant mass and tank fluid volumes [#894](https://github.com/RocketPy-Team/RocketPy/pull/894)
38+
- ENH: Rail button bending moments calculation in Flight class [#893](https://github.com/RocketPy-Team/RocketPy/pull/893)
39+
- ENH: Implement Bootstrapping for Confidence Interval Estimation [#891](https://github.com/RocketPy-Team/RocketPy/pull/897)
40+
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
3741
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
42+
- ENH: Add axial_acceleration attribute to the Flight class [#876](https://github.com/RocketPy-Team/RocketPy/pull/876)
43+
- ENH: custom warning no motor or aerosurface [#871](https://github.com/RocketPy-Team/RocketPy/pull/871)
44+
- ENH: Add thrustcurve api integration to retrieve motor eng data [#870](https://github.com/RocketPy-Team/RocketPy/pull/870)
3845
- ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825)
3946
- ENH: Enable only radial burning [#815](https://github.com/RocketPy-Team/RocketPy/pull/815)
40-
- ENH: Add thrustcurve api integration to retrieve motor eng data [#870](https://github.com/RocketPy-Team/RocketPy/pull/870)
41-
- ENH: custom warning no motor or aerosurface [#871](https://github.com/RocketPy-Team/RocketPy/pull/871)
4247

4348
### Changed
4449

4550
-
4651

4752
### Fixed
4853

49-
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
5054
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)
55+
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
5156

5257

5358
## [v1.11.0] - 2025-11-01

docs/user/flight.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,39 @@ The Flight object provides access to all forces and accelerations acting on the
266266
M2 = flight.M2 # Pitch moment
267267
M3 = flight.M3 # Yaw moment
268268

269+
Rail Button Forces and Bending Moments
270+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271+
272+
During the rail launch phase, RocketPy calculates reaction forces and internal bending moments at the rail button attachment points:
273+
274+
**Rail Button Forces (N):**
275+
276+
- ``rail_button1_normal_force`` : Normal reaction force at upper rail button
277+
- ``rail_button1_shear_force`` : Shear (tangential) reaction force at upper rail button
278+
- ``rail_button2_normal_force`` : Normal reaction force at lower rail button
279+
- ``rail_button2_shear_force`` : Shear (tangential) reaction force at lower rail button
280+
281+
**Rail Button Bending Moments (N⋅m):**
282+
283+
- ``rail_button1_bending_moment`` : Time-dependent bending moment at upper rail button attachment
284+
- ``max_rail_button1_bending_moment`` : Maximum absolute bending moment at upper rail button
285+
- ``rail_button2_bending_moment`` : Time-dependent bending moment at lower rail button attachment
286+
- ``max_rail_button2_bending_moment`` : Maximum absolute bending moment at lower rail button
287+
288+
**Calculation Method:**
289+
290+
Bending moments are calculated using beam theory assuming simple supports (rail buttons provide reaction forces but no moment reaction at rail contact). The total moment combines:
291+
292+
1. Shear force × button height (cantilever moment from button standoff)
293+
2. Normal force × distance to center of dry mass (lever arm effect)
294+
295+
Moments are zero after rail departure and represent internal structural loads for airframe and fastener stress analysis. Requires ``button_height`` to be defined when adding rail buttons via ``rocket.set_rail_buttons()``.
296+
297+
.. note::
298+
See Issue #893 for implementation details and validation approach.
299+
300+
301+
269302
Attitude and Orientation
270303
~~~~~~~~~~~~~~~~~~~~~~~~
271304

docs/user/flight_comparator.rst

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
Flight Comparator
2+
=================
3+
4+
This example demonstrates how to use the RocketPy ``FlightComparator`` class to
5+
compare a Flight simulation against external data sources.
6+
7+
Users must explicitly create a `FlightComparator` instance.
8+
9+
10+
This class is designed to compare a RocketPy Flight simulation against external
11+
data sources, such as:
12+
13+
- Real flight data (avionics logs, altimeter CSVs)
14+
- Simulations from other software (OpenRocket, RASAero)
15+
- Theoretical models or manual calculations
16+
17+
Unlike ``CompareFlights`` (which compares multiple RocketPy simulations),
18+
``FlightComparator`` specifically handles the challenge of aligning different
19+
time steps and calculating error metrics (RMSE, MAE, etc.) between a
20+
"Reference" simulation and "External" data.
21+
22+
Importing classes
23+
-----------------
24+
25+
We will start by importing the necessary classes and modules:
26+
27+
.. jupyter-execute::
28+
29+
import numpy as np
30+
31+
from rocketpy import Environment, Rocket, SolidMotor, Flight
32+
from rocketpy.simulation import FlightComparator, FlightDataImporter
33+
34+
Create Simulation (Reference)
35+
-----------------------------
36+
37+
First, let's create the standard RocketPy simulation that will serve as our
38+
"Ground Truth" or reference model. This follows the standard setup.
39+
40+
.. jupyter-execute::
41+
42+
# 1. Setup Environment
43+
env = Environment(
44+
date=(2022, 10, 1, 12),
45+
latitude=32.990254,
46+
longitude=-106.974998,
47+
elevation=1400,
48+
)
49+
env.set_atmospheric_model(type="standard_atmosphere")
50+
51+
# 2. Setup Motor
52+
Pro75M1670 = SolidMotor(
53+
thrust_source="../data/motors/cesaroni/Cesaroni_M1670.eng",
54+
burn_time=3.9,
55+
grain_number=5,
56+
grain_density=1815,
57+
grain_outer_radius=33 / 1000,
58+
grain_initial_inner_radius=15 / 1000,
59+
grain_initial_height=120 / 1000,
60+
grain_separation=5 / 1000,
61+
nozzle_radius=33 / 1000,
62+
throat_radius=11 / 1000,
63+
interpolation_method="linear",
64+
coordinate_system_orientation="nozzle_to_combustion_chamber",
65+
dry_mass=1.815,
66+
dry_inertia=(0.125, 0.125, 0.002),
67+
grains_center_of_mass_position=0.33,
68+
center_of_dry_mass_position=0.317,
69+
nozzle_position=0,
70+
)
71+
72+
# 3. Setup Rocket
73+
calisto = Rocket(
74+
radius=127 / 2000,
75+
mass=19.197 - 2.956,
76+
inertia=(6.321, 6.321, 0.034),
77+
power_off_drag="../data/calisto/powerOffDragCurve.csv",
78+
power_on_drag="../data/calisto/powerOnDragCurve.csv",
79+
center_of_mass_without_motor=0,
80+
coordinate_system_orientation="tail_to_nose",
81+
)
82+
83+
calisto.set_rail_buttons(0.0818, -0.618, 45)
84+
calisto.add_motor(Pro75M1670, position=-1.255)
85+
86+
# Add aerodynamic surfaces
87+
nosecone = calisto.add_nose(length=0.55829, kind="vonKarman", position=0.71971)
88+
fin_set = calisto.add_trapezoidal_fins(
89+
n=4,
90+
root_chord=0.120,
91+
tip_chord=0.040,
92+
span=0.100,
93+
position=-1.04956,
94+
cant_angle=0.5,
95+
airfoil=("../data/calisto/fins/NACA0012-radians.txt", "radians"),
96+
)
97+
tail = calisto.add_tail(
98+
top_radius=0.0635,
99+
bottom_radius=0.0435,
100+
length=0.060,
101+
position=-1.194656,
102+
)
103+
104+
# 4. Simulate
105+
flight = Flight(
106+
rocket=calisto,
107+
environment=env,
108+
rail_length=5.2,
109+
inclination=85,
110+
heading=0,
111+
)
112+
113+
# 5. Create FlightComparator instance
114+
comparator = FlightComparator(flight)
115+
116+
Adding Another Flight Object
117+
----------------------------
118+
119+
You can compare against another RocketPy Flight simulation directly:
120+
121+
.. jupyter-execute::
122+
123+
# Create a second simulation with slightly different parameters
124+
flight2 = Flight(
125+
rocket=calisto,
126+
environment=env,
127+
rail_length=5.0, # Slightly shorter rail
128+
inclination=85,
129+
heading=0,
130+
)
131+
132+
# Add the second flight directly
133+
comparator.add_data("Alternative Sim", flight2)
134+
135+
print(f"Added variables from flight2: {list(comparator.data_sources['Alternative Sim'].keys())}")
136+
137+
Importing External Data (dict)
138+
------------------------------
139+
140+
The primary data format expected by ``FlightComparator.add_data`` is a dictionary
141+
where keys are variable names (e.g. ``"z"``, ``"vz"``, ``"altitude"``) and values
142+
are either:
143+
144+
- A RocketPy ``Function`` object, or
145+
- A tuple of ``(time_array, data_array)``.
146+
147+
Let's create some synthetic external data to compare against our reference
148+
simulation:
149+
150+
.. jupyter-execute::
151+
152+
# Generate synthetic external data with realistic noise
153+
time_external = np.linspace(0, flight.t_final, 80) # Different time steps
154+
external_altitude = flight.z(time_external) + np.random.normal(0, 3, 80) # 3m noise
155+
external_velocity = flight.vz(time_external) + np.random.normal(0, 0.5, 80) # 0.5 m/s noise
156+
157+
# Add the external data to our comparator
158+
comparator.add_data(
159+
"External Simulator",
160+
{
161+
"altitude": (time_external, external_altitude),
162+
"vz": (time_external, external_velocity),
163+
}
164+
)
165+
166+
Running Comparisons
167+
-------------------
168+
169+
Now we can run the various comparison methods:
170+
171+
.. jupyter-execute::
172+
173+
# Generate summary with key events
174+
comparator.summary()
175+
176+
# Compare specific variable
177+
comparator.compare("altitude")
178+
179+
# Compare all available variables
180+
comparator.all()
181+
182+
# Plot 2D trajectory
183+
comparator.trajectories_2d(plane="xz")

docs/user/motors/liquidmotor.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,21 @@ For example:
160160

161161
example_liquid.exhaust_velocity.plot(0, 5)
162162

163+
The tanks added to a ``LiquidMotor`` can now be animated to visualize
164+
how the liquid and gas volumes evolve during the burn.
165+
166+
To animate the tanks, we can use the ``animate_fluid_volume()`` method:
167+
168+
.. jupyter-execute::
169+
170+
example_liquid.animate_fluid_volume(fps=30)
171+
172+
Optionally, the animation can be saved to a GIF file:
173+
174+
.. jupyter-execute::
175+
176+
example_liquid.animate_fluid_volume(fps=30, save_as="liquid_motor.gif")
177+
163178
Alternatively, you can plot all the information at once:
164179

165180
.. jupyter-execute::

docs/user/motors/tanks.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,21 @@ We can see some outputs with:
263263
# Evolution of the Propellant center of mass position
264264
N2O_mass_tank.center_of_mass.plot()
265265

266+
All tank types now include a built-in method for animating the evolution
267+
of liquid and gas volumes over time. This visualization aids in understanding the dynamic behavior
268+
of the tank's contents. To animate the tanks, we can use the
269+
``animate_fluid_volume()`` method:
270+
271+
.. jupyter-execute::
272+
273+
N2O_mass_tank.animate_fluid_volume(fps=30)
274+
275+
Optionally, the animation can be saved to a GIF file:
276+
277+
.. jupyter-execute::
278+
279+
N2O_mass_tank.animate_fluid_volume(fps=30, save_as="mass_based_tank.gif")
280+
266281

267282
Ullage Based Tank
268283
-----------------

docs/user/mrs.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,37 @@ Finally, we can compare the ellipses for the apogees and landing points using th
204204
Note we can pass along parameters used in the usual `ellipses` method of the
205205
`MonteCarlo` class, in this case the `ylim` argument to expand the y-axis limits.
206206

207+
208+
Calculating Confidence Intervals
209+
--------------------------------
210+
211+
Beyond visual comparisons, you may want to calculate statistical confidence intervals
212+
for specific attributes of the flight (e.g., apogee, max velocity) based on the
213+
resampled data. Since the resulting data is loaded into a ``MonteCarlo`` object,
214+
you can use its method to compute these intervals using the bootstrap method.
215+
216+
The following example shows how to calculate the 95% confidence interval for the
217+
mean of the apogee using the ``mrs_results`` object created earlier:
218+
219+
.. jupyter-execute::
220+
221+
# Calculate the 95% Confidence Interval for the mean apogee
222+
# We pass np.mean as the statistic to be evaluated
223+
apogee_ci = mrs_results.calculate_confidence_interval(
224+
attribute="apogee",
225+
statistic=np.mean,
226+
confidence_level=0.95,
227+
n_resamples=10000
228+
)
229+
230+
print(f"95% Confidence Interval for Apogee Mean: {apogee_ci}")
231+
232+
You can use any statistic function that accepts an array of data, such as ``np.median``
233+
or ``np.std``.
234+
235+
236+
237+
207238
Time Comparison
208239
---------------
209240

0 commit comments

Comments
 (0)