Skip to content

Commit e00a07e

Browse files
ENH: add tube-fin aerodynamic surface (#1144)
1 parent 364214b commit e00a07e

13 files changed

Lines changed: 684 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
TubeFins Class
2+
==============
3+
4+
.. autoclass:: rocketpy.TubeFins
5+
:members:

‎docs/reference/classes/aero_surfaces/index.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ AeroSurface Classes
1212
TrapezoidalFins
1313
EllipticalFins
1414
FreeFormFins
15+
TubeFins
1516
Fin
1617
TrapezoidalFin
1718
EllipticalFin
1819
FreeFormFin
1920
RailButtons
2021
AirBrakes
2122
GenericSurface
22-
LinearGenericSurface
23+
LinearGenericSurface
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
Tube Fin Aerodynamics
2+
=====================
3+
4+
RocketPy models a tube-fin set as a symmetric ring of uncanted ring airfoils.
5+
The implementation follows the preliminary tube-fin model in OpenRocket's
6+
`TubeFinSetCalc <https://github.com/openrocket/openrocket/blob/unstable/core/src/main/java/info/openrocket/core/aerodynamics/barrowman/TubeFinSetCalc.java>`_.
7+
The normal-force derivative is based on Ribner's analysis of a ring airfoil in
8+
nonaxial flow [1]_.
9+
10+
Geometry
11+
--------
12+
13+
Let :math:`n` be the number of tubes, :math:`R` the rocket-body radius,
14+
:math:`r_i` the tube inner radius, :math:`r_o` the tube outer radius, and
15+
:math:`L` the tube length. The tubes are distributed evenly around the rocket.
16+
The current implementation requires each tube to touch its two neighbors:
17+
18+
.. math::
19+
20+
r_o = R \frac{\sin(\pi / n)}{1 - \sin(\pi / n)}.
21+
22+
This constraint also places each tube against the rocket body. Configurations
23+
with fewer than three tubes, gaps between tubes, or overlapping tubes are
24+
rejected.
25+
26+
Normal Force
27+
------------
28+
29+
The ring-airfoil aspect ratio and its modified form are
30+
31+
.. math::
32+
33+
AR = \frac{2 r_i}{L}, \qquad AR' = \frac{2 AR}{\pi}.
34+
35+
For a rocket reference area :math:`A_{ref} = \pi R^2`, the normal-force
36+
coefficient derivative of the complete tube set is
37+
38+
.. math::
39+
40+
C_{N_\alpha} =
41+
\frac{n}{A_{ref}}
42+
2 \left(\frac{AR'}{1 + AR'}\right) \pi^2 r_i L.
43+
44+
RocketPy applies this derivative symmetrically for positive and negative
45+
angles of attack and caps the magnitude at 20 degrees:
46+
47+
.. math::
48+
49+
C_N(\alpha) = C_{N_\alpha}
50+
\operatorname{clip}\left(\alpha, -20^\circ, 20^\circ\right).
51+
52+
Center of Pressure
53+
------------------
54+
55+
For Mach numbers up to 0.5, the center of pressure is placed at the quarter
56+
chord:
57+
58+
.. math::
59+
60+
x_{CP} = \frac{L}{4}.
61+
62+
The position is measured from the tube leading edge. OpenRocket moves this
63+
position with Mach number above Mach 0.5; RocketPy does not yet implement that
64+
correction. Simulations that exceed Mach 0.5 should use aerodynamic data from a
65+
higher-fidelity source instead of this fixed-CP model.
66+
67+
Model Limits
68+
------------
69+
70+
The tube-fin surface contributes normal force and the corresponding pitch and
71+
yaw moments about its center of pressure. It does not calculate:
72+
73+
- friction or pressure drag from the tubes;
74+
- roll forcing or damping;
75+
- side-force or yaw behavior for asymmetric tube layouts;
76+
- tube cant; or
77+
- aerodynamic corrections for separated or overlapping tubes.
78+
79+
Represent tube-fin drag in the rocket's power-on and power-off drag curves.
80+
Use :class:`rocketpy.GenericSurface` when measured, wind-tunnel, or CFD
81+
coefficients are available outside the limits above.
82+
83+
References
84+
----------
85+
86+
.. [1] Ribner, H. S. "The Ring Airfoil in Nonaxial Flow." *Journal of the
87+
Aeronautical Sciences*, 14(9), 529--530, 1947.

‎docs/technical/index.rst‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ in their code.
1414
Equations of Motion v1 <equations_of_motion_v1.rst>
1515
Elliptical Fins <aerodynamics/elliptical_fins.rst>
1616
Individual Fin <aerodynamics/individual_fins.rst>
17+
Tube Fins <aerodynamics/tube_fins.rst>
1718
Roll Moment <aerodynamics/roll_equations.rst>
1819
Sensitivity Analysis <sensitivity.rst>
1920
References <references.rst>
2021

2122
This section is still a work in progress, however, and not everything is documented yet.
22-
If you have any questions, please contact the maintainers of RocketPy.
23+
If you have any questions, please contact the maintainers of RocketPy.

‎docs/user/aerodynamics/surfaces.rst‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ Aerodynamic Surfaces
77
This page provides an overview of the aerodynamic surfaces available in
88
RocketPy and explains how they connect to the rocket's simulation.
99

10-
RocketPy models the aerodynamic forces and moments generated by three types
11-
of surfaces: **nose cones**, **fins**, and **tails**. Each surface is
12-
defined by its geometric parameters and, optionally, by an airfoil profile.
13-
The aerodynamic coefficients are computed internally using the Barrowman
14-
method and are used during the flight simulation to evaluate the rocket's
15-
stability and control.
10+
RocketPy models the aerodynamic forces and moments generated by **nose
11+
cones**, **planar fins**, **tube fins**, and **tails**. Each surface is defined
12+
by its geometric parameters. Planar fins can also use a measured airfoil lift
13+
curve. The resulting aerodynamic coefficients are used during the flight
14+
simulation to evaluate the rocket's stability and control.
1615

1716
.. seealso::
1817

@@ -112,6 +111,10 @@ RocketPy distinguishes between two levels of fin definition:
112111
separately. Individual fins are useful for canards, asymmetric
113112
configurations, or when you need fine-grained control.
114113

114+
- **Tube-fin sets** (:class:`rocketpy.TubeFins`): This class defines a
115+
symmetric ring of cylindrical fins. Its normal-force derivative follows
116+
Ribner's ring-airfoil model rather than the planar-fin Barrowman equations.
117+
115118
.. seealso::
116119

117120
For the mathematical model of individual fins, including the moment
@@ -178,6 +181,31 @@ Parameters:
178181
- ``coordinates``: A list of ``(x, y)`` tuples defining the fin shape
179182
in the fin coordinate frame.
180183

184+
Tube Fins
185+
~~~~~~~~~
186+
187+
Tube fins are defined by the number and length of the tubes, their inner and
188+
outer radii, and the rocket-body radius at the mounting position. Add them to a
189+
rocket with :meth:`rocketpy.Rocket.add_tube_fins` or create a
190+
:class:`rocketpy.TubeFins` object and pass it to
191+
:meth:`rocketpy.Rocket.add_surfaces`.
192+
193+
The current implementation is a subsonic normal-force model with these
194+
limits:
195+
196+
- The center of pressure is fixed at one quarter of the tube length, measured
197+
from the leading edge. Use the model only through Mach 0.5.
198+
- Lift is capped at an absolute angle of attack of 20 degrees.
199+
- At least three uncanted tubes must be distributed evenly around the body.
200+
Every tube must touch the body and both adjacent tubes. Separated and
201+
overlapping tube layouts are rejected.
202+
- The model does not calculate tube-fin friction drag, pressure drag, roll,
203+
side force, or yaw. Include tube-fin drag in the rocket's power-on and
204+
power-off drag curves.
205+
206+
For the equations and geometry constraint, see
207+
:doc:`Tube Fin Aerodynamics </technical/aerodynamics/tube_fins>`.
208+
181209
Common Fin Set Parameters
182210
-------------------------
183211

@@ -260,6 +288,7 @@ Fins can be added to a rocket using the ``Rocket`` class methods:
260288
- :meth:`rocketpy.Rocket.add_trapezoidal_fins`
261289
- :meth:`rocketpy.Rocket.add_elliptical_fins`
262290
- :meth:`rocketpy.Rocket.add_free_form_fins`
291+
- :meth:`rocketpy.Rocket.add_tube_fins`
263292
- :meth:`rocketpy.Rocket.add_surfaces` (for individual fins)
264293

265294
Tail

‎rocketpy/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
Tail,
5353
TrapezoidalFin,
5454
TrapezoidalFins,
55+
TubeFins,
5556
)
5657
from .sensitivity import SensitivityModel
5758
from .sensors import Accelerometer, Barometer, GnssReceiver, Gyroscope

