Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
Attention: The newest changes should be on top -->

### Added
- ENH: Rail button bending moments calculation in Flight class [#893](https://github.com/RocketPy-Team/RocketPy/pull/893)
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
- ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825)
Expand Down
33 changes: 33 additions & 0 deletions docs/user/flight.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,39 @@ The Flight object provides access to all forces and accelerations acting on the
M2 = flight.M2 # Pitch moment
M3 = flight.M3 # Yaw moment

Rail Button Forces and Bending Moments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

During the rail launch phase, RocketPy calculates reaction forces and internal bending moments at the rail button attachment points:

**Rail Button Forces (N):**

- ``rail_button1_normal_force`` : Normal reaction force at upper rail button
- ``rail_button1_shear_force`` : Shear (tangential) reaction force at upper rail button
- ``rail_button2_normal_force`` : Normal reaction force at lower rail button
- ``rail_button2_shear_force`` : Shear (tangential) reaction force at lower rail button

**Rail Button Bending Moments (N⋅m):**

- ``rail_button1_bending_moment`` : Time-dependent bending moment at upper rail button attachment
- ``max_rail_button1_bending_moment`` : Maximum absolute bending moment at upper rail button
- ``rail_button2_bending_moment`` : Time-dependent bending moment at lower rail button attachment
- ``max_rail_button2_bending_moment`` : Maximum absolute bending moment at lower rail button

**Calculation Method:**

Bending moments are calculated using beam theory assuming simple supports (rail buttons provide reaction forces but no moment reaction at rail contact). The total moment combines:

1. Shear force × button height (cantilever moment from button standoff)
2. Normal force × distance to center of dry mass (lever arm effect)

Moments are zero after rail departure and represent internal structural loads for airframe and fastener stress analysis. Requires ``button_height`` to be defined when adding rail buttons via ``rocket.set_rail_buttons()``.

.. note::
See Issue #893 for implementation details and validation approach.



Attitude and Orientation
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
67 changes: 67 additions & 0 deletions rocketpy/plots/flight_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,70 @@ def angular_kinematics_data(self, *, filename=None): # pylint: disable=too-many
plt.subplots_adjust(hspace=0.5)
show_or_save_plot(filename)

def rail_buttons_bending_moments(self, *, filename=None):
"""Prints out Rail Buttons Bending Moments graphs.

Parameters
----------
filename : str | None, optional
The path the plot should be saved to. By default None, in which case
the plot will be shown instead of saved. Supported file endings are:
eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff
and webp (these are the formats supported by matplotlib).

Returns
-------
None
"""
if len(self.flight.rocket.rail_buttons) == 0:
print(
"No rail buttons were defined. Skipping rail button bending moment plots."
)
elif self.flight.out_of_rail_time_index == 0:
print("No rail phase was found. Skipping rail button bending moment plots.")
else:
# Check if button_height is defined
rail_buttons_tuple = self.flight.rocket.rail_buttons[0]
if rail_buttons_tuple.component.button_height is None:
print("Rail button height not defined. Skipping bending moment plots.")
else:
plt.figure(figsize=(9, 3))

ax1 = plt.subplot(111)
ax1.plot(
self.flight.rail_button1_bending_moment[
: self.flight.out_of_rail_time_index, 0
],
self.flight.rail_button1_bending_moment[
: self.flight.out_of_rail_time_index, 1
],
label="Upper Rail Button",
)
ax1.plot(
self.flight.rail_button2_bending_moment[
: self.flight.out_of_rail_time_index, 0
],
self.flight.rail_button2_bending_moment[
: self.flight.out_of_rail_time_index, 1
],
label="Lower Rail Button",
)
ax1.set_xlim(
0,
(
self.flight.out_of_rail_time
if self.flight.out_of_rail_time > 0
else self.flight.tFinal
),
)
ax1.legend()
ax1.grid(True)
ax1.set_xlabel("Time (s)")
ax1.set_ylabel("Bending Moment (N·m)")
ax1.set_title("Rail Button Bending Moments")

show_or_save_plot(filename)

def rail_buttons_forces(self, *, filename=None): # pylint: disable=too-many-statements
"""Prints out all Rail Buttons Forces graphs available about the Flight.

Expand Down Expand Up @@ -959,6 +1023,9 @@ def all(self): # pylint: disable=too-many-statements
print("\n\nAerodynamic Forces Plots\n")
self.aerodynamic_forces()

print("\n\nRail Buttons Bending Moments Plots\n")
self.rail_buttons_bending_moments()

print("\n\nRail Buttons Forces Plots\n")
self.rail_buttons_forces()

Expand Down
31 changes: 31 additions & 0 deletions rocketpy/prints/flight_prints.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,34 @@ def maximum_values(self):
f"{self.flight.max_rail_button2_shear_force:.3f} N"
)

def rail_button_bending_moments(self):
"""Prints rail button bending moment data.

Returns
-------
None
"""
if (
len(self.flight.rocket.rail_buttons) == 0
or self.flight.out_of_rail_time_index == 0
):
return

# Check if button_height is defined
rail_buttons_tuple = self.flight.rocket.rail_buttons[0]
if rail_buttons_tuple.component.button_height is None:
return

print("\nRail Button Bending Moments\n")
print(
"Maximum Upper Rail Button Bending Moment: "
f"{self.flight.max_rail_button1_bending_moment:.3f} N·m"
)
print(
"Maximum Lower Rail Button Bending Moment: "
f"{self.flight.max_rail_button2_bending_moment:.3f} N·m"
)

def stability_margin(self):
"""Prints out the stability margins of the flight at different times.

Expand Down Expand Up @@ -429,5 +457,8 @@ def all(self):
self.maximum_values()
print()

self.rail_button_bending_moments()
print()

self.numerical_integration_settings()
print()
10 changes: 10 additions & 0 deletions rocketpy/rocket/aero_surface/rail_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ class RailButtons(AeroSurface):
relative to one of the other principal axis.
RailButtons.angular_position_rad : float
Angular position of the rail buttons in radians.
RailButtons.button_height : float, optional
Height (standoff distance) of the rail button from the rocket
body surface to the rail contact point, in meters. Used for
calculating bending moments at the attachment point.
Default is None. If not provided, bending moments cannot be
calculated but flight dynamics remain unaffected.
"""

def __init__(
self,
buttons_distance,
angular_position=45,
button_height=None,
name="Rail Buttons",
rocket_radius=None,
):
Expand All @@ -48,6 +55,7 @@ def __init__(
super().__init__(name, None, None)
self.buttons_distance = buttons_distance
self.angular_position = angular_position
self.button_height = button_height
self.name = name
self.rocket_radius = rocket_radius
self.evaluate_lift_coefficient()
Expand Down Expand Up @@ -104,6 +112,7 @@ def to_dict(self, **kwargs): # pylint: disable=unused-argument
return {
"buttons_distance": self.buttons_distance,
"angular_position": self.angular_position,
"button_height": self.button_height,
"name": self.name,
"rocket_radius": self.rocket_radius,
}
Expand All @@ -113,6 +122,7 @@ def from_dict(cls, data):
return cls(
data["buttons_distance"],
data["angular_position"],
data.get("button_height", None),
data["name"],
data["rocket_radius"],
)
Expand Down
151 changes: 151 additions & 0 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,18 @@ class Flight:
array.
Flight.simulation_mode : str
Simulation mode for the flight. Can be "6 DOF" or "3 DOF".
Flight.rail_button1_bending_moment : Function
Internal bending moment at upper rail button attachment point in N·m
as a function of time. Calculated using beam theory during rail phase.
Flight.max_rail_button1_bending_moment : float
Maximum internal bending moment experienced at upper rail button
attachment point during rail flight phase in N·m.
Flight.rail_button2_bending_moment : Function
Internal bending moment at lower rail button attachment point in N·m
as a function of time. Calculated using beam theory during rail phase.
Flight.max_rail_button2_bending_moment : float
Maximum internal bending moment experienced at lower rail button
attachment point during rail flight phase in N·m.
"""

def __init__( # pylint: disable=too-many-arguments,too-many-statements
Expand Down Expand Up @@ -3958,3 +3970,142 @@ def __lt__(self, other):
otherwise.
"""
return self.t < other.t

@cached_property
def calculate_rail_button_bending_moments(self):
"""
Calculate internal bending moments at rail button attachment points.

Uses beam theory to determine internal structural moments for stress
analysis of the rail button attachments (fasteners and airframe).

The bending moment at each button attachment consists of:
1. Bending from shear force at button contact point: M = S × h
where S is the shear (tangential) force and h is button height
2. Direct moment contribution from the button's reaction forces

Assumptions
-----------
- Rail buttons act as simple supports: provide reaction forces (normal
and shear) but no moment reaction at the rail contact point.
- The rocket acts as a beam supported at two points (rail buttons).
- Bending moments arise from the lever arm effect of reaction forces
and the cantilever moment from button standoff height.

The bending moment at each button attachment consists of:
1. Normal force moment: M = N x d, where N is normal reaction force
and d is distance from button to center of dry mass
2. Shear force cantilever moment: M = S x h, where S is shear force
and h is button standoff height

Notes
-----
- Calculated only during the rail phase of flight
- Maximum values use absolute values for worst-case stress analysis
- The bending moments represent internal stresses in the rocket
airframe at the rail button attachment points

Returns
-------
tuple
(rail_button1_bending_moment : Function,
max_rail_button1_bending_moment : float,
rail_button2_bending_moment : Function,
max_rail_button2_bending_moment : float)

Where rail_button1/2_bending_moment are Function objects of time
in N·m, and max values are floats in N·m.
"""
# Check if rail buttons exist
null_moment = Function(0)
if len(self.rocket.rail_buttons) == 0:
warnings.warn(
"Trying to calculate rail button bending moments without "
"rail buttons defined. Setting moments to zero.",
UserWarning,
)
return (null_moment, 0.0, null_moment, 0.0)

# Get rail button geometry
rail_buttons_tuple = self.rocket.rail_buttons[0]
# Rail button standoff height
h_button = rail_buttons_tuple.component.button_height
if h_button is None:
warnings.warn(
"Rail button height not defined. Bending moments cannot be "
"calculated. Setting moments to zero.",
UserWarning,
)
return (null_moment, 0.0, null_moment, 0.0)
upper_button_position = (
rail_buttons_tuple.component.buttons_distance
+ rail_buttons_tuple.position.z
)
lower_button_position = rail_buttons_tuple.position.z

# Get center of dry mass (handle both callable and property)
if callable(self.rocket.center_of_dry_mass_position):
cdm = self.rocket.center_of_dry_mass_position(self.rocket._csys)
else:
cdm = self.rocket.center_of_dry_mass_position

# Distances from buttons to center of dry mass
d1 = abs(upper_button_position - cdm)
d2 = abs(lower_button_position - cdm)

# forces
N1 = self.rail_button1_normal_force
N2 = self.rail_button2_normal_force
S1 = self.rail_button1_shear_force
S2 = self.rail_button2_shear_force
t = N1.source[:, 0]

# Calculate bending moments at attachment points
# Primary contribution from shear force acting at button height
# Secondary contribution from normal force creating moment about attachment
m1_values = N2.source[:, 1] * d2 + S1.source[:, 1] * h_button
m2_values = N1.source[:, 1] * d1 + S2.source[:, 1] * h_button

rail_button1_bending_moment = Function(
np.column_stack([t, m1_values]),
inputs="Time (s)",
outputs="Bending Moment (N·m)",
interpolation="linear",
)
rail_button2_bending_moment = Function(
np.column_stack([t, m2_values]),
inputs="Time (s)",
outputs="Bending Moment (N·m)",
interpolation="linear",
)

# Maximum bending moments (absolute value for stress calculations)
max_rail_button1_bending_moment = float(np.max(np.abs(m1_values)))
max_rail_button2_bending_moment = float(np.max(np.abs(m2_values)))

return (
rail_button1_bending_moment,
max_rail_button1_bending_moment,
rail_button2_bending_moment,
max_rail_button2_bending_moment,
)

@property
def rail_button1_bending_moment(self):
"""Upper rail button bending moment as a Function of time."""
return self.calculate_rail_button_bending_moments[0]

@property
def max_rail_button1_bending_moment(self):
"""Maximum upper rail button bending moment, in N·m."""
return self.calculate_rail_button_bending_moments[1]

@property
def rail_button2_bending_moment(self):
"""Lower rail button bending moment as a Function of time."""
return self.calculate_rail_button_bending_moments[2]

@property
def max_rail_button2_bending_moment(self):
"""Maximum lower rail button bending moment, in N·m."""
return self.calculate_rail_button_bending_moments[3]
Loading