diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b394e4fa..479357b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ Attention: The newest changes should be on top --> ### Changed +- DOC: Update examples to use `FlightDataExporter` and add deprecation notes for legacy `Flight.export_*` methods. (#XXXX) +- DEP: `Flight.export_data`, `Flight.export_pressures`, `Flight.export_sensor_data`, and `Flight.export_kml` are deprecated. Use `rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_*`. Planned removal in v1.12.0. (#XXXX) +- MNT: Extract `Flight.export_data`, `export_pressures`, `export_sensor_data`, and `export_kml` into `rocketpy.simulation.flight_data_exporter.FlightDataExporter` (no behavior change). (#XXXX) - 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 5624ed926..3b70924de 100644 --- a/docs/user/first_simulation.rst +++ b/docs/user/first_simulation.rst @@ -559,12 +559,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:: @@ -572,6 +576,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 -------------------- @@ -604,17 +612,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:: @@ -622,11 +640,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", @@ -635,24 +655,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 ------------------------ 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 5283fd12a..cf7fbcae6 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 = { @@ -3225,191 +3226,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): + """ + .. deprecated:: 1.11 + Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` + and call ``.export_pressures(...)``. """ - 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 + 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, @@ -3417,78 +3280,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.""" 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!") diff --git a/tests/unit/test_flight_data_exporter.py b/tests/unit/test_flight_data_exporter.py new file mode 100644 index 000000000..f93bb2248 --- /dev/null +++ b/tests/unit/test_flight_data_exporter.py @@ -0,0 +1,42 @@ +import json +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..646c249dd --- /dev/null +++ b/tests/unit/test_flight_export_deprecation.py @@ -0,0 +1,37 @@ +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" + 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()