Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 258 additions & 53 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
@@ -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

Comment on lines +2 to 11
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of import json and import simplekml at the module level, along with the removal of the FlightDataExporter import and the deprecated import from tools, appears to be unrelated to the stated purpose of this PR (adding axial acceleration calculation).

These changes seem to be reverting the refactoring that moved export methods to FlightDataExporter, as the methods export_pressures, export_data, export_sensor_data, and export_kml have been restored to the Flight class with their full implementations (lines 3276-3539).

This significantly expands the scope of the PR beyond what is described and should be addressed in a separate PR or the PR description should be updated to reflect these additional changes.

Copilot uses AI. Check for mistakes.
from rocketpy.simulation.flight_data_exporter import FlightDataExporter

from ..mathutils.function import Function, funcify_method
from ..mathutils.vector_matrix import Matrix, Vector
from ..motors.point_mass_motor import PointMassMotor
Expand All @@ -17,7 +17,6 @@
from ..rocket import PointMassRocket
from ..tools import (
calculate_cubic_hermite_coefficients,
deprecated,
euler313_to_quaternions,
Comment on lines 18 to 20
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of the deprecated import and restoration of export methods (export_pressures, export_data, export_sensor_data, export_kml) appears to be reverting a previous deprecation without explanation. These methods were previously marked as deprecated in favor of FlightDataExporter class, but are now being restored with full implementations.

This change is unrelated to the PR's stated purpose of adding axial acceleration and represents a significant architectural change that should be discussed separately or properly documented in the PR description.

Copilot uses AI. Check for mistakes.
find_closest,
find_root_linear_interpolation,
Expand Down Expand Up @@ -1136,7 +1135,6 @@ def __init_solution_monitors(self):
self.out_of_rail_time_index = 0
self.out_of_rail_state = np.array([0])
self.apogee_state = np.array([0])
self.apogee = 0
self.apogee_time = 0
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of self.apogee = 0 initialization appears to be unintentional. This attribute is still used in the codebase (e.g., line 899 where self.apogee = self.apogee_state[2] is assigned). Without this initialization, accessing self.apogee before apogee is reached during flight simulation could raise an AttributeError.

This line should be restored to maintain proper initialization of instance attributes.

Suggested change
self.apogee_time = 0
self.apogee_time = 0
self.apogee = 0

Copilot uses AI. Check for mistakes.
self.x_impact = 0
self.y_impact = 0
Expand Down Expand Up @@ -2619,6 +2617,15 @@ def max_acceleration(self):
"""Maximum acceleration reached by the rocket."""
max_acceleration_time_index = np.argmax(self.acceleration[:, 1])
return self.acceleration[max_acceleration_time_index, 1]

Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace detected. This line should end immediately after the closing brace without extra spaces.

Suggested change

Copilot uses AI. Check for mistakes.
@cached_property
def axial_acceleration(self):
"""Axial acceleration magnitude as a function of time."""
return (
self.ax * self.attitude_vector_x
+ self.ay * self.attitude_vector_y
+ self.az * self.attitude_vector_z
)
Comment on lines +2621 to +2628
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The axial_acceleration property is missing the @funcify_method decorator that other similar properties use. This decorator is used throughout the codebase to define the output labels and interpolation methods for Function objects. It should include appropriate labels with units.

Suggested fix:

@funcify_method("Time (s)", "Axial Acceleration (m/s²)", "spline", "zero")
def axial_acceleration(self):
    """Axial acceleration magnitude as a function of time, in m/s²."""
    return (
        self.ax * self.attitude_vector_x
        + self.ay * self.attitude_vector_y
        + self.az * self.attitude_vector_z
    )

Copilot generated this review using guidance from repository custom instructions.

@funcify_method("Time (s)", "Horizontal Speed (m/s)")
def horizontal_speed(self):
Expand Down Expand Up @@ -3415,72 +3422,270 @@ def calculate_stall_wind_velocity(self, stall_angle): # TODO: move to utilities
+ f" of attack exceeds {stall_angle:.3f}°: {w_v:.3f} m/s"
)

@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(...)``.
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
"""
return FlightDataExporter(self).export_pressures(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(
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.
"""
.. 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
# 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(
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.
"""
.. 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)
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.")

@deprecated(
reason="Moved to FlightDataExporter.export_kml()",
version="v1.12.0",
alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml",
)
def export_kml(
with open(file_name, "w") as file:
json.dump(data_dict, file)
print("Sensor data exported to: ", file_name)

def export_kml( # TODO: should be moved out of this class.
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.
"""
.. 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,
)
# 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!")

def info(self):
"""Prints out a summary of the data available about the Flight."""
Expand Down
Loading