From a4ebfab86fa0a6837ae4ce2a168b507499dcd0d1 Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Tue, 26 Aug 2025 23:26:28 -0400 Subject: [PATCH 1/9] mnt: extract flight data exporters into FlightDataExporter with deprecation wrappers --- rocketpy/simulation/__init__.py | 1 + rocketpy/simulation/flight.py | 316 ++++---------------- rocketpy/simulation/flight_data_exporter.py | 298 ++++++++++++++++++ 3 files changed, 359 insertions(+), 256 deletions(-) create mode 100644 rocketpy/simulation/flight_data_exporter.py diff --git a/rocketpy/simulation/__init__.py b/rocketpy/simulation/__init__.py index 6b98fdcf4..382d50d0d 100644 --- a/rocketpy/simulation/__init__.py +++ b/rocketpy/simulation/__init__.py @@ -2,3 +2,4 @@ from .flight_data_importer import FlightDataImporter from .monte_carlo import MonteCarlo from .multivariate_rejection_sampler import MultivariateRejectionSampler +from .flight_data_exporter import FlightDataExporter diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index e6861a820..e04b3ed13 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -1,14 +1,14 @@ # pylint: disable=too-many-lines -import json import math import warnings from copy import deepcopy from functools import cached_property import numpy as np -import simplekml from scipy.integrate import BDF, DOP853, LSODA, RK23, RK45, OdeSolver, Radau +from rocketpy.simulation.flight_data_exporter import FlightDataExporter + from ..mathutils.function import Function, funcify_method from ..mathutils.vector_matrix import Matrix, Vector from ..plots.flight_plots import _FlightPlots @@ -22,6 +22,7 @@ quaternions_to_nutation, quaternions_to_precession, quaternions_to_spin, + deprecated, ) ODE_SOLVER_MAP = { @@ -1019,8 +1020,7 @@ def __simulate(self, verbose): i += 1 # Create flight phase for time after inflation callbacks = [ - lambda self, - parachute_cd_s=parachute.cd_s: setattr( + lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s ), lambda self, @@ -1437,7 +1437,9 @@ def udot_rail2(self, t, u, post_processing=False): # pragma: no cover # Hey! We will finish this function later, now we just can use u_dot return self.u_dot_generalized(t, u, post_processing=post_processing) - def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements + def u_dot( + self, t, u, post_processing=False + ): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when rocket is flying in 6 DOF motion during ascent out of rail and descent without parachute. @@ -1757,7 +1759,9 @@ def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals return u_dot - def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements + def u_dot_generalized( + self, t, u, post_processing=False + ): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when the rocket is flying in 6 DOF motion in space and significant mass variation effects exist. Typical flight phases include powered ascent after launch @@ -3264,191 +3268,53 @@ def calculate_stall_wind_velocity(self, stall_angle): # TODO: move to utilities + f" of attack exceeds {stall_angle:.3f}°: {w_v:.3f} m/s" ) - def export_pressures(self, file_name, time_step): # TODO: move out - """Exports the pressure experienced by the rocket during the flight to - an external file, the '.csv' format is recommended, as the columns will - be separated by commas. It can handle flights with or without - parachutes, although it is not possible to get a noisy pressure signal - if no parachute is added. - - If a parachute is added, the file will contain 3 columns: time in - seconds, clean pressure in Pascals and noisy pressure in Pascals. - For flights without parachutes, the third column will be discarded - - This function was created especially for the 'Projeto Jupiter' - Electronics Subsystems team and aims to help in configuring - micro-controllers. - - Parameters - ---------- - file_name : string - The final file name, - time_step : float - Time step desired for the final file - - Return - ------ - None + @deprecated( + reason="Moved to FlightDataExporter.export_pressures()", + version="v1.12.0", + alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_pressures", + ) + def export_pressures(self, file_name, time_step): """ - time_points = np.arange(0, self.t_final, time_step) - # pylint: disable=W1514, E1121 - with open(file_name, "w") as file: - if len(self.rocket.parachutes) == 0: - print("No parachutes in the rocket, saving static pressure.") - for t in time_points: - file.write(f"{t:f}, {self.pressure.get_value_opt(t):.5f}\n") - else: - for parachute in self.rocket.parachutes: - for t in time_points: - p_cl = parachute.clean_pressure_signal_function.get_value_opt(t) - p_ns = parachute.noisy_pressure_signal_function.get_value_opt(t) - file.write(f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n") - # We need to save only 1 parachute data - break + .. deprecated:: 1.11 + Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` + and call ``.export_pressures(...)``. + """ + return FlightDataExporter(self).export_pressures(file_name, time_step) + @deprecated( + reason="Moved to FlightDataExporter.export_data()", + version="v1.12.0", + alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data", + ) def export_data(self, file_name, *variables, time_step=None): - """Exports flight data to a comma separated value file (.csv). - - Data is exported in columns, with the first column representing time - steps. The first line of the file is a header line, specifying the - meaning of each column and its units. - - Parameters - ---------- - file_name : string - The file name or path of the exported file. Example: flight_data.csv - Do not use forbidden characters, such as / in Linux/Unix and - `<, >, :, ", /, \\, | ?, *` in Windows. - variables : strings, optional - Names of the data variables which shall be exported. Must be Flight - class attributes which are instances of the Function class. Usage - example: test_flight.export_data('test.csv', 'z', 'angle_of_attack', - 'mach_number'). - time_step : float, optional - Time step desired for the data. If None, all integration time steps - will be exported. Otherwise, linear interpolation is carried out to - calculate values at the desired time steps. Example: 0.001. """ - # TODO: we should move this method to outside of class. - - # Fast evaluation for the most basic scenario - if time_step is None and len(variables) == 0: - np.savetxt( - file_name, - self.solution, - fmt="%.6f", - delimiter=",", - header="" - "Time (s)," - "X (m)," - "Y (m)," - "Z (m)," - "E0," - "E1," - "E2," - "E3," - "W1 (rad/s)," - "W2 (rad/s)," - "W3 (rad/s)", - ) - return - - # Not so fast evaluation for general case - if variables is None: - variables = [ - "x", - "y", - "z", - "vx", - "vy", - "vz", - "e0", - "e1", - "e2", - "e3", - "w1", - "w2", - "w3", - ] - - if time_step is None: - time_points = self.time - else: - time_points = np.arange(self.t_initial, self.t_final, time_step) - - exported_matrix = [time_points] - exported_header = "Time (s)" - - # Loop through variables, get points and names (for the header) - for variable in variables: - if variable in self.__dict__: - variable_function = self.__dict__[variable] - # Deal with decorated Flight methods - else: - try: - obj = getattr(self.__class__, variable) - variable_function = obj.__get__(self, self.__class__) - except AttributeError as exc: - raise AttributeError( - f"Variable '{variable}' not found in Flight class" - ) from exc - variable_points = variable_function(time_points) - exported_matrix += [variable_points] - exported_header += f", {variable_function.__outputs__[0]}" - - exported_matrix = np.array(exported_matrix).T # Fix matrix orientation - - np.savetxt( - file_name, - exported_matrix, - fmt="%.6f", - delimiter=",", - header=exported_header, - encoding="utf-8", + .. deprecated:: 1.11 + Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` + and call ``.export_data(...)``. + """ + return FlightDataExporter(self).export_data( + file_name, *variables, time_step=time_step ) + @deprecated( + reason="Moved to FlightDataExporter.export_sensor_data()", + version="v1.12.0", + alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_sensor_data", + ) def export_sensor_data(self, file_name, sensor=None): - """Exports sensors data to a file. The file format can be either .csv or - .json. - - Parameters - ---------- - file_name : str - The file name or path of the exported file. Example: flight_data.csv - Do not use forbidden characters, such as / in Linux/Unix and - `<, >, :, ", /, \\, | ?, *` in Windows. - sensor : Sensor, string, optional - The sensor to export data from. Can be given as a Sensor object or - as a string with the sensor name. If None, all sensors data will be - exported. Default is None. """ - if sensor is None: - data_dict = {} - for used_sensor, measured_data in self.sensor_data.items(): - data_dict[used_sensor.name] = measured_data - else: - # export data of only that sensor - data_dict = {} - - if not isinstance(sensor, str): - data_dict[sensor.name] = self.sensor_data[sensor] - else: # sensor is a string - matching_sensors = [s for s in self.sensor_data if s.name == sensor] - - if len(matching_sensors) > 1: - data_dict[sensor] = [] - for s in matching_sensors: - data_dict[s.name].append(self.sensor_data[s]) - elif len(matching_sensors) == 1: - data_dict[sensor] = self.sensor_data[matching_sensors[0]] - else: - raise ValueError("Sensor not found in the Flight.sensor_data.") - - with open(file_name, "w") as file: - json.dump(data_dict, file) - print("Sensor data exported to: ", file_name) + .. deprecated:: 1.11 + Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` + and call ``.export_sensor_data(...)``. + """ + return FlightDataExporter(self).export_sensor_data(file_name, sensor=sensor) - def export_kml( # TODO: should be moved out of this class. + @deprecated( + reason="Moved to FlightDataExporter.export_kml()", + version="v1.12.0", + alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml", + ) + def export_kml( self, file_name="trajectory.kml", time_step=None, @@ -3456,78 +3322,18 @@ def export_kml( # TODO: should be moved out of this class. color="641400F0", altitude_mode="absolute", ): - """Exports flight data to a .kml file, which can be opened with Google - Earth to display the rocket's trajectory. - - Parameters - ---------- - file_name : string - The file name or path of the exported file. Example: flight_data.csv - time_step : float, optional - Time step desired for the data. If None, all integration time steps - will be exported. Otherwise, linear interpolation is carried out to - calculate values at the desired time steps. Example: 0.001. - extrude: bool, optional - To be used if you want to project the path over ground by using an - extruded polygon. In case False only the linestring containing the - flight path will be created. Default is True. - color : str, optional - Color of your trajectory path, need to be used in specific kml - format. Refer to http://www.zonums.com/gmaps/kml_color/ for more - info. - altitude_mode: str - Select elevation values format to be used on the kml file. Use - 'relativetoground' if you want use Above Ground Level elevation, or - 'absolute' if you want to parse elevation using Above Sea Level. - Default is 'relativetoground'. Only works properly if the ground - level is flat. Change to 'absolute' if the terrain is to irregular - or contains mountains. """ - # Define time points vector - if time_step is None: - time_points = self.time - else: - time_points = np.arange(self.t_initial, self.t_final + time_step, time_step) - # Open kml file with simplekml library - kml = simplekml.Kml(open=1) - trajectory = kml.newlinestring(name="Rocket Trajectory - Powered by RocketPy") - - if altitude_mode == "relativetoground": - # In this mode the elevation data will be the Above Ground Level - # elevation. Only works properly if the ground level is similar to - # a plane, i.e. it might not work well if the terrain has mountains - coords = [ - ( - self.longitude.get_value_opt(t), - self.latitude.get_value_opt(t), - self.altitude.get_value_opt(t), - ) - for t in time_points - ] - trajectory.coords = coords - trajectory.altitudemode = simplekml.AltitudeMode.relativetoground - else: # altitude_mode == 'absolute' - # In this case the elevation data will be the Above Sea Level elevation - # Ensure you use the correct value on self.env.elevation, otherwise - # the trajectory path can be offset from ground - coords = [ - ( - self.longitude.get_value_opt(t), - self.latitude.get_value_opt(t), - self.z.get_value_opt(t), - ) - for t in time_points - ] - trajectory.coords = coords - trajectory.altitudemode = simplekml.AltitudeMode.absolute - # Modify style of trajectory linestring - trajectory.style.linestyle.color = color - trajectory.style.polystyle.color = color - if extrude: - trajectory.extrude = 1 - # Save the KML - kml.save(file_name) - print("File ", file_name, " saved with success!") + .. deprecated:: 1.11 + Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` + and call ``.export_kml(...)``. + """ + return FlightDataExporter(self).export_kml( + file_name=file_name, + time_step=time_step, + extrude=extrude, + color=color, + altitude_mode=altitude_mode, + ) def info(self): """Prints out a summary of the data available about the Flight.""" @@ -3767,9 +3573,7 @@ def add(self, flight_phase, index=None): # TODO: quite complex method new_index = ( index - 1 if flight_phase.t < previous_phase.t - else index + 1 - if flight_phase.t > next_phase.t - else index + else index + 1 if flight_phase.t > next_phase.t else index ) flight_phase.t += adjust self.add(flight_phase, new_index) diff --git a/rocketpy/simulation/flight_data_exporter.py b/rocketpy/simulation/flight_data_exporter.py new file mode 100644 index 000000000..3c30292df --- /dev/null +++ b/rocketpy/simulation/flight_data_exporter.py @@ -0,0 +1,298 @@ +""" +Exports a rocketpy.Flight object's data to external files. +""" + +import json +import numpy as np +import simplekml + + +class FlightDataExporter: + """Export data from a rocketpy.Flight object to various formats.""" + + def __init__(self, flight, name="Flight Data"): + """ + Parameters + ---------- + flight : rocketpy.simulation.flight.Flight + The Flight instance to export from. + name : str, optional + A label for this exporter instance. + """ + self.name = name + self._flight = flight + + def __repr__(self): + return f"FlightDataExporter(name='{self.name}', flight='{type(self._flight).__name__}')" + + def export_pressures(self, file_name, time_step): + """Exports the pressure experienced by the rocket during the flight to + an external file, the '.csv' format is recommended, as the columns will + be separated by commas. It can handle flights with or without + parachutes, although it is not possible to get a noisy pressure signal + if no parachute is added. + + If a parachute is added, the file will contain 3 columns: time in + seconds, clean pressure in Pascals and noisy pressure in Pascals. + For flights without parachutes, the third column will be discarded + + This function was created especially for the 'Projeto Jupiter' + Electronics Subsystems team and aims to help in configuring + micro-controllers. + + Parameters + ---------- + file_name : string + The final file name, + time_step : float + Time step desired for the final file + + Return + ------ + None + """ + f = self._flight + time_points = np.arange(0, f.t_final, time_step) + # pylint: disable=W1514, E1121 + with open(file_name, "w") as file: + if len(f.rocket.parachutes) == 0: + print("No parachutes in the rocket, saving static pressure.") + for t in time_points: + file.write(f"{t:f}, {f.pressure.get_value_opt(t):.5f}\n") + else: + for parachute in f.rocket.parachutes: + for t in time_points: + p_cl = parachute.clean_pressure_signal_function.get_value_opt(t) + p_ns = parachute.noisy_pressure_signal_function.get_value_opt(t) + file.write(f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n") + # We need to save only 1 parachute data + break + + def export_data(self, file_name, *variables, time_step=None): + """Exports flight data to a comma separated value file (.csv). + + Data is exported in columns, with the first column representing time + steps. The first line of the file is a header line, specifying the + meaning of each column and its units. + + Parameters + ---------- + file_name : string + The file name or path of the exported file. Example: flight_data.csv + Do not use forbidden characters, such as / in Linux/Unix and + `<, >, :, ", /, \\, | ?, *` in Windows. + variables : strings, optional + Names of the data variables which shall be exported. Must be Flight + class attributes which are instances of the Function class. Usage + example: test_flight.export_data('test.csv', 'z', 'angle_of_attack', + 'mach_number'). + time_step : float, optional + Time step desired for the data. If None, all integration time steps + will be exported. Otherwise, linear interpolation is carried out to + calculate values at the desired time steps. Example: 0.001. + """ + f = self._flight + + # Fast evaluation for the most basic scenario + if time_step is None and len(variables) == 0: + np.savetxt( + file_name, + f.solution, + fmt="%.6f", + delimiter=",", + header="" + "Time (s)," + "X (m)," + "Y (m)," + "Z (m)," + "E0," + "E1," + "E2," + "E3," + "W1 (rad/s)," + "W2 (rad/s)," + "W3 (rad/s)", + ) + return + + # Not so fast evaluation for general case + if variables is None: + variables = [ + "x", + "y", + "z", + "vx", + "vy", + "vz", + "e0", + "e1", + "e2", + "e3", + "w1", + "w2", + "w3", + ] + + if time_step is None: + time_points = f.time + else: + time_points = np.arange(f.t_initial, f.t_final, time_step) + + exported_matrix = [time_points] + exported_header = "Time (s)" + + # Loop through variables, get points and names (for the header) + for variable in variables: + if variable in f.__dict__: + variable_function = f.__dict__[variable] + # Deal with decorated Flight methods + else: + try: + obj = getattr(f.__class__, variable) + variable_function = obj.__get__(f, f.__class__) + except AttributeError as exc: + raise AttributeError( + f"Variable '{variable}' not found in Flight class" + ) from exc + variable_points = variable_function(time_points) + exported_matrix += [variable_points] + exported_header += f", {variable_function.__outputs__[0]}" + + exported_matrix = np.array(exported_matrix).T # Fix matrix orientation + + np.savetxt( + file_name, + exported_matrix, + fmt="%.6f", + delimiter=",", + header=exported_header, + encoding="utf-8", + ) + + def export_sensor_data(self, file_name, sensor=None): + """Exports sensors data to a file. The file format can be either .csv or + .json. + + Parameters + ---------- + file_name : str + The file name or path of the exported file. Example: flight_data.csv + Do not use forbidden characters, such as / in Linux/Unix and + `<, >, :, ", /, \\, | ?, *` in Windows. + sensor : Sensor, string, optional + The sensor to export data from. Can be given as a Sensor object or + as a string with the sensor name. If None, all sensors data will be + exported. Default is None. + """ + f = self._flight + + if sensor is None: + data_dict = {} + for used_sensor, measured_data in f.sensor_data.items(): + data_dict[used_sensor.name] = measured_data + else: + # export data of only that sensor + data_dict = {} + + if not isinstance(sensor, str): + data_dict[sensor.name] = f.sensor_data[sensor] + else: # sensor is a string + matching_sensors = [s for s in f.sensor_data if s.name == sensor] + + if len(matching_sensors) > 1: + data_dict[sensor] = [] + for s in matching_sensors: + data_dict[s.name].append(f.sensor_data[s]) + elif len(matching_sensors) == 1: + data_dict[sensor] = f.sensor_data[matching_sensors[0]] + else: + raise ValueError("Sensor not found in the Flight.sensor_data.") + + with open(file_name, "w") as file: + json.dump(data_dict, file) + print("Sensor data exported to: ", file_name) + + def export_kml( + self, + file_name="trajectory.kml", + time_step=None, + extrude=True, + color="641400F0", + altitude_mode="absolute", + ): + """Exports flight data to a .kml file, which can be opened with Google + Earth to display the rocket's trajectory. + + Parameters + ---------- + file_name : string + The file name or path of the exported file. Example: flight_data.csv + time_step : float, optional + Time step desired for the data. If None, all integration time steps + will be exported. Otherwise, linear interpolation is carried out to + calculate values at the desired time steps. Example: 0.001. + extrude: bool, optional + To be used if you want to project the path over ground by using an + extruded polygon. In case False only the linestring containing the + flight path will be created. Default is True. + color : str, optional + Color of your trajectory path, need to be used in specific kml + format. Refer to http://www.zonums.com/gmaps/kml_color/ for more + info. + altitude_mode: str + Select elevation values format to be used on the kml file. Use + 'relativetoground' if you want use Above Ground Level elevation, or + 'absolute' if you want to parse elevation using Above Sea Level. + Default is 'relativetoground'. Only works properly if the ground + level is flat. Change to 'absolute' if the terrain is to irregular + or contains mountains. + """ + f = self._flight + + # Define time points vector + if time_step is None: + time_points = f.time + else: + time_points = np.arange(f.t_initial, f.t_final + time_step, time_step) + + kml = simplekml.Kml(open=1) + trajectory = kml.newlinestring(name="Rocket Trajectory - Powered by RocketPy") + + if altitude_mode == "relativetoground": + # In this mode the elevation data will be the Above Ground Level + # elevation. Only works properly if the ground level is similar to + # a plane, i.e. it might not work well if the terrain has mountains + coords = [ + ( + f.longitude.get_value_opt(t), + f.latitude.get_value_opt(t), + f.altitude.get_value_opt(t), + ) + for t in time_points + ] + trajectory.coords = coords + trajectory.altitudemode = simplekml.AltitudeMode.relativetoground + else: # altitude_mode == 'absolute' + # In this case the elevation data will be the Above Sea Level elevation + # Ensure you use the correct value on self.env.elevation, otherwise + # the trajectory path can be offset from ground + coords = [ + ( + f.longitude.get_value_opt(t), + f.latitude.get_value_opt(t), + f.z.get_value_opt(t), + ) + for t in time_points + ] + trajectory.coords = coords + trajectory.altitudemode = simplekml.AltitudeMode.absolute + + # Modify style of trajectory linestring + trajectory.style.linestyle.color = color + trajectory.style.polystyle.color = color + if extrude: + trajectory.extrude = 1 + + # Save the KML + kml.save(file_name) + print("File ", file_name, " saved with success!") From 670fd1739b8ff6c2b1dd3b6fda05e19f69c33664 Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Wed, 27 Aug 2025 00:40:16 -0400 Subject: [PATCH 2/9] tst: add unit tests for exporter delegation and direct exports --- tests/unit/test_flight_data_exporter.py | 43 ++++++++++++++++++++ tests/unit/test_flight_export_deprecation.py | 37 +++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 tests/unit/test_flight_data_exporter.py create mode 100644 tests/unit/test_flight_export_deprecation.py diff --git a/tests/unit/test_flight_data_exporter.py b/tests/unit/test_flight_data_exporter.py new file mode 100644 index 000000000..c89e92918 --- /dev/null +++ b/tests/unit/test_flight_data_exporter.py @@ -0,0 +1,43 @@ +import json +from types import SimpleNamespace +from rocketpy.simulation import FlightDataExporter + + +def test_export_data_writes_csv_header(flight_calisto, tmp_path): + """Expect: direct exporter writes a CSV with a header containing 'Time (s)'.""" + out = tmp_path / "out.csv" + FlightDataExporter(flight_calisto).export_data(str(out)) + text = out.read_text(encoding="utf-8") + assert "Time (s)" in text + + +def test_export_pressures_writes_rows(flight_calisto_robust, tmp_path): + """Expect: direct exporter writes a pressure file with time-first CSV rows.""" + out = tmp_path / "p.csv" + FlightDataExporter(flight_calisto_robust).export_pressures(str(out), time_step=0.2) + lines = out.read_text(encoding="utf-8").strip().splitlines() + assert len(lines) > 5 + # basic CSV shape “t, value” + parts = lines[0].split(",") + assert len(parts) in (2, 3) + + +def test_export_sensor_data_writes_json_when_sensor_data_present( + flight_calisto, tmp_path, monkeypatch +): + """Expect: exporter writes JSON mapping sensor.name -> data when sensor_data is present.""" + + class DummySensor: + def __init__(self, name): + self.name = name + + s1 = DummySensor("DummySensor") + monkeypatch.setattr( + flight_calisto, "sensor_data", {s1: [1.0, 2.0, 3.0]}, raising=False + ) + out = tmp_path / "sensors.json" + + FlightDataExporter(flight_calisto).export_sensor_data(str(out)) + + data = json.loads(out.read_text(encoding="utf-8")) + assert data["DummySensor"] == [1.0, 2.0, 3.0] diff --git a/tests/unit/test_flight_export_deprecation.py b/tests/unit/test_flight_export_deprecation.py new file mode 100644 index 000000000..5c0fa7ee5 --- /dev/null +++ b/tests/unit/test_flight_export_deprecation.py @@ -0,0 +1,37 @@ +import pytest +from unittest.mock import patch + + +def test_export_data_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path): + """Expect: calling Flight.export_data emits DeprecationWarning and delegates to FlightDataExporter.export_data.""" + out = tmp_path / "out.csv" + with patch( + "rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data" + ) as spy: + with pytest.warns(DeprecationWarning): + flight_calisto.export_data(str(out)) + spy.assert_called_once() + + +def test_export_pressures_deprecated_emits_warning_and_delegates( + flight_calisto_robust, tmp_path +): + """Expect: calling Flight.export_pressures emits DeprecationWarning and delegates to FlightDataExporter.export_pressures.""" + out = tmp_path / "p.csv" + with patch( + "rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_pressures" + ) as spy: + with pytest.warns(DeprecationWarning): + flight_calisto_robust.export_pressures(str(out), time_step=0.1) + spy.assert_called_once() + + +def test_export_kml_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path): + """Expect: calling Flight.export_kml emits DeprecationWarning and delegates to FlightDataExporter.export_kml.""" + out = tmp_path / "traj.kml" + with patch( + "rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml" + ) as spy: + with pytest.warns(DeprecationWarning): + flight_calisto.export_kml(str(out), time_step=0.5) + spy.assert_called_once() From 193383cec1f27c88a29df13b9814441c30dca6c1 Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Wed, 27 Aug 2025 00:51:44 -0400 Subject: [PATCH 3/9] tst: minor tweaks to clean up imports on added tests --- tests/unit/test_flight_data_exporter.py | 1 - tests/unit/test_flight_export_deprecation.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_flight_data_exporter.py b/tests/unit/test_flight_data_exporter.py index c89e92918..f93bb2248 100644 --- a/tests/unit/test_flight_data_exporter.py +++ b/tests/unit/test_flight_data_exporter.py @@ -1,5 +1,4 @@ import json -from types import SimpleNamespace from rocketpy.simulation import FlightDataExporter diff --git a/tests/unit/test_flight_export_deprecation.py b/tests/unit/test_flight_export_deprecation.py index 5c0fa7ee5..a2fd9fa74 100644 --- a/tests/unit/test_flight_export_deprecation.py +++ b/tests/unit/test_flight_export_deprecation.py @@ -1,6 +1,5 @@ -import pytest from unittest.mock import patch - +import pytest def test_export_data_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path): """Expect: calling Flight.export_data emits DeprecationWarning and delegates to FlightDataExporter.export_data.""" From 345f8acea03e2b0d96e4627a1b4a0e4f745b81fc Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Wed, 27 Aug 2025 01:20:37 -0400 Subject: [PATCH 4/9] doc: switch export examples to FlightDataExporter and note deprecations (KML, CSV) --- docs/user/first_simulation.rst | 51 +++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/user/first_simulation.rst b/docs/user/first_simulation.rst index d0e679e6e..9e3359992 100644 --- a/docs/user/first_simulation.rst +++ b/docs/user/first_simulation.rst @@ -565,12 +565,16 @@ Visualizing the Trajectory in Google Earth We can export the trajectory to ``.kml`` to visualize it in Google Earth: +Use the dedicated exporter class: + .. jupyter-input:: - test_flight.export_kml( + from rocketpy.simulation import FlightDataExporter + + FlightDataExporter(test_flight).export_kml( file_name="trajectory.kml", extrude=True, - altitude_mode="relative_to_ground", + altitude_mode="relativetoground", ) .. note:: @@ -578,6 +582,10 @@ We can export the trajectory to ``.kml`` to visualize it in Google Earth: To learn more about the ``.kml`` format, see `KML Reference `_. +.. note:: + + The legacy method ``Flight.export_kml`` is deprecated. Use + :meth:`rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml`. Manipulating results -------------------- @@ -610,17 +618,27 @@ In this section, we will explore how to export specific data from your RocketPy simulations to CSV files. This is particularly useful if you want to insert the data into spreadsheets or other software for further analysis. -The main method that is used to export data is the :meth:`rocketpy.Flight.export_data` method. This method exports selected flight attributes to a CSV file. In this first example, we will export the rocket angle of attack (see :meth:`rocketpy.Flight.angle_of_attack`) and the rocket mach number (see :meth:`rocketpy.Flight.mach_number`) to the file ``calisto_flight_data.csv``. +The recommended API is +:meth:`rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data`, +which exports selected flight attributes to a CSV file. In this first example, +we export the rocket angle of attack (see :meth:`rocketpy.Flight.angle_of_attack`) +and the rocket Mach number (see :meth:`rocketpy.Flight.mach_number`) to the file +``calisto_flight_data.csv``. .. jupyter-execute:: - test_flight.export_data( + from rocketpy.simulation import FlightDataExporter + + exporter = FlightDataExporter(test_flight) + exporter.export_data( "calisto_flight_data.csv", "angle_of_attack", "mach_number", ) -| As you can see, the first argument of the method is the name of the file to be created. The following arguments are the attributes to be exported. We can check the file that was created by reading it with the :func:`pandas.read_csv` function: +| As you can see, the first argument is the file name to be created. The following +arguments are the attributes to be exported. We can check the file by reading it +with :func:`pandas.read_csv`: .. jupyter-execute:: @@ -628,11 +646,13 @@ The main method that is used to export data is the :meth:`rocketpy.Flight.export pd.read_csv("calisto_flight_data.csv") -| The file header specifies the meaning of each column. The time samples are obtained from the simulation solver steps. Should you want to export the data at a different sampling rate, you can use the ``time_step`` argument of the :meth:`rocketpy.Flight.export_data` method as follows. +| The file header specifies the meaning of each column. The time samples are +obtained from the simulation solver steps. To export the data at a different +sampling rate, use the ``time_step`` argument: .. jupyter-execute:: - test_flight.export_data( + exporter.export_data( "calisto_flight_data.csv", "angle_of_attack", "mach_number", @@ -641,24 +661,29 @@ The main method that is used to export data is the :meth:`rocketpy.Flight.export pd.read_csv("calisto_flight_data.csv") -This will export the same data at a sampling rate of 1 second. The flight data will be interpolated to match the new sampling rate. +This exports the same data at a sampling rate of 1 second. The flight data is +interpolated to match the new sampling rate. -Finally, the :meth:`rocketpy.Flight.export_data` method also provides a convenient way to export the entire flight solution (see :meth:`rocketpy.Flight.solution_array`) to a CSV file. This is done by not passing any attributes names to the method. +Finally, ``FlightDataExporter.export_data`` also provides a convenient way to +export the entire flight solution (see :meth:`rocketpy.Flight.solution_array`) +by not passing any attribute names: .. jupyter-execute:: - test_flight.export_data( - "calisto_flight_data.csv", - ) + exporter.export_data("calisto_flight_data.csv") .. jupyter-execute:: :hide-code: :hide-output: # Sample file cleanup - import os + import ospython.exe -m pip install --upgrade pip os.remove("calisto_flight_data.csv") +.. note:: + + The legacy method ``Flight.export_data`` is deprecated. Use + :meth:`rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data`. Saving and Storing Plots ------------------------ From a6d0a3fdeeb098cb1e5687f57a72652578644b78 Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Wed, 27 Aug 2025 01:31:53 -0400 Subject: [PATCH 5/9] doc(changelog): record FlightDataExporter extraction, deprecations, and doc updates --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074e28af0..1feb58e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Attention: The newest changes should be on top --> ### Added - ENH: Tank Fluids with Variable Density from Temperature and Pressure [#852](https://github.com/RocketPy-Team/RocketPy/pull/852) - ENH: Controller (AirBrakes) and Sensors Encoding [#849](https://github.com/RocketPy-Team/RocketPy/pull/849) -- EHN: Addition of ensemble variable to ECMWF dictionaries [#842](https://github.com/RocketPy-Team/RocketPy/pull/842) +- EHN: Addition of ensemble variable to ECMWF dictionaries [#842](https://github.com/RocketPy-Team/RocketPy/pull/842) - ENH: Added Crop and Clip Methods to Function Class [#817](https://github.com/RocketPy-Team/RocketPy/pull/817) - DOC: Add Flight class usage documentation and update index [#841](https://github.com/RocketPy-Team/RocketPy/pull/841) - ENH: Discretized and No-Pickle Encoding Options [#827](https://github.com/RocketPy-Team/RocketPy/pull/827) From e9697785027bff820d7758a9f529e9c7a5303a88 Mon Sep 17 00:00:00 2001 From: Cameron K Brooks Date: Mon, 1 Sep 2025 14:16:11 -0400 Subject: [PATCH 6/9] format flight.py with ruff to comply with project style --- rocketpy/simulation/flight.py | 15 +++++++-------- tests/unit/test_flight_export_deprecation.py | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index e04b3ed13..d1330e6c2 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -1020,7 +1020,8 @@ def __simulate(self, verbose): i += 1 # Create flight phase for time after inflation callbacks = [ - lambda self, parachute_cd_s=parachute.cd_s: setattr( + lambda self, + parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s ), lambda self, @@ -1437,9 +1438,7 @@ def udot_rail2(self, t, u, post_processing=False): # pragma: no cover # Hey! We will finish this function later, now we just can use u_dot return self.u_dot_generalized(t, u, post_processing=post_processing) - def u_dot( - self, t, u, post_processing=False - ): # pylint: disable=too-many-locals,too-many-statements + def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when rocket is flying in 6 DOF motion during ascent out of rail and descent without parachute. @@ -1759,9 +1758,7 @@ def u_dot( return u_dot - def u_dot_generalized( - self, t, u, post_processing=False - ): # pylint: disable=too-many-locals,too-many-statements + def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when the rocket is flying in 6 DOF motion in space and significant mass variation effects exist. Typical flight phases include powered ascent after launch @@ -3573,7 +3570,9 @@ def add(self, flight_phase, index=None): # TODO: quite complex method new_index = ( index - 1 if flight_phase.t < previous_phase.t - else index + 1 if flight_phase.t > next_phase.t else index + else index + 1 + if flight_phase.t > next_phase.t + else index ) flight_phase.t += adjust self.add(flight_phase, new_index) diff --git a/tests/unit/test_flight_export_deprecation.py b/tests/unit/test_flight_export_deprecation.py index a2fd9fa74..646c249dd 100644 --- a/tests/unit/test_flight_export_deprecation.py +++ b/tests/unit/test_flight_export_deprecation.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest + def test_export_data_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path): """Expect: calling Flight.export_data emits DeprecationWarning and delegates to FlightDataExporter.export_data.""" out = tmp_path / "out.csv" From 54a989dc3e6141e53d782ce5543595d811603f92 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 Oct 2025 20:48:10 -0300 Subject: [PATCH 7/9] TST: Improve tests for Flight Data Exporter --- rocketpy/simulation/__init__.py | 2 +- rocketpy/simulation/flight.py | 2 +- rocketpy/simulation/flight_data_exporter.py | 1 + tests/integration/test_flight.py | 125 ------------- tests/unit/test_flight_data_exporter.py | 178 +++++++++++++++++-- tests/unit/test_flight_export_deprecation.py | 3 + 6 files changed, 170 insertions(+), 141 deletions(-) diff --git a/rocketpy/simulation/__init__.py b/rocketpy/simulation/__init__.py index 382d50d0d..1ade0f16f 100644 --- a/rocketpy/simulation/__init__.py +++ b/rocketpy/simulation/__init__.py @@ -1,5 +1,5 @@ from .flight import Flight +from .flight_data_exporter import FlightDataExporter from .flight_data_importer import FlightDataImporter from .monte_carlo import MonteCarlo from .multivariate_rejection_sampler import MultivariateRejectionSampler -from .flight_data_exporter import FlightDataExporter diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index d1330e6c2..27043b892 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -15,6 +15,7 @@ from ..prints.flight_prints import _FlightPrints from ..tools import ( calculate_cubic_hermite_coefficients, + deprecated, euler313_to_quaternions, find_closest, find_root_linear_interpolation, @@ -22,7 +23,6 @@ quaternions_to_nutation, quaternions_to_precession, quaternions_to_spin, - deprecated, ) ODE_SOLVER_MAP = { diff --git a/rocketpy/simulation/flight_data_exporter.py b/rocketpy/simulation/flight_data_exporter.py index 3c30292df..31c798dd4 100644 --- a/rocketpy/simulation/flight_data_exporter.py +++ b/rocketpy/simulation/flight_data_exporter.py @@ -3,6 +3,7 @@ """ import json + import numpy as np import simplekml diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index 7a36e0629..1f8f4432f 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -69,131 +69,6 @@ def test_all_info_different_solvers( assert test_flight.all_info() is None -class TestExportData: - """Tests the export_data method of the Flight class.""" - - def test_basic_export(self, flight_calisto): - """Tests basic export functionality""" - file_name = "test_export_data_1.csv" - flight_calisto.export_data(file_name) - self.validate_basic_export(flight_calisto, file_name) - os.remove(file_name) - - def test_custom_export(self, flight_calisto): - """Tests custom export functionality""" - file_name = "test_export_data_2.csv" - flight_calisto.export_data( - file_name, - "z", - "vz", - "e1", - "w3", - "angle_of_attack", - time_step=0.1, - ) - self.validate_custom_export(flight_calisto, file_name) - os.remove(file_name) - - def validate_basic_export(self, flight_calisto, file_name): - """Validates the basic export file content""" - test_data = np.loadtxt(file_name, delimiter=",") - assert np.allclose(flight_calisto.x[:, 0], test_data[:, 0], atol=1e-5) - assert np.allclose(flight_calisto.x[:, 1], test_data[:, 1], atol=1e-5) - assert np.allclose(flight_calisto.y[:, 1], test_data[:, 2], atol=1e-5) - assert np.allclose(flight_calisto.z[:, 1], test_data[:, 3], atol=1e-5) - assert np.allclose(flight_calisto.vx[:, 1], test_data[:, 4], atol=1e-5) - assert np.allclose(flight_calisto.vy[:, 1], test_data[:, 5], atol=1e-5) - assert np.allclose(flight_calisto.vz[:, 1], test_data[:, 6], atol=1e-5) - assert np.allclose(flight_calisto.e0[:, 1], test_data[:, 7], atol=1e-5) - assert np.allclose(flight_calisto.e1[:, 1], test_data[:, 8], atol=1e-5) - assert np.allclose(flight_calisto.e2[:, 1], test_data[:, 9], atol=1e-5) - assert np.allclose(flight_calisto.e3[:, 1], test_data[:, 10], atol=1e-5) - assert np.allclose(flight_calisto.w1[:, 1], test_data[:, 11], atol=1e-5) - assert np.allclose(flight_calisto.w2[:, 1], test_data[:, 12], atol=1e-5) - assert np.allclose(flight_calisto.w3[:, 1], test_data[:, 13], atol=1e-5) - - def validate_custom_export(self, flight_calisto, file_name): - """Validates the custom export file content""" - test_data = np.loadtxt(file_name, delimiter=",") - time_points = np.arange(flight_calisto.t_initial, flight_calisto.t_final, 0.1) - assert np.allclose(time_points, test_data[:, 0], atol=1e-5) - assert np.allclose(flight_calisto.z(time_points), test_data[:, 1], atol=1e-5) - assert np.allclose(flight_calisto.vz(time_points), test_data[:, 2], atol=1e-5) - assert np.allclose(flight_calisto.e1(time_points), test_data[:, 3], atol=1e-5) - assert np.allclose(flight_calisto.w3(time_points), test_data[:, 4], atol=1e-5) - assert np.allclose( - flight_calisto.angle_of_attack(time_points), test_data[:, 5], atol=1e-5 - ) - - -def test_export_kml(flight_calisto_robust): - """Tests weather the method Flight.export_kml is working as intended. - - Parameters: - ----------- - flight_calisto_robust : rocketpy.Flight - Flight object to be tested. See the conftest.py file for more info - regarding this pytest fixture. - """ - - test_flight = flight_calisto_robust - - # Basic export - test_flight.export_kml( - "test_export_data_1.kml", time_step=None, extrude=True, altitude_mode="absolute" - ) - - # Load exported files and fixtures and compare them - with open("test_export_data_1.kml", "r") as test_1: - for row in test_1: - if row[:29] == " ": - r = row[29:-15] - r = r.split(",") - for i, j in enumerate(r): - r[i] = j.split(" ") - lon, lat, z, coords = [], [], [], [] - for i in r: - for j in i: - coords.append(j) - for i in range(0, len(coords), 3): - lon.append(float(coords[i])) - lat.append(float(coords[i + 1])) - z.append(float(coords[i + 2])) - os.remove("test_export_data_1.kml") - - assert np.allclose(test_flight.latitude[:, 1], lat, atol=1e-3) - assert np.allclose(test_flight.longitude[:, 1], lon, atol=1e-3) - assert np.allclose(test_flight.z[:, 1], z, atol=1e-3) - - -def test_export_pressures(flight_calisto_robust): - """Tests if the method Flight.export_pressures is working as intended. - - Parameters - ---------- - flight_calisto_robust : Flight - Flight object to be tested. See the conftest.py file for more info - regarding this pytest fixture. - """ - file_name = "pressures.csv" - time_step = 0.5 - parachute = flight_calisto_robust.rocket.parachutes[0] - - flight_calisto_robust.export_pressures(file_name, time_step) - - with open(file_name, "r") as file: - contents = file.read() - - expected_data = "" - for t in np.arange(0, flight_calisto_robust.t_final, time_step): - p_cl = parachute.clean_pressure_signal_function(t) - p_ns = parachute.noisy_pressure_signal_function(t) - expected_data += f"{t:f}, {p_cl:.5f}, {p_ns:.5f}\n" - - assert contents == expected_data - os.remove(file_name) - - @patch("matplotlib.pyplot.show") def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint: disable=unused-argument """Test the flight of a rocket with a hybrid motor. This test only validates diff --git a/tests/unit/test_flight_data_exporter.py b/tests/unit/test_flight_data_exporter.py index f93bb2248..315b76008 100644 --- a/tests/unit/test_flight_data_exporter.py +++ b/tests/unit/test_flight_data_exporter.py @@ -1,30 +1,53 @@ +"""Unit tests for FlightDataExporter class. + +This module tests the data export functionality of the FlightDataExporter class, +which exports flight simulation data to various formats (CSV, JSON, KML). +""" + import json + +import numpy as np + from rocketpy.simulation import FlightDataExporter -def test_export_data_writes_csv_header(flight_calisto, tmp_path): - """Expect: direct exporter writes a CSV with a header containing 'Time (s)'.""" - out = tmp_path / "out.csv" - FlightDataExporter(flight_calisto).export_data(str(out)) - text = out.read_text(encoding="utf-8") - assert "Time (s)" in text +def test_export_pressures_writes_csv_rows(flight_calisto_robust, tmp_path): + """Test that export_pressures writes CSV rows with pressure data. + Validates that the exported file contains multiple data rows in CSV format + with 2-3 columns (time and pressure values). -def test_export_pressures_writes_rows(flight_calisto_robust, tmp_path): - """Expect: direct exporter writes a pressure file with time-first CSV rows.""" - out = tmp_path / "p.csv" + Parameters + ---------- + flight_calisto_robust : rocketpy.Flight + Flight object with parachutes configured. + tmp_path : pathlib.Path + Pytest fixture for temporary directories. + """ + out = tmp_path / "pressures.csv" FlightDataExporter(flight_calisto_robust).export_pressures(str(out), time_step=0.2) lines = out.read_text(encoding="utf-8").strip().splitlines() assert len(lines) > 5 - # basic CSV shape “t, value” + # Basic CSV shape "t, value" or "t, clean, noisy" parts = lines[0].split(",") assert len(parts) in (2, 3) -def test_export_sensor_data_writes_json_when_sensor_data_present( - flight_calisto, tmp_path, monkeypatch -): - """Expect: exporter writes JSON mapping sensor.name -> data when sensor_data is present.""" +def test_export_sensor_data_writes_json(flight_calisto, tmp_path, monkeypatch): + """Test that export_sensor_data writes JSON with sensor data. + + Validates that sensor data is exported as JSON with sensor names as keys + and measurement arrays as values. + + Parameters + ---------- + flight_calisto : rocketpy.Flight + Flight object to be tested. + tmp_path : pathlib.Path + Pytest fixture for temporary directories. + monkeypatch : pytest.MonkeyPatch + Pytest fixture for modifying attributes. + """ class DummySensor: def __init__(self, name): @@ -40,3 +63,130 @@ def __init__(self, name): data = json.loads(out.read_text(encoding="utf-8")) assert data["DummySensor"] == [1.0, 2.0, 3.0] + + +def test_export_data_default_variables(flight_calisto, tmp_path): + """Test export_data with default variables (full solution matrix). + + Validates that all state variables are exported correctly when no specific + variables are requested: position (x, y, z), velocity (vx, vy, vz), + quaternions (e0, e1, e2, e3), and angular velocities (w1, w2, w3). + + Parameters + ---------- + flight_calisto : rocketpy.Flight + Flight object to be tested. + tmp_path : pathlib.Path + Pytest fixture for temporary directories. + """ + file_name = tmp_path / "flight_data.csv" + FlightDataExporter(flight_calisto).export_data(str(file_name)) + + test_data = np.loadtxt(file_name, delimiter=",", skiprows=1) + + # Verify time column + assert np.allclose(flight_calisto.x[:, 0], test_data[:, 0], atol=1e-5) + + # Verify position + assert np.allclose(flight_calisto.x[:, 1], test_data[:, 1], atol=1e-5) + assert np.allclose(flight_calisto.y[:, 1], test_data[:, 2], atol=1e-5) + assert np.allclose(flight_calisto.z[:, 1], test_data[:, 3], atol=1e-5) + + # Verify velocity + assert np.allclose(flight_calisto.vx[:, 1], test_data[:, 4], atol=1e-5) + assert np.allclose(flight_calisto.vy[:, 1], test_data[:, 5], atol=1e-5) + assert np.allclose(flight_calisto.vz[:, 1], test_data[:, 6], atol=1e-5) + + # Verify quaternions + assert np.allclose(flight_calisto.e0[:, 1], test_data[:, 7], atol=1e-5) + assert np.allclose(flight_calisto.e1[:, 1], test_data[:, 8], atol=1e-5) + assert np.allclose(flight_calisto.e2[:, 1], test_data[:, 9], atol=1e-5) + assert np.allclose(flight_calisto.e3[:, 1], test_data[:, 10], atol=1e-5) + + # Verify angular velocities + assert np.allclose(flight_calisto.w1[:, 1], test_data[:, 11], atol=1e-5) + assert np.allclose(flight_calisto.w2[:, 1], test_data[:, 12], atol=1e-5) + assert np.allclose(flight_calisto.w3[:, 1], test_data[:, 13], atol=1e-5) + + +def test_export_data_custom_variables_and_time_step(flight_calisto, tmp_path): + """Test export_data with custom variables and time step. + + Validates that specific variables can be exported with custom time intervals, + including derived quantities like angle of attack. + + Parameters + ---------- + flight_calisto : rocketpy.Flight + Flight object to be tested. + tmp_path : pathlib.Path + Pytest fixture for temporary directories. + """ + file_name = tmp_path / "custom_flight_data.csv" + time_step = 0.1 + + FlightDataExporter(flight_calisto).export_data( + str(file_name), + "z", + "vz", + "e1", + "w3", + "angle_of_attack", + time_step=time_step, + ) + + test_data = np.loadtxt(file_name, delimiter=",", skiprows=1) + time_points = np.arange(flight_calisto.t_initial, flight_calisto.t_final, time_step) + + # Verify time column + assert np.allclose(time_points, test_data[:, 0], atol=1e-5) + + # Verify custom variables + assert np.allclose(flight_calisto.z(time_points), test_data[:, 1], atol=1e-5) + assert np.allclose(flight_calisto.vz(time_points), test_data[:, 2], atol=1e-5) + assert np.allclose(flight_calisto.e1(time_points), test_data[:, 3], atol=1e-5) + assert np.allclose(flight_calisto.w3(time_points), test_data[:, 4], atol=1e-5) + assert np.allclose( + flight_calisto.angle_of_attack(time_points), test_data[:, 5], atol=1e-5 + ) + + +def test_export_kml_trajectory(flight_calisto_robust, tmp_path): + """Test export_kml creates valid KML file with trajectory coordinates. + + Validates that the KML export contains correct latitude, longitude, and + altitude coordinates for the flight trajectory in absolute altitude mode. + + Parameters + ---------- + flight_calisto_robust : rocketpy.Flight + Flight object to be tested. + tmp_path : pathlib.Path + Pytest fixture for temporary directories. + """ + file_name = tmp_path / "trajectory.kml" + FlightDataExporter(flight_calisto_robust).export_kml( + str(file_name), time_step=None, extrude=True, altitude_mode="absolute" + ) + + # Parse KML coordinates + with open(file_name, "r") as kml_file: + for row in kml_file: + if row.strip().startswith(""): + coords_str = ( + row.strip() + .replace("", "") + .replace("", "") + ) + coords_list = coords_str.strip().split(" ") + + # Extract lon, lat, z from coordinates + parsed_coords = [c.split(",") for c in coords_list] + lon = [float(point[0]) for point in parsed_coords] + lat = [float(point[1]) for point in parsed_coords] + z = [float(point[2]) for point in parsed_coords] + + # Verify coordinates match flight data + assert np.allclose(flight_calisto_robust.latitude[:, 1], lat, atol=1e-3) + assert np.allclose(flight_calisto_robust.longitude[:, 1], lon, atol=1e-3) + assert np.allclose(flight_calisto_robust.z[:, 1], z, atol=1e-3) diff --git a/tests/unit/test_flight_export_deprecation.py b/tests/unit/test_flight_export_deprecation.py index 646c249dd..6fb6952b4 100644 --- a/tests/unit/test_flight_export_deprecation.py +++ b/tests/unit/test_flight_export_deprecation.py @@ -1,6 +1,9 @@ from unittest.mock import patch + import pytest +# TODO: these tests should be deleted after the deprecated methods are removed + def test_export_data_deprecated_emits_warning_and_delegates(flight_calisto, tmp_path): """Expect: calling Flight.export_data emits DeprecationWarning and delegates to FlightDataExporter.export_data.""" From bebc8ab8809063be3eafd89ae679be4fff348c2c Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 Oct 2025 20:52:50 -0300 Subject: [PATCH 8/9] FIx commentaries --- CHANGELOG.md | 1 + docs/user/first_simulation.rst | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1feb58e1a..7c50965e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Attention: The newest changes should be on top --> ### Changed - DOC: Update docs dependencies and sub dependencies [#851](https://github.com/RocketPy-Team/RocketPy/pull/851) +- MNT: extract flight data exporters [#845](https://github.com/RocketPy-Team/RocketPy/pull/845) - ENH: _MotorPrints inheritance - issue #460 [#828](https://github.com/RocketPy-Team/RocketPy/pull/828) - MNT: fix deprecations and warnings [#829](https://github.com/RocketPy-Team/RocketPy/pull/829) diff --git a/docs/user/first_simulation.rst b/docs/user/first_simulation.rst index 9e3359992..1e341e9b1 100644 --- a/docs/user/first_simulation.rst +++ b/docs/user/first_simulation.rst @@ -677,7 +677,7 @@ by not passing any attribute names: :hide-output: # Sample file cleanup - import ospython.exe -m pip install --upgrade pip + import os os.remove("calisto_flight_data.csv") .. note:: @@ -695,7 +695,7 @@ For instance, we can save our rocket drawing as a ``.png`` file: calisto.draw(filename="calisto_drawing.png") -Also, if you want to save a specific rocketpy plot, every RocketPy +Also, if you want to save a specific rocketpy plot, every RocketPy attribute of type :class:`rocketpy.Function` is capable of saving its plot as an image file. For example, we can save our rocket's speed plot and the trajectory plot as ``.jpg`` files: From b88abc495b30f2e1d7c46afc5e379aad4bc3c9e3 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 4 Oct 2025 21:01:30 -0300 Subject: [PATCH 9/9] Fix lint --- rocketpy/simulation/flight_data_exporter.py | 3 +-- tests/integration/test_flight.py | 1 - tests/unit/test_flight_data_exporter.py | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rocketpy/simulation/flight_data_exporter.py b/rocketpy/simulation/flight_data_exporter.py index 31c798dd4..d73c0897a 100644 --- a/rocketpy/simulation/flight_data_exporter.py +++ b/rocketpy/simulation/flight_data_exporter.py @@ -149,8 +149,7 @@ class attributes which are instances of the Function class. Usage # Deal with decorated Flight methods else: try: - obj = getattr(f.__class__, variable) - variable_function = obj.__get__(f, f.__class__) + variable_function = getattr(f, variable) except AttributeError as exc: raise AttributeError( f"Variable '{variable}' not found in Flight class" diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index 1f8f4432f..f40eb6b27 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -1,4 +1,3 @@ -import os from unittest.mock import patch import matplotlib as plt diff --git a/tests/unit/test_flight_data_exporter.py b/tests/unit/test_flight_data_exporter.py index 315b76008..b7f8f4e9d 100644 --- a/tests/unit/test_flight_data_exporter.py +++ b/tests/unit/test_flight_data_exporter.py @@ -50,6 +50,8 @@ def test_export_sensor_data_writes_json(flight_calisto, tmp_path, monkeypatch): """ class DummySensor: + """Dummy sensor with name attribute for testing.""" + def __init__(self, name): self.name = name