Skip to content

Commit f427024

Browse files
Implement angle-of-attack dependent stability margin
Add new methods to compute center of pressure and stability margin as a function of angle of attack (α) instead of just using the lift coefficient derivative. This provides a more accurate stability margin that varies with angle of attack, similar to OpenRocket. - Add Rocket.get_cp_position_from_alpha(alpha, mach) method - Add Rocket.get_stability_margin_from_alpha(alpha, mach, time) method - Update Flight.stability_margin to use angle of attack from simulation - Add tests for new functionality Co-authored-by: Gui-FernandesBR <[email protected]>
1 parent 251f09f commit f427024

File tree

3 files changed

+164
-3
lines changed

3 files changed

+164
-3
lines changed

rocketpy/rocket/rocket.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,88 @@ def evaluate_stability_margin(self):
663663
)
664664
return self.stability_margin
665665

666+
def get_cp_position_from_alpha(self, alpha, mach):
667+
"""Computes the center of pressure position as a function of the angle
668+
of attack and Mach number. This method uses the actual lift coefficient
669+
CN(α) instead of the lift coefficient derivative CNα, providing a more
670+
accurate CP position that varies with angle of attack.
671+
672+
Parameters
673+
----------
674+
alpha : float
675+
Angle of attack in radians.
676+
mach : float
677+
Mach number.
678+
679+
Returns
680+
-------
681+
float
682+
Center of pressure position relative to the user-defined rocket
683+
reference system, in meters.
684+
685+
Notes
686+
-----
687+
The center of pressure is calculated as:
688+
CP = Σ(CN(α) × X_cp) / Σ(CN(α))
689+
where CN(α) is the lift coefficient as a function of angle of attack
690+
and X_cp is the center of pressure position of each aerodynamic surface.
691+
"""
692+
# Handle edge case where alpha is effectively zero
693+
if abs(alpha) < 1e-10:
694+
return self.cp_position.get_value_opt(mach)
695+
696+
total_cn = 0.0
697+
weighted_cp = 0.0
698+
699+
for aero_surface, position in self.aerodynamic_surfaces:
700+
if isinstance(aero_surface, GenericSurface):
701+
continue
702+
703+
ref_factor = (aero_surface.rocket_radius / self.radius) ** 2
704+
cn = ref_factor * aero_surface.cl.get_value_opt(alpha, mach)
705+
surface_cp = position.z - self._csys * aero_surface.cpz
706+
707+
total_cn += cn
708+
weighted_cp += cn * surface_cp
709+
710+
if abs(total_cn) < 1e-10:
711+
return self.cp_position.get_value_opt(mach)
712+
713+
return weighted_cp / total_cn
714+
715+
def get_stability_margin_from_alpha(self, alpha, mach, time):
716+
"""Computes the stability margin using the angle of attack-dependent
717+
center of pressure position.
718+
719+
Parameters
720+
----------
721+
alpha : float
722+
Angle of attack in radians.
723+
mach : float
724+
Mach number.
725+
time : float
726+
Time in seconds.
727+
728+
Returns
729+
-------
730+
float
731+
Stability margin in calibers. A positive value indicates the rocket
732+
is stable (center of pressure is behind the center of mass).
733+
734+
Notes
735+
-----
736+
The stability margin is calculated as:
737+
SM = (CG - CP(α)) / d
738+
where CG is the center of gravity, CP(α) is the angle of attack-dependent
739+
center of pressure, and d is the rocket diameter.
740+
"""
741+
cp_position = self.get_cp_position_from_alpha(alpha, mach)
742+
return (
743+
(self.center_of_mass.get_value_opt(time) - cp_position)
744+
/ (2 * self.radius)
745+
* self._csys
746+
)
747+
666748
def evaluate_static_margin(self):
667749
"""Calculates the static margin of the rocket as a function of time.
668750

rocketpy/simulation/flight.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3100,8 +3100,8 @@ def static_margin(self):
31003100
def stability_margin(self):
31013101
"""Stability margin of the rocket along the flight, it considers the
31023102
variation of the center of pressure position according to the mach
3103-
number, as well as the variation of the center of gravity position
3104-
according to the propellant mass evolution.
3103+
number and angle of attack, as well as the variation of the center of
3104+
gravity position according to the propellant mass evolution.
31053105
31063106
Parameters
31073107
----------
@@ -3113,8 +3113,26 @@ def stability_margin(self):
31133113
Stability margin as a rocketpy.Function of time. The stability margin
31143114
is defined as the distance between the center of pressure and the
31153115
center of gravity, divided by the rocket diameter.
3116+
3117+
Notes
3118+
-----
3119+
The center of pressure position is computed using the actual lift
3120+
coefficient CN(α) instead of the lift coefficient derivative CNα. This
3121+
provides a more accurate stability margin that varies with angle of
3122+
attack, similar to OpenRocket's implementation.
31163123
"""
3117-
return [(t, self.rocket.stability_margin(m, t)) for t, m in self.mach_number]
3124+
aoa_values = self.angle_of_attack.y_array
3125+
mach_values = self.mach_number.y_array
3126+
time_values = self.time
3127+
3128+
results = []
3129+
for i, t in enumerate(time_values):
3130+
alpha_rad = np.deg2rad(aoa_values[i])
3131+
mach = mach_values[i]
3132+
sm = self.rocket.get_stability_margin_from_alpha(alpha_rad, mach, t)
3133+
results.append((t, sm))
3134+
3135+
return results
31183136

31193137
# Rail Button Forces
31203138

tests/unit/simulation/test_flight.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,67 @@ def test_out_of_rail_stability_margin(flight_calisto_custom_wind):
166166
assert np.isclose(res, 2.14, atol=0.1)
167167

168168

169+
def test_stability_margin_uses_angle_of_attack(flight_calisto_custom_wind):
170+
"""Test that the stability margin calculation accounts for angle of attack.
171+
172+
The stability margin should use the actual lift coefficient CN(α) instead of
173+
just the lift coefficient derivative CNα. This provides a more accurate
174+
stability margin that varies with angle of attack.
175+
176+
Parameters
177+
----------
178+
flight_calisto_custom_wind : rocketpy.Flight
179+
"""
180+
# Get stability margin at various times
181+
stability_array = flight_calisto_custom_wind.stability_margin[:, 1]
182+
time_array = flight_calisto_custom_wind.stability_margin[:, 0]
183+
184+
# Verify stability margin is a reasonable Function
185+
assert len(stability_array) == len(time_array)
186+
assert len(stability_array) > 0
187+
188+
# Verify the rocket's get_stability_margin_from_alpha method works
189+
rocket = flight_calisto_custom_wind.rocket
190+
alpha = np.deg2rad(5) # 5 degrees angle of attack
191+
mach = 0.5
192+
time = 0.0
193+
194+
sm_from_alpha = rocket.get_stability_margin_from_alpha(alpha, mach, time)
195+
sm_from_mach = rocket.stability_margin(mach, time)
196+
197+
# Both should return reasonable stability margins (positive for stable rocket)
198+
assert isinstance(sm_from_alpha, float)
199+
assert isinstance(sm_from_mach, float)
200+
assert sm_from_alpha > 0 # Should be stable
201+
assert sm_from_mach > 0 # Should be stable
202+
203+
204+
def test_cp_position_from_alpha_edge_cases(flight_calisto_custom_wind):
205+
"""Test edge cases for the get_cp_position_from_alpha method.
206+
207+
Parameters
208+
----------
209+
flight_calisto_custom_wind : rocketpy.Flight
210+
"""
211+
rocket = flight_calisto_custom_wind.rocket
212+
mach = 0.5
213+
214+
# Test with zero angle of attack - should fall back to standard cp_position
215+
cp_zero_alpha = rocket.get_cp_position_from_alpha(0.0, mach)
216+
cp_standard = rocket.cp_position.get_value_opt(mach)
217+
assert np.isclose(cp_zero_alpha, cp_standard)
218+
219+
# Test with small positive angle of attack
220+
alpha_small = np.deg2rad(1)
221+
cp_small = rocket.get_cp_position_from_alpha(alpha_small, mach)
222+
assert isinstance(cp_small, float)
223+
224+
# Test with larger angle of attack
225+
alpha_large = np.deg2rad(10)
226+
cp_large = rocket.get_cp_position_from_alpha(alpha_large, mach)
227+
assert isinstance(cp_large, float)
228+
229+
169230
def test_export_sensor_data(flight_calisto_with_sensors):
170231
"""Test the export of sensor data.
171232

0 commit comments

Comments
 (0)