Skip to content

Commit 5037d87

Browse files
authored
Merge branch 'develop' into enh/improve-tanks-and-motors-visualizations
2 parents 6a9fc6a + afb3e3e commit 5037d87

8 files changed

Lines changed: 556 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Attention: The newest changes should be on top -->
3434

3535
- ENH: Add animations for motor propellant mass and tank fluid volume [#656](https://github.com/RocketPy-Team/RocketPy/issues/656)
3636
- ENH: Add save functionality to `_MonteCarloPlots.all` method [#848](https://github.com/RocketPy-Team/RocketPy/pull/848)
37+
- ENH: Add axial_acceleration attribute to the Flight class [#876](https://github.com/RocketPy-Team/RocketPy/pull/876)
38+
- ENH: Rail button bending moments calculation in Flight class [#893](https://github.com/RocketPy-Team/RocketPy/pull/893)
3739
- ENH: Built-in flight comparison tool (`FlightComparator`) to validate simulations against external data [#888](https://github.com/RocketPy-Team/RocketPy/pull/888)
3840
- ENH: Add persistent caching for ThrustCurve API [#881](https://github.com/RocketPy-Team/RocketPy/pull/881)
3941
- 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: 160 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
@@ -2571,6 +2583,15 @@ def acceleration(self):
25712583
"""Rocket acceleration magnitude as a Function of time."""
25722584
return (self.ax**2 + self.ay**2 + self.az**2) ** 0.5
25732585

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

0 commit comments

Comments
 (0)