Skip to content

Commit 689abd0

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 14b9984 commit 689abd0

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
@@ -671,6 +671,88 @@ def evaluate_stability_margin(self):
671671
)
672672
return self.stability_margin
673673

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

rocketpy/simulation/flight.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3353,8 +3353,8 @@ def static_margin(self):
33533353
def stability_margin(self):
33543354
"""Stability margin of the rocket along the flight, it considers the
33553355
variation of the center of pressure position according to the mach
3356-
number, as well as the variation of the center of gravity position
3357-
according to the propellant mass evolution.
3356+
number and angle of attack, as well as the variation of the center of
3357+
gravity position according to the propellant mass evolution.
33583358
33593359
Parameters
33603360
----------
@@ -3366,8 +3366,26 @@ def stability_margin(self):
33663366
Stability margin as a rocketpy.Function of time. The stability margin
33673367
is defined as the distance between the center of pressure and the
33683368
center of gravity, divided by the rocket diameter.
3369+
3370+
Notes
3371+
-----
3372+
The center of pressure position is computed using the actual lift
3373+
coefficient CN(α) instead of the lift coefficient derivative CNα. This
3374+
provides a more accurate stability margin that varies with angle of
3375+
attack, similar to OpenRocket's implementation.
33693376
"""
3370-
return [(t, self.rocket.stability_margin(m, t)) for t, m in self.mach_number]
3377+
aoa_values = self.angle_of_attack.y_array
3378+
mach_values = self.mach_number.y_array
3379+
time_values = self.time
3380+
3381+
results = []
3382+
for i, t in enumerate(time_values):
3383+
alpha_rad = np.deg2rad(aoa_values[i])
3384+
mach = mach_values[i]
3385+
sm = self.rocket.get_stability_margin_from_alpha(alpha_rad, mach, t)
3386+
results.append((t, sm))
3387+
3388+
return results
33713389

33723390
# Rail Button Forces
33733391

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)