Skip to content

Commit 4bf04d5

Browse files
committed
fixed code, added documentation, updated changelog, added tests
1 parent 77cece0 commit 4bf04d5

4 files changed

Lines changed: 207 additions & 36 deletions

File tree

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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,36 @@ 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+
These bending moments are calculated using beam theory, combining:
289+
1. Shear force x button height (cantilever moment)
290+
2. Normal reaction forces x distance to center of dry mass (lever arm moment)
291+
292+
The moments are zero after rail departure and represent internal structural loads for airframe and fastener stress analysis.
293+
294+
.. note::
295+
Requires rail buttons to be defined on the Rocket using ``rocket.set_rail_buttons()``.
296+
See Issue #893 for implementation details.
297+
298+
269299
Attitude and Orientation
270300
~~~~~~~~~~~~~~~~~~~~~~~~
271301

rocketpy/simulation/flight.py

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,74 +3970,106 @@ def __lt__(self, other):
39703970
otherwise.
39713971
"""
39723972
return self.t < other.t
3973+
39733974
@cached_property
39743975
def calculate_rail_button_bending_moments(self):
39753976
"""
39763977
Calculate internal bending moments at rail button attachment points.
39773978
3978-
Uses beam theory with simple support assumptions (ΣF≠0, M_contact=0)
3979-
to determine internal structural moments for stress analysis.
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
39803986
3981-
The bending moment at each button consists of two components:
3982-
1. Main bending from the opposing button's normal force (M = N × L)
3983-
2. Additional moment from shear force at button tip (M = S × h)
3987+
Notes
3988+
-----
3989+
- Calculated only during the rail phase of flight
3990+
- Maximum values use absolute values for worst-case stress analysis
3991+
- The bending moments represent internal stresses in the rocket
3992+
airframe at the rail button attachment points
39843993
39853994
Returns
39863995
-------
39873996
tuple
3988-
(rail_button1_bending_moment, max_rail_button1_bending_moment,
3989-
rail_button2_bending_moment, max_rail_button2_bending_moment)
3997+
(rail_button1_bending_moment : Function,
3998+
max_rail_button1_bending_moment : float,
3999+
rail_button2_bending_moment : Function,
4000+
max_rail_button2_bending_moment : float)
4001+
4002+
Where rail_button1/2_bending_moment are Function objects of time
4003+
in N·m, and max values are floats in N·m.
39904004
"""
39914005
# Check if rail buttons exist
4006+
null_moment = Function(0)
39924007
if len(self.rocket.rail_buttons) == 0:
39934008
warnings.warn(
39944009
"Trying to calculate rail button bending moments without "
39954010
"rail buttons defined. Setting moments to zero.",
39964011
UserWarning,
39974012
)
3998-
null_moment = Function(0)
3999-
self.rail_button1_bending_moment = null_moment
4000-
self.rail_button2_bending_moment = null_moment
4001-
self.max_rail_button1_bending_moment = 0.0
4002-
self.max_rail_button2_bending_moment = 0.0
4003-
return
4004-
#distance between points
4005-
button1_pos = self.rocket.rail_buttons.component_coordinate_system_orientation * (self.rocket.rail_buttons.position)
4006-
button2_pos = button1_pos + self.rocket.rail_buttons.buttons_distance
4007-
d1 = abs(button1_pos - self.rocket.center_of_mass_without_motor)
4008-
d2 = abs(button2_pos - self.rocket.center_of_mass_without_motor)
4009-
L = d1 + d2 # Distance between buttons
4010-
h_button = self.rocket.rail_buttons.button_height #rail button height
4011-
#forces
4013+
return (null_moment, 0.0, null_moment, 0.0)
4014+
4015+
# Get rail button geometry
4016+
rail_buttons_tuple = self.rocket.rail_buttons[0]
4017+
upper_button_position = (
4018+
rail_buttons_tuple.component.buttons_distance
4019+
+ rail_buttons_tuple.position.z
4020+
)
4021+
lower_button_position = rail_buttons_tuple.position.z
4022+
4023+
# Signed distances from buttons to center of dry mass
4024+
D1 = upper_button_position - self.rocket.center_of_dry_mass_position(
4025+
self.rocket._csys
4026+
)
4027+
D2 = lower_button_position - self.rocket.center_of_dry_mass_position(
4028+
self.rocket._csys
4029+
)
4030+
d1 = abs(D1)
4031+
d2 = abs(D2)
4032+
4033+
# Rail button standoff height
4034+
h_button = rail_buttons_tuple.component.button_height
4035+
4036+
# forces
40124037
N1 = self.rail_button1_normal_force
40134038
N2 = self.rail_button2_normal_force
40144039
S1 = self.rail_button1_shear_force
40154040
S2 = self.rail_button2_shear_force
40164041
t = N1.source[:, 0]
4017-
# Main bending from opposing button + additional moment from shear at tip
4018-
M1_values = N2.source[:, 1] * L + S1.source[:, 1] * h_button
4019-
M2_values = N1.source[:, 1] * L + S2.source[:, 1] * h_button
4020-
self.rail_button1_bending_moment = Function(
4021-
np.column_stack([t, M1_values]),
4042+
4043+
# Calculate bending moments at attachment points
4044+
# Primary contribution from shear force acting at button height
4045+
# Secondary contribution from normal force creating moment about attachment
4046+
m1_values = N2.source[:, 1] * d2 + S1.source[:, 1] * h_button
4047+
m2_values = N1.source[:, 1] * d1 + S2.source[:, 1] * h_button
4048+
4049+
rail_button1_bending_moment = Function(
4050+
np.column_stack([t, m1_values]),
40224051
inputs="Time (s)",
40234052
outputs="Bending Moment (N·m)",
40244053
interpolation="linear",
40254054
)
4026-
self.rail_button2_bending_moment = Function(
4027-
np.column_stack([t, M2_values]),
4055+
rail_button2_bending_moment = Function(
4056+
np.column_stack([t, m2_values]),
40284057
inputs="Time (s)",
40294058
outputs="Bending Moment (N·m)",
40304059
interpolation="linear",
40314060
)
4032-
# Maximum bending moments in terms of absolute value for stress calculations
4033-
self.max_rail_button1_bending_moment = float(np.max(np.abs(M1_values)))
4034-
self.max_rail_button2_bending_moment = float(np.max(np.abs(M2_values)))
4061+
4062+
# Maximum bending moments (absolute value for stress calculations)
4063+
max_rail_button1_bending_moment = float(np.max(np.abs(m1_values)))
4064+
max_rail_button2_bending_moment = float(np.max(np.abs(m2_values)))
4065+
40354066
return (
4036-
self.rail_button1_bending_moment,
4037-
self.max_rail_button1_bending_moment,
4038-
self.rail_button2_bending_moment,
4039-
self.max_rail_button2_bending_moment,
4067+
rail_button1_bending_moment,
4068+
max_rail_button1_bending_moment,
4069+
rail_button2_bending_moment,
4070+
max_rail_button2_bending_moment,
40404071
)
4072+
40414073
@property
40424074
def rail_button1_bending_moment(self):
40434075
"""Upper rail button bending moment as a Function of time."""
@@ -4056,4 +4088,4 @@ def rail_button2_bending_moment(self):
40564088
@property
40574089
def max_rail_button2_bending_moment(self):
40584090
"""Maximum lower rail button bending moment, in N·m."""
4059-
return self.calculate_rail_button_bending_moments[3]
4091+
return self.calculate_rail_button_bending_moments[3]
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Unit tests for RailButtons bending moment formulas and calculation logic.
2+
3+
4+
These tests focus on verifying the mathematical correctness and realism
5+
of the calculate_rail_button_bending_moments method in isolation.
6+
"""
7+
8+
import warnings
9+
10+
import numpy as np
11+
12+
from rocketpy.mathutils.function import Function
13+
from rocketpy.rocket.aero_surface.rail_buttons import RailButtons
14+
15+
16+
def test_bending_moment_zero_without_rail_buttons():
17+
"""Verify bending moments return zero functions if no rail buttons present."""
18+
19+
class NoRailButtonsMock:
20+
"""Mock Flight class with no rail buttons for testing zero moment case."""
21+
22+
def __init__(self):
23+
self.rocket = type("", (), {})()
24+
self.rocket.rail_buttons = []
25+
self.rocket.center_of_dry_mass_position = lambda x: 0
26+
self.rocket._csys = 1
27+
28+
def calculate_rail_button_bending_moments(self):
29+
null_moment = Function(0)
30+
if len(self.rocket.rail_buttons) == 0:
31+
warnings.warn(
32+
"Trying to calculate rail button bending moments without "
33+
"rail buttons defined. Setting moments to zero.",
34+
UserWarning,
35+
)
36+
return (null_moment, 0.0, null_moment, 0.0)
37+
38+
flight = NoRailButtonsMock()
39+
moments = flight.calculate_rail_button_bending_moments()
40+
41+
m1_func, max_m1, m2_func, max_m2 = moments
42+
43+
# Verify types
44+
assert isinstance(m1_func, Function)
45+
assert isinstance(m2_func, Function)
46+
assert isinstance(max_m1, float)
47+
assert isinstance(max_m2, float)
48+
49+
# Verify zero functions - check first few values instead of full source
50+
assert m1_func(0) == 0
51+
assert m1_func(1) == 0
52+
assert m2_func(0) == 0
53+
assert m2_func(1) == 0
54+
assert max_m1 == 0.0
55+
assert max_m2 == 0.0
56+
57+
58+
def test_railbuttons_serialization_roundtrip():
59+
"""Test RailButtons to_dict and from_dict serialization roundtrip."""
60+
rb_orig = RailButtons(
61+
buttons_distance=0.7,
62+
angular_position=60,
63+
button_height=0.02,
64+
name="Test Rail Buttons",
65+
rocket_radius=0.045,
66+
)
67+
68+
data = rb_orig.to_dict()
69+
rb_reconstructed = RailButtons.from_dict(data)
70+
71+
assert rb_reconstructed.buttons_distance == rb_orig.buttons_distance
72+
assert rb_reconstructed.angular_position == rb_orig.angular_position
73+
assert rb_reconstructed.button_height == rb_orig.button_height
74+
assert rb_reconstructed.name == rb_orig.name
75+
assert rb_reconstructed.rocket_radius == rb_orig.rocket_radius
76+
77+
78+
def test_railbuttons_serialization_backward_compat():
79+
"""Test backward compatibility when button_height is missing from dict."""
80+
# Simulate old serialized data without button_height
81+
old_data = {
82+
"buttons_distance": 0.5,
83+
"angular_position": 45,
84+
"name": "Legacy Buttons",
85+
"rocket_radius": 0.05,
86+
}
87+
88+
rb = RailButtons.from_dict(old_data)
89+
assert rb.button_height == 0.015 # Should use default
90+
91+
92+
def test_railbuttons_angular_position_rad_property():
93+
"""Test angular_position_rad property calculation."""
94+
rb = RailButtons(buttons_distance=0.5, angular_position=30)
95+
expected_rad = np.radians(30)
96+
assert np.isclose(rb.angular_position_rad, expected_rad, rtol=1e-10)
97+
98+
99+
def test_railbuttons_no_aero_contribution():
100+
"""Test RailButtons provide zero aerodynamic contributions."""
101+
rb = RailButtons(buttons_distance=0.5)
102+
103+
rb.evaluate_center_of_pressure()
104+
assert rb.cp == (0, 0, 0)
105+
106+
rb.evaluate_lift_coefficient()
107+
assert rb.clalpha(1.0) == 0 # Zero lift derivative
108+
assert rb.cl(0.1, 1.0) == 0 # Zero lift coefficient

0 commit comments

Comments
 (0)