‎rocketpy/plots/aero_surface_plots.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,41 @@ def draw(self, *, filename=None):
842842
show_or_save_plot(filename)
843843

844844

845+
class _TubeFinsPlots(_AeroSurfacePlots):
846+
"""Class that contains all tube-fin plots."""
847+
848+
def draw(self, *, filename=None):
849+
"""Draw a side-view envelope of the tube-fin set."""
850+
axial, radial = self.aero_surface.shape_vec
851+
_, ax = plt.subplots()
852+
853+
ax.plot(axial, radial, color="#A60628", label="Tube-fin envelope")
854+
ax.plot(axial, -radial, color="#A60628")
855+
ax.plot(
856+
[0, self.aero_surface.length],
857+
[0, 0],
858+
color="#7A68A6",
859+
linestyle="--",
860+
label="Rocket centerline",
861+
)
862+
863+
cp_point = (self.aero_surface.cpz, 0)
864+
ax.scatter(*cp_point, label="Center of Pressure", color="red", zorder=10)
865+
ax.scatter(*cp_point, facecolors="none", edgecolors="red", s=300, zorder=10)
866+
867+
limit = self.aero_surface.rocket_radius + 2 * self.aero_surface.outer_radius
868+
ax.set_xlim(-0.02 * self.aero_surface.length, 1.02 * self.aero_surface.length)
869+
ax.set_ylim(-1.05 * limit, 1.05 * limit)
870+
ax.set_aspect("equal")
871+
ax.set_xlabel("Length (m)")
872+
ax.set_ylabel("Radius (m)")
873+
ax.set_title("Tube Fin Set Side-View Envelope")
874+
ax.grid(True, linestyle="--", linewidth=0.5)
875+
ax.legend(bbox_to_anchor=(1.05, 1.0), loc="upper left")
876+
plt.tight_layout()
877+
show_or_save_plot(filename)
878+
879+
845880
class _TailPlots(_AeroSurfacePlots):
846881
"""Class that contains all tail plots."""
847882

‎rocketpy/prints/aero_surface_prints.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,21 @@ class _FreeFormFinPrints(_FinPrints):
291291
"""Class that contains all free form fins prints."""
292292

293293

294+
class _TubeFinsPrints(_AeroSurfacePrints):
295+
"""Class that contains all tube-fin prints."""
296+
297+
def geometry(self):
298+
"""Print the geometric information of the tube-fin set."""
299+
print("Geometric information of the tube-fin set:")
300+
print("------------------------------------------")
301+
print(f"Number of tubes: {self.aero_surface.n}")
302+
print(f"Tube length: {self.aero_surface.length:.3f} m")
303+
print(f"Inner tube radius: {self.aero_surface.inner_radius:.3f} m")
304+
print(f"Outer tube radius: {self.aero_surface.outer_radius:.3f} m")
305+
print(f"Reference rocket radius: {self.aero_surface.rocket_radius:.3f} m")
306+
print(f"Ring-airfoil aspect ratio: {self.aero_surface.aspect_ratio:.3f}\n")
307+
308+
294309
class _TailPrints(_AeroSurfacePrints):
295310
"""Class that contains all tail prints."""
296311

‎rocketpy/rocket/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Tail,
1616
TrapezoidalFin,
1717
TrapezoidalFins,
18+
TubeFins,
1819
)
1920
from rocketpy.rocket.components import Components
2021
from rocketpy.rocket.parachute import Parachute

‎rocketpy/rocket/aero_surface/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
from rocketpy.rocket.aero_surface.nose_cone import NoseCone
1616
from rocketpy.rocket.aero_surface.rail_buttons import RailButtons
1717
from rocketpy.rocket.aero_surface.tail import Tail
18+
from rocketpy.rocket.aero_surface.tube_fins import TubeFins

0 commit comments

Comments
 (0)