Skip to content

Commit 2b6fa42

Browse files
authored
Feat/flight comparison (#888)
* ENH: Create FlightComparator class * ENH: Integrate FlightComparator into Flight class * TST: Add comprehensive unit tests for FlightComparator * DOCS: Add FlightComparator user guide and update CHANGELOG * DOC: Fix doctest failure in FlightComparator docstring * MTN: Cleaned up some comments * ran ruff format * ENH: Addressed feedback * MNT: fix legend wording typos * keeping these changes here * Removed Flight.compare wrapper to fix circular imports, updated docs and tests accordingly * removed pylint silencer * addressed copilot feedback
1 parent 793e5f6 commit 2b6fa42

File tree

7 files changed

+1760
-21
lines changed

7 files changed

+1760
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3131
Attention: The newest changes should be on top -->
3232

3333
### Added
34-
35-
- ENH: Add save functionality to `_MonteCarloPlots.all` method [#848](https://github.com/RocketPy-Team/RocketPy/pull/848)
34+
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
3635
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
3736
- ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825)
3837
- ENH: Enable only radial burning [#815](https://github.com/RocketPy-Team/RocketPy/pull/815)

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")

rocketpy/plots/compare/compare_flights.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def positions(
139139
limit and second item, the y axis upper limit. If set to None, will
140140
be calculated automatically by matplotlib.
141141
legend : bool, optional
142-
Weather or not to show the legend, by default True
142+
Whether or not to show the legend, by default True
143143
filename : str, optional
144144
If a filename is provided, the plot will be saved to a file, by
145145
default None. Image options are: png, pdf, ps, eps and svg.
@@ -196,7 +196,7 @@ def velocities(
196196
limit and second item, the y axis upper limit. If set to None, will
197197
be calculated automatically by matplotlib.
198198
legend : bool, optional
199-
Weather or not to show the legend, by default True
199+
Whether or not to show the legend, by default True
200200
filename : str, optional
201201
If a filename is provided, the plot will be saved to a file, by
202202
default None. Image options are: png, pdf, ps, eps and svg.
@@ -253,7 +253,7 @@ def stream_velocities(
253253
limit and second item, the y axis upper limit. If set to None, will
254254
be calculated automatically by matplotlib.
255255
legend : bool, optional
256-
Weather or not to show the legend, by default True
256+
Whether or not to show the legend, by default True
257257
filename : str, optional
258258
If a filename is provided, the plot will be saved to a file, by
259259
default None. Image options are: png, pdf, ps, eps and svg.
@@ -319,7 +319,7 @@ def accelerations(
319319
limit and second item, the y axis upper limit. If set to None, will
320320
be calculated automatically by matplotlib.
321321
legend : bool, optional
322-
Weather or not to show the legend, by default True
322+
Whether or not to show the legend, by default True
323323
filename : str, optional
324324
If a filename is provided, the plot will be saved to a file, by
325325
default None. Image options are: png, pdf, ps, eps and svg.
@@ -375,7 +375,7 @@ def euler_angles(
375375
limit and second item, the y axis upper limit. If set to None, will
376376
be calculated automatically by matplotlib.
377377
legend : bool, optional
378-
Weather or not to show the legend, by default True
378+
Whether or not to show the legend, by default True
379379
filename : str, optional
380380
If a filename is provided, the plot will be saved to a file, by
381381
default None. Image options are: png, pdf, ps, eps and svg.
@@ -435,7 +435,7 @@ def quaternions(
435435
limit and second item, the y axis upper limit. If set to None, will
436436
be calculated automatically by matplotlib.
437437
legend : bool, optional
438-
Weather or not to show the legend, by default True
438+
Whether or not to show the legend, by default True
439439
filename : str, optional
440440
If a filename is provided, the plot will be saved to a file, by
441441
default None. Image options are: png, pdf, ps, eps and svg.
@@ -491,7 +491,7 @@ def attitude_angles(
491491
limit and second item, the y axis upper limit. If set to None, will
492492
be calculated automatically by matplotlib.
493493
legend : bool, optional
494-
Weather or not to show the legend, by default True
494+
Whether or not to show the legend, by default True
495495
filename : str, optional
496496
If a filename is provided, the plot will be saved to a file, by
497497
default None. Image options are: png, pdf, ps, eps and svg.
@@ -546,7 +546,7 @@ def angular_velocities(
546546
limit and second item, the y axis upper limit. If set to None, will
547547
be calculated automatically by matplotlib.
548548
legend : bool, optional
549-
Weather or not to show the legend, by default True
549+
Whether or not to show the legend, by default True
550550
filename : str, optional
551551
If a filename is provided, the plot will be saved to a file, by
552552
default None. Image options are: png, pdf, ps, eps and svg.
@@ -601,7 +601,7 @@ def angular_accelerations(
601601
limit and second item, the y axis upper limit. If set to None, will
602602
be calculated automatically by matplotlib.
603603
legend : bool, optional
604-
Weather or not to show the legend, by default True
604+
Whether or not to show the legend, by default True
605605
filename : str, optional
606606
If a filename is provided, the plot will be saved to a file, by
607607
default None. Image options are: png, pdf, ps, eps and svg.
@@ -661,7 +661,7 @@ def aerodynamic_forces(
661661
limit and second item, the y axis upper limit. If set to None, will
662662
be calculated automatically by matplotlib.
663663
legend : bool, optional
664-
Weather or not to show the legend, by default True
664+
Whether or not to show the legend, by default True
665665
filename : str, optional
666666
If a filename is provided, the plot will be saved to a file, by
667667
default None. Image options are: png, pdf, ps, eps and svg.
@@ -720,7 +720,7 @@ def aerodynamic_moments(
720720
limit and second item, the y axis upper limit. If set to None, will
721721
be calculated automatically by matplotlib.
722722
legend : bool, optional
723-
Weather or not to show the legend, by default True
723+
Whether or not to show the legend, by default True
724724
filename : str, optional
725725
If a filename is provided, the plot will be saved to a file,
726726
by default None.
@@ -774,7 +774,7 @@ def energies(
774774
limit and second item, the y axis upper limit. If set to None, will
775775
be calculated automatically by matplotlib.
776776
legend : bool, optional
777-
Weather or not to show the legend, by default True
777+
Whether or not to show the legend, by default True
778778
filename : str, optional
779779
If a filename is provided, the plot will be saved to a file,
780780
by default None.
@@ -834,7 +834,7 @@ def powers(
834834
limit and second item, the y axis upper limit. If set to None, will
835835
be calculated automatically by matplotlib.
836836
legend : bool, optional
837-
Weather or not to show the legend, by default True
837+
Whether or not to show the legend, by default True
838838
filename : str, optional
839839
If a filename is provided, the plot will be saved to a file,
840840
by default None.
@@ -889,7 +889,7 @@ def rail_buttons_forces(
889889
limit and second item, the y axis upper limit. If set to None, will
890890
be calculated automatically by matplotlib.
891891
legend : bool, optional
892-
Weather or not to show the legend, by default True
892+
Whether or not to show the legend, by default True
893893
filename : str, optional
894894
If a filename is provided, the plot will be saved to a file,
895895
by default None.
@@ -955,7 +955,7 @@ def angles_of_attack(
955955
limit and second item, the y axis upper limit. If set to None, will
956956
be calculated automatically by matplotlib.
957957
legend : bool, optional
958-
Weather or not to show the legend, by default True
958+
Whether or not to show the legend, by default True
959959
filename : str, optional
960960
If a filename is provided, the plot will be saved to a file,
961961
by default None.
@@ -1011,7 +1011,7 @@ def fluid_mechanics(
10111011
limit and second item, the y axis upper limit. If set to None, will
10121012
be calculated automatically by matplotlib.
10131013
legend : bool, optional
1014-
Weather or not to show the legend, by default True
1014+
Whether or not to show the legend, by default True
10151015
filename : str, optional
10161016
If a filename is provided, the plot will be saved to a file,
10171017
by default None.
@@ -1074,7 +1074,7 @@ def stability_margin(
10741074
limit and second item, the y axis upper limit. If set to None, will
10751075
be calculated automatically by matplotlib.
10761076
legend : bool, optional
1077-
Weather or not to show the legend, by default True
1077+
Whether or not to show the legend, by default True
10781078
filename : str, optional
10791079
If a filename is provided, the plot will be saved to a file,
10801080
by default None.
@@ -1113,7 +1113,7 @@ def attitude_frequency(
11131113
limit and second item, the y axis upper limit. If set to None, will
11141114
be calculated automatically by matplotlib.
11151115
legend : bool, optional
1116-
Weather or not to show the legend, by default True
1116+
Whether or not to show the legend, by default True
11171117
filename : str, optional
11181118
If a filename is provided, the plot will be saved to a file,
11191119
by default None.

0 commit comments

Comments
 (0)