Skip to content

Commit 4ef7e77

Browse files
authored
FEAT/Rail Button Bending Moments (#901)
* Added a button_height attribute to RailButtons with a default value of 15mm and updated the docstring * added a _calculate_rail_button_bending_moments to calculate the desired attributes * fixed new method added properties * fixed typo * fixed code, added documentation, updated changelog, added tests * formatting * FIX: Handle center_of_dry_mass_position as property or callable * ENH: Address review feedback - make button_height optional (None), add prints/plots, improve documentation * clarified documentation(assumptions) in flight.rst * TST: Split default vs explicit None button_height tests * make format * MNT: Fix pylint config and remove duplicate test imports
1 parent b252199 commit 4ef7e77

File tree

7 files changed

+515
-0
lines changed

7 files changed

+515
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3131
Attention: The newest changes should be on top -->
3232

3333
### Added
34+
- ENH: Rail button bending moments calculation in Flight class [#893](https://github.com/RocketPy-Team/RocketPy/pull/893)
3435
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
3536
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
3637
- ENH: Compatibility with MERRA-2 atmosphere reanalysis files [#825](https://github.com/RocketPy-Team/RocketPy/pull/825)

docs/user/flight.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,39 @@ The Flight object provides access to all forces and accelerations acting on the
266266
M2 = flight.M2 # Pitch moment
267267
M3 = flight.M3 # Yaw moment
268268

269+
Rail Button Forces and Bending Moments
270+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271+
272+
During the rail launch phase, RocketPy calculates reaction forces and internal bending moments at the rail button attachment points:
273+
274+
**Rail Button Forces (N):**
275+
276+
- ``rail_button1_normal_force`` : Normal reaction force at upper rail button
277+
- ``rail_button1_shear_force`` : Shear (tangential) reaction force at upper rail button
278+
- ``rail_button2_normal_force`` : Normal reaction force at lower rail button
279+
- ``rail_button2_shear_force`` : Shear (tangential) reaction force at lower rail button
280+
281+
**Rail Button Bending Moments (N⋅m):**
282+
283+
- ``rail_button1_bending_moment`` : Time-dependent bending moment at upper rail button attachment
284+
- ``max_rail_button1_bending_moment`` : Maximum absolute bending moment at upper rail button
285+
- ``rail_button2_bending_moment`` : Time-dependent bending moment at lower rail button attachment
286+
- ``max_rail_button2_bending_moment`` : Maximum absolute bending moment at lower rail button
287+
288+
**Calculation Method:**
289+
290+
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:
291+
292+
1. Shear force × button height (cantilever moment from button standoff)
293+
2. Normal force × distance to center of dry mass (lever arm effect)
294+
295+
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()``.
296+
297+
.. note::
298+
See Issue #893 for implementation details and validation approach.
299+
300+
301+
269302
Attitude and Orientation
270303
~~~~~~~~~~~~~~~~~~~~~~~~
271304

rocketpy/plots/flight_plots.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,70 @@ def angular_kinematics_data(self, *, filename=None): # pylint: disable=too-many
395395
plt.subplots_adjust(hspace=0.5)
396396
show_or_save_plot(filename)
397397

398+
def rail_buttons_bending_moments(self, *, filename=None):
399+
"""Prints out Rail Buttons Bending Moments graphs.
400+
401+
Parameters
402+
----------
403+
filename : str | None, optional
404+
The path the plot should be saved to. By default None, in which case
405+
the plot will be shown instead of saved. Supported file endings are:
406+
eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff
407+
and webp (these are the formats supported by matplotlib).
408+
409+
Returns
410+
-------
411+
None
412+
"""
413+
if len(self.flight.rocket.rail_buttons) == 0:
414+
print(
415+
"No rail buttons were defined. Skipping rail button bending moment plots."
416+
)
417+
elif self.flight.out_of_rail_time_index == 0:
418+
print("No rail phase was found. Skipping rail button bending moment plots.")
419+
else:
420+
# Check if button_height is defined
421+
rail_buttons_tuple = self.flight.rocket.rail_buttons[0]
422+
if rail_buttons_tuple.component.button_height is None:
423+
print("Rail button height not defined. Skipping bending moment plots.")
424+
else:
425+
plt.figure(figsize=(9, 3))
426+
427+
ax1 = plt.subplot(111)
428+
ax1.plot(
429+
self.flight.rail_button1_bending_moment[
430+
: self.flight.out_of_rail_time_index, 0
431+
],
432+
self.flight.rail_button1_bending_moment[
433+
: self.flight.out_of_rail_time_index, 1
434+
],
435+
label="Upper Rail Button",
436+
)
437+
ax1.plot(
438+
self.flight.rail_button2_bending_moment[
439+
: self.flight.out_of_rail_time_index, 0
440+
],
441+
self.flight.rail_button2_bending_moment[
442+
: self.flight.out_of_rail_time_index, 1
443+
],
444+
label="Lower Rail Button",
445+
)
446+
ax1.set_xlim(
447+
0,
448+
(
449+
self.flight.out_of_rail_time
450+
if self.flight.out_of_rail_time > 0
451+
else self.flight.tFinal
452+
),
453+
)
454+
ax1.legend()
455+
ax1.grid(True)
456+
ax1.set_xlabel("Time (s)")
457+
ax1.set_ylabel("Bending Moment (N·m)")
458+
ax1.set_title("Rail Button Bending Moments")
459+
460+
show_or_save_plot(filename)
461+
398462
def rail_buttons_forces(self, *, filename=None): # pylint: disable=too-many-statements
399463
"""Prints out all Rail Buttons Forces graphs available about the Flight.
400464
@@ -959,6 +1023,9 @@ def all(self): # pylint: disable=too-many-statements
9591023
print("\n\nAerodynamic Forces Plots\n")
9601024
self.aerodynamic_forces()
9611025

1026+
print("\n\nRail Buttons Bending Moments Plots\n")
1027+
self.rail_buttons_bending_moments()
1028+
9621029
print("\n\nRail Buttons Forces Plots\n")
9631030
self.rail_buttons_forces()
9641031

rocketpy/prints/flight_prints.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,34 @@ def maximum_values(self):
358358
f"{self.flight.max_rail_button2_shear_force:.3f} N"
359359
)
360360

361+
def rail_button_bending_moments(self):
362+
"""Prints rail button bending moment data.
363+
364+
Returns
365+
-------
366+
None
367+
"""
368+
if (
369+
len(self.flight.rocket.rail_buttons) == 0
370+
or self.flight.out_of_rail_time_index == 0
371+
):
372+
return
373+
374+
# Check if button_height is defined
375+
rail_buttons_tuple = self.flight.rocket.rail_buttons[0]
376+
if rail_buttons_tuple.component.button_height is None:
377+
return
378+
379+
print("\nRail Button Bending Moments\n")
380+
print(
381+
"Maximum Upper Rail Button Bending Moment: "
382+
f"{self.flight.max_rail_button1_bending_moment:.3f} N·m"
383+
)
384+
print(
385+
"Maximum Lower Rail Button Bending Moment: "
386+
f"{self.flight.max_rail_button2_bending_moment:.3f} N·m"
387+
)
388+
361389
def stability_margin(self):
362390
"""Prints out the stability margins of the flight at different times.
363391
@@ -429,5 +457,8 @@ def all(self):
429457
self.maximum_values()
430458
print()
431459

460+
self.rail_button_bending_moments()
461+
print()
462+
432463
self.numerical_integration_settings()
433464
print()

rocketpy/rocket/aero_surface/rail_buttons.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,19 @@ class RailButtons(AeroSurface):
1919
relative to one of the other principal axis.
2020
RailButtons.angular_position_rad : float
2121
Angular position of the rail buttons in radians.
22+
RailButtons.button_height : float, optional
23+
Height (standoff distance) of the rail button from the rocket
24+
body surface to the rail contact point, in meters. Used for
25+
calculating bending moments at the attachment point.
26+
Default is None. If not provided, bending moments cannot be
27+
calculated but flight dynamics remain unaffected.
2228
"""
2329

2430
def __init__(
2531
self,
2632
buttons_distance,
2733
angular_position=45,
34+
button_height=None,
2835
name="Rail Buttons",
2936
rocket_radius=None,
3037
):
@@ -48,6 +55,7 @@ def __init__(
4855
super().__init__(name, None, None)
4956
self.buttons_distance = buttons_distance
5057
self.angular_position = angular_position
58+
self.button_height = button_height
5159
self.name = name
5260
self.rocket_radius = rocket_radius
5361
self.evaluate_lift_coefficient()
@@ -104,6 +112,7 @@ def to_dict(self, **kwargs): # pylint: disable=unused-argument
104112
return {
105113
"buttons_distance": self.buttons_distance,
106114
"angular_position": self.angular_position,
115+
"button_height": self.button_height,
107116
"name": self.name,
108117
"rocket_radius": self.rocket_radius,
109118
}
@@ -113,6 +122,7 @@ def from_dict(cls, data):
113122
return cls(
114123
data["buttons_distance"],
115124
data["angular_position"],
125+
data.get("button_height", None),
116126
data["name"],
117127
data["rocket_radius"],
118128
)

rocketpy/simulation/flight.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,18 @@ class Flight:
470470
array.
471471
Flight.simulation_mode : str
472472
Simulation mode for the flight. Can be "6 DOF" or "3 DOF".
473+
Flight.rail_button1_bending_moment : Function
474+
Internal bending moment at upper rail button attachment point in N·m
475+
as a function of time. Calculated using beam theory during rail phase.
476+
Flight.max_rail_button1_bending_moment : float
477+
Maximum internal bending moment experienced at upper rail button
478+
attachment point during rail flight phase in N·m.
479+
Flight.rail_button2_bending_moment : Function
480+
Internal bending moment at lower rail button attachment point in N·m
481+
as a function of time. Calculated using beam theory during rail phase.
482+
Flight.max_rail_button2_bending_moment : float
483+
Maximum internal bending moment experienced at lower rail button
484+
attachment point during rail flight phase in N·m.
473485
"""
474486

475487
def __init__( # pylint: disable=too-many-arguments,too-many-statements
@@ -3958,3 +3970,142 @@ def __lt__(self, other):
39583970
otherwise.
39593971
"""
39603972
return self.t < other.t
3973+
3974+
@cached_property
3975+
def calculate_rail_button_bending_moments(self):
3976+
"""
3977+
Calculate internal bending moments at rail button attachment points.
3978+
3979+
Uses beam theory to determine internal structural moments for stress
3980+
analysis of the rail button attachments (fasteners and airframe).
3981+
3982+
The bending moment at each button attachment consists of:
3983+
1. Bending from shear force at button contact point: M = S × h
3984+
where S is the shear (tangential) force and h is button height
3985+
2. Direct moment contribution from the button's reaction forces
3986+
3987+
Assumptions
3988+
-----------
3989+
- Rail buttons act as simple supports: provide reaction forces (normal
3990+
and shear) but no moment reaction at the rail contact point.
3991+
- The rocket acts as a beam supported at two points (rail buttons).
3992+
- Bending moments arise from the lever arm effect of reaction forces
3993+
and the cantilever moment from button standoff height.
3994+
3995+
The bending moment at each button attachment consists of:
3996+
1. Normal force moment: M = N x d, where N is normal reaction force
3997+
and d is distance from button to center of dry mass
3998+
2. Shear force cantilever moment: M = S x h, where S is shear force
3999+
and h is button standoff height
4000+
4001+
Notes
4002+
-----
4003+
- Calculated only during the rail phase of flight
4004+
- Maximum values use absolute values for worst-case stress analysis
4005+
- The bending moments represent internal stresses in the rocket
4006+
airframe at the rail button attachment points
4007+
4008+
Returns
4009+
-------
4010+
tuple
4011+
(rail_button1_bending_moment : Function,
4012+
max_rail_button1_bending_moment : float,
4013+
rail_button2_bending_moment : Function,
4014+
max_rail_button2_bending_moment : float)
4015+
4016+
Where rail_button1/2_bending_moment are Function objects of time
4017+
in N·m, and max values are floats in N·m.
4018+
"""
4019+
# Check if rail buttons exist
4020+
null_moment = Function(0)
4021+
if len(self.rocket.rail_buttons) == 0:
4022+
warnings.warn(
4023+
"Trying to calculate rail button bending moments without "
4024+
"rail buttons defined. Setting moments to zero.",
4025+
UserWarning,
4026+
)
4027+
return (null_moment, 0.0, null_moment, 0.0)
4028+
4029+
# Get rail button geometry
4030+
rail_buttons_tuple = self.rocket.rail_buttons[0]
4031+
# Rail button standoff height
4032+
h_button = rail_buttons_tuple.component.button_height
4033+
if h_button is None:
4034+
warnings.warn(
4035+
"Rail button height not defined. Bending moments cannot be "
4036+
"calculated. Setting moments to zero.",
4037+
UserWarning,
4038+
)
4039+
return (null_moment, 0.0, null_moment, 0.0)
4040+
upper_button_position = (
4041+
rail_buttons_tuple.component.buttons_distance
4042+
+ rail_buttons_tuple.position.z
4043+
)
4044+
lower_button_position = rail_buttons_tuple.position.z
4045+
4046+
# Get center of dry mass (handle both callable and property)
4047+
if callable(self.rocket.center_of_dry_mass_position):
4048+
cdm = self.rocket.center_of_dry_mass_position(self.rocket._csys)
4049+
else:
4050+
cdm = self.rocket.center_of_dry_mass_position
4051+
4052+
# Distances from buttons to center of dry mass
4053+
d1 = abs(upper_button_position - cdm)
4054+
d2 = abs(lower_button_position - cdm)
4055+
4056+
# forces
4057+
N1 = self.rail_button1_normal_force
4058+
N2 = self.rail_button2_normal_force
4059+
S1 = self.rail_button1_shear_force
4060+
S2 = self.rail_button2_shear_force
4061+
t = N1.source[:, 0]
4062+
4063+
# Calculate bending moments at attachment points
4064+
# Primary contribution from shear force acting at button height
4065+
# Secondary contribution from normal force creating moment about attachment
4066+
m1_values = N2.source[:, 1] * d2 + S1.source[:, 1] * h_button
4067+
m2_values = N1.source[:, 1] * d1 + S2.source[:, 1] * h_button
4068+
4069+
rail_button1_bending_moment = Function(
4070+
np.column_stack([t, m1_values]),
4071+
inputs="Time (s)",
4072+
outputs="Bending Moment (N·m)",
4073+
interpolation="linear",
4074+
)
4075+
rail_button2_bending_moment = Function(
4076+
np.column_stack([t, m2_values]),
4077+
inputs="Time (s)",
4078+
outputs="Bending Moment (N·m)",
4079+
interpolation="linear",
4080+
)
4081+
4082+
# Maximum bending moments (absolute value for stress calculations)
4083+
max_rail_button1_bending_moment = float(np.max(np.abs(m1_values)))
4084+
max_rail_button2_bending_moment = float(np.max(np.abs(m2_values)))
4085+
4086+
return (
4087+
rail_button1_bending_moment,
4088+
max_rail_button1_bending_moment,
4089+
rail_button2_bending_moment,
4090+
max_rail_button2_bending_moment,
4091+
)
4092+
4093+
@property
4094+
def rail_button1_bending_moment(self):
4095+
"""Upper rail button bending moment as a Function of time."""
4096+
return self.calculate_rail_button_bending_moments[0]
4097+
4098+
@property
4099+
def max_rail_button1_bending_moment(self):
4100+
"""Maximum upper rail button bending moment, in N·m."""
4101+
return self.calculate_rail_button_bending_moments[1]
4102+
4103+
@property
4104+
def rail_button2_bending_moment(self):
4105+
"""Lower rail button bending moment as a Function of time."""
4106+
return self.calculate_rail_button_bending_moments[2]
4107+
4108+
@property
4109+
def max_rail_button2_bending_moment(self):
4110+
"""Maximum lower rail button bending moment, in N·m."""
4111+
return self.calculate_rail_button_bending_moments[3]

0 commit comments

Comments
 (0)