Skip to content

Commit 04416cd

Browse files
committed
Removed Flight.compare wrapper to fix circular imports, updated docs and tests accordingly
1 parent a210c6f commit 04416cd

File tree

4 files changed

+91
-104
lines changed

4 files changed

+91
-104
lines changed

docs/user/compare_flights.rst

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
Flight Comparator
22
=================
33

4-
This example demonstrates how to use the RocketPy ``FlightComparator`` class.
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+
59

610
This class is designed to compare a RocketPy Flight simulation against external
711
data sources, such as:
@@ -97,14 +101,38 @@ First, let's create the standard RocketPy simulation that will serve as our
97101
position=-1.194656,
98102
)
99103

100-
# 4. Simulate
101-
flight = Flight(
102-
rocket=calisto,
103-
environment=env,
104-
rail_length=5.2,
105-
inclination=85,
106-
heading=0,
107-
)
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())}")
108136

109137
Importing External Data (dict)
110138
------------------------------
@@ -116,4 +144,40 @@ are either:
116144
- A RocketPy ``Function`` object, or
117145
- A tuple of ``(time_array, data_array)``.
118146

119-
In this example, external data is generated synthetically, but in practice you
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/simulation/flight.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3958,36 +3958,3 @@ def __lt__(self, other):
39583958
otherwise.
39593959
"""
39603960
return self.t < other.t
3961-
3962-
def compare(self, data, variable="z", label="External Data"): # pylint: disable=import-outside-toplevel)
3963-
"""
3964-
Compares the simulated flight against provided external data.
3965-
3966-
This is a convenience wrapper for the FlightComparator class.
3967-
3968-
Parameters
3969-
----------
3970-
data : dict
3971-
Dictionary containing the external data.
3972-
Keys should be variables (e.g. 'z', 'vz') and values should be
3973-
(time_array, data_array) tuples or RocketPy Functions.
3974-
variable : str, optional
3975-
The variable to compare immediately. Default is "z" (Altitude).
3976-
label : str, optional
3977-
Legend label for the external data. Default is "External Data".
3978-
3979-
Returns
3980-
-------
3981-
FlightComparator
3982-
The comparator object, allowing for further comparisons.
3983-
"""
3984-
3985-
from rocketpy.simulation.flight_comparator import (
3986-
FlightComparator,
3987-
)
3988-
3989-
comparator = FlightComparator(self)
3990-
comparator.add_data(label=label, data_dict=data)
3991-
comparator.compare(variable)
3992-
3993-
return comparator

rocketpy/simulation/flight_comparator.py

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55

66
from rocketpy.mathutils import Function
7+
from rocketpy.simulation.flight import Flight
78
from rocketpy.simulation.flight_data_importer import FlightDataImporter
89

910
from ..plots.plot_helpers import show_or_save_fig
@@ -15,7 +16,9 @@ class FlightComparator:
1516
(such as flight logs, OpenRocket simulations, RASAero).
1617
1718
This class handles the time-interpolation required to compare datasets
18-
recorded at different frequencies.
19+
recorded at different frequencies, and computes error metrics (RMSE, MAE, etc.)
20+
between your RocketPy simulation and external or reference data.
21+
1922
2023
Parameters
2124
----------
@@ -35,15 +38,20 @@ class FlightComparator:
3538
3639
.. code-block:: python
3740
38-
# Assuming you have a Flight object named 'my_flight'
41+
from rocketpy.simulation import FlightComparator
42+
43+
# Suppose you have a Flight object named 'my_flight'
3944
comparator = FlightComparator(my_flight)
4045
41-
# Add external data
46+
# Add external data (e.g., from OpenRocket or logs)
4247
comparator.add_data('OpenRocket', {
4348
'altitude': (time_array, altitude_array),
4449
'vz': (time_array, velocity_array)
4550
})
4651
52+
# You can also add another RocketPy Flight directly:
53+
comparator.add_data('OtherSimulation', other_flight)
54+
4755
# Run comparisons
4856
comparator.compare('altitude')
4957
comparator.summary()
@@ -52,7 +60,7 @@ class FlightComparator:
5260

5361
DEFAULT_GRID_POINTS = 1000 # number of points for interpolation grids
5462

55-
def __init__(self, flight):
63+
def __init__(self, flight: Flight):
5664
"""
5765
Initialize the comparator with a reference RocketPy Flight.
5866
@@ -65,19 +73,19 @@ def __init__(self, flight):
6573
-------
6674
None
6775
"""
68-
# Minimal duck-typed validation to give clear errors early
76+
# Duck-typed validation gives clear errors for Flight-like objects, more flexible than an isinstance check
6977
required_attrs = ("t_final", "apogee", "apogee_time", "impact_velocity")
7078
missing = [attr for attr in required_attrs if not hasattr(flight, attr)]
7179
if missing:
7280
raise TypeError(
73-
"flight must be a rocketpy.Flight-like object with attributes "
81+
"flight must be a rocketpy.Flight or Flight-like object with attributes "
7482
f"{required_attrs}. Missing: {', '.join(missing)}"
7583
)
7684

7785
self.flight = flight
7886
self.data_sources = {} # The format is {'Source Name': {'variable': Function}}
7987

80-
def add_data(self, label, data_dict): # pylint: disable=too-many-statements,import-outside-toplevel,cyclic-import
88+
def add_data(self, label, data_dict): # pylint: disable=too-many-statements
8189
"""
8290
Add an external dataset to the comparator.
8391
@@ -102,9 +110,8 @@ def add_data(self, label, data_dict): # pylint: disable=too-many-statements,imp
102110
if present.
103111
"""
104112

105-
from rocketpy.simulation.flight import (
106-
Flight,
107-
)
113+
if isinstance(data_dict, dict) and not data_dict:
114+
raise ValueError("data_dict cannot be empty")
108115

109116
processed_data = {}
110117

@@ -197,45 +204,9 @@ def add_data(self, label, data_dict): # pylint: disable=too-many-statements,imp
197204
"or FlightDataImporter object."
198205
)
199206

200-
self.data_sources[label] = processed_data
201-
print(
202-
f"Added data source '{label}' with variables: {list(processed_data.keys())}"
203-
)
204-
# If this is not a dict (e.g., a Flight or FlightDataImporter), we're done.
205-
if not isinstance(data_dict, dict):
206-
return
207-
208-
# Check if label already exists
209207
if label in self.data_sources:
210208
warnings.warn(f"Data source '{label}' already exists. Overwriting.")
211209

212-
# Making sure that data_dict is not empty
213-
if not data_dict:
214-
raise ValueError("data_dict cannot be empty")
215-
216-
processed_data = {}
217-
218-
for key, value in data_dict.items():
219-
# If already a Function, store it
220-
if isinstance(value, Function):
221-
processed_data[key] = value
222-
223-
# If raw data, convert to a function
224-
elif isinstance(value, (tuple, list)) and len(value) == 2:
225-
time_arr, data_arr = value
226-
# Creating a Function for automatic interpolation
227-
processed_data[key] = Function(
228-
np.column_stack((time_arr, data_arr)),
229-
inputs="Time (s)",
230-
outputs=key,
231-
interpolation="linear",
232-
)
233-
else:
234-
warnings.warn(
235-
f"Skipping '{key}' in '{label}'. Format not recognized. "
236-
"Expected RocketPy Function or (time, data) tuple."
237-
)
238-
239210
self.data_sources[label] = processed_data
240211
print(
241212
f"Added data source '{label}' with variables: {list(processed_data.keys())}"

tests/integration/simulation/test_flight_comparator_workflow.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,3 @@ def test_full_workflow_with_importer(flight_calisto, tmp_path):
9090
assert isinstance(results, dict)
9191
assert "Apogee Altitude (m)" in results
9292
assert "Impact Velocity (m/s)" in results
93-
94-
95-
def test_flight_compare_helper(flight_calisto):
96-
"""Test Flight.compare() convenience wrapper."""
97-
time_data = np.linspace(0, flight_calisto.t_final, 100)
98-
external = {
99-
"z": (time_data, flight_calisto.z(time_data) + 5.0),
100-
"vz": (time_data, flight_calisto.vz(time_data)),
101-
}
102-
103-
comparator = flight_calisto.compare(external, variable="z", label="External")
104-
105-
assert isinstance(comparator, FlightComparator)
106-
assert "External" in comparator.data_sources
107-
assert "z" in comparator.data_sources["External"]

0 commit comments

Comments
 (0)