import numpy as np
from rocketpy.mathutils.function import Function
from ..aero_surface import AeroSurface
[docs]
class Fins(AeroSurface):
"""Abstract class that holds common methods for the fin classes.
Cannot be instantiated.
Note
----
Local coordinate system:
- Origin located at the top of the root chord.
- Z axis along the longitudinal axis of symmetry, positive downwards (top -> bottom).
- Y axis perpendicular to the Z axis, in the span direction, positive upwards.
- X axis completes the right-handed coordinate system.
Attributes
----------
Fins.n : int
Number of fins in fin set.
Fins.rocket_radius : float
The reference rocket radius used for lift coefficient normalization,
in meters.
Fins.airfoil : tuple
Tuple of two items. First is the airfoil lift curve.
Second is the unit of the curve (radians or degrees).
Fins.cant_angle : float
Fins cant angle with respect to the rocket centerline, in degrees.
Fins.changing_attribute_dict : dict
Dictionary that stores the name and the values of the attributes that
may be changed during a simulation. Useful for control systems.
Fins.cant_angle_rad : float
Fins cant angle with respect to the rocket centerline, in radians.
Fins.root_chord : float
Fin root chord in meters.
Fins.tip_chord : float
Fin tip chord in meters.
Fins.span : float
Fin span in meters.
Fins.name : string
Name of fin set.
Fins.sweep_length : float
Fins sweep length in meters. By sweep length, understand the axial
distance between the fin root leading edge and the fin tip leading edge
measured parallel to the rocket centerline.
Fins.sweep_angle : float
Fins sweep angle with respect to the rocket centerline. Must
be given in degrees.
Fins.d : float
Reference diameter of the rocket. Has units of length and is given
in meters.
Fins.ref_area : float
Reference area of the rocket.
Fins.Af : float
Area of the longitudinal section of each fin in the set.
Fins.AR : float
Aspect ratio of each fin in the set.
Fins.gamma_c : float
Fin mid-chord sweep angle.
Fins.Yma : float
Span wise position of the mean aerodynamic chord.
Fins.roll_geometrical_constant : float
Geometrical constant used in roll calculations.
Fins.tau : float
Geometrical relation used to simplify lift and roll calculations.
Fins.lift_interference_factor : float
Factor of Fin-Body interference in the lift coefficient.
Fins.cp : tuple
Tuple with the x, y and z local coordinates of the fin set center of
pressure. Has units of length and is given in meters.
Fins.cpx : float
Fin set local center of pressure x coordinate. Has units of length and
is given in meters.
Fins.cpy : float
Fin set local center of pressure y coordinate. Has units of length and
is given in meters.
Fins.cpz : float
Fin set local center of pressure z coordinate. Has units of length and
is given in meters.
Fins.cl : Function
Function which defines the lift coefficient as a function of the angle
of attack and the Mach number. Takes as input the angle of attack in
radians and the Mach number. Returns the lift coefficient.
Fins.clalpha : float
Lift coefficient slope. Has units of 1/rad.
Fins.roll_parameters : list
List containing the roll moment lift coefficient, the roll moment
damping coefficient and the cant angle in radians.
"""
[docs]
def __init__(
self,
n,
root_chord,
span,
rocket_radius,
cant_angle=0,
airfoil=None,
name="Fins",
):
"""Initialize Fins class.
Parameters
----------
n : int
Number of fins, must be larger than 2.
root_chord : int, float
Fin root chord in meters.
span : int, float
Fin span in meters.
rocket_radius : int, float
Reference rocket radius used for lift coefficient normalization.
cant_angle : int, float, optional
Fins cant angle with respect to the rocket centerline. Must
be given in degrees.
airfoil : tuple, optional
Default is null, in which case fins will be treated as flat plates.
Otherwise, if tuple, fins will be considered as airfoils. The
tuple's first item specifies the airfoil's lift coefficient
by angle of attack and must be either a .csv, .txt, ndarray
or callable. The .csv and .txt files can contain a single line
header and the first column must specify the angle of attack, while
the second column must specify the lift coefficient. The
ndarray should be as [(x0, y0), (x1, y1), (x2, y2), ...]
where x0 is the angle of attack and y0 is the lift coefficient.
If callable, it should take an angle of attack as input and
return the lift coefficient at that angle of attack.
The tuple's second item is the unit of the angle of attack,
accepting either "radians" or "degrees".
name : str
Name of fin set.
Returns
-------
None
"""
# Compute auxiliary geometrical parameters
d = 2 * rocket_radius
ref_area = np.pi * rocket_radius**2 # Reference area
super().__init__(name, ref_area, d)
# Store values
self._n = n
self._rocket_radius = rocket_radius
self._airfoil = airfoil
self._cant_angle = cant_angle
self._root_chord = root_chord
self._span = span
self.name = name
self.d = d
self.ref_area = ref_area # Reference area
@property
def n(self):
return self._n
@n.setter
def n(self, value):
self._n = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
@property
def root_chord(self):
return self._root_chord
@root_chord.setter
def root_chord(self, value):
self._root_chord = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
@property
def span(self):
return self._span
@span.setter
def span(self, value):
self._span = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
@property
def rocket_radius(self):
return self._rocket_radius
@rocket_radius.setter
def rocket_radius(self, value):
self._rocket_radius = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
@property
def cant_angle(self):
return self._cant_angle
@cant_angle.setter
def cant_angle(self, value):
self._cant_angle = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
@property
def airfoil(self):
return self._airfoil
@airfoil.setter
def airfoil(self, value):
self._airfoil = value
self.evaluate_geometrical_parameters()
self.evaluate_center_of_pressure()
self.evaluate_lift_coefficient()
self.evaluate_roll_parameters()
[docs]
def evaluate_lift_coefficient(self):
"""Calculates and returns the fin set's lift coefficient.
The lift coefficient is saved and returned. This function
also calculates and saves the lift coefficient derivative
for a single fin and the lift coefficient derivative for
a number of n fins corrected for Fin-Body interference.
Returns
-------
None
"""
if not self.airfoil:
# Defines clalpha2D as 2*pi for planar fins
clalpha2D_incompressible = 2 * np.pi
else:
# Defines clalpha2D as the derivative of the lift coefficient curve
# for the specific airfoil
self.airfoil_cl = Function(
self.airfoil[0],
interpolation="linear",
)
# Differentiating at alpha = 0 to get cl_alpha
clalpha2D_incompressible = self.airfoil_cl.differentiate_complex_step(
x=1e-3, dx=1e-3
)
# Convert to radians if needed
if self.airfoil[1] == "degrees":
clalpha2D_incompressible *= 180 / np.pi
# Correcting for compressible flow (apply Prandtl-Glauert correction)
clalpha2D = Function(lambda mach: clalpha2D_incompressible / self._beta(mach))
# Diederich's Planform Correlation Parameter
planform_correlation_parameter = (
2 * np.pi * self.AR / (clalpha2D * np.cos(self.gamma_c))
)
# Lift coefficient derivative for a single fin
def lift_source(mach):
return (
clalpha2D(mach)
* planform_correlation_parameter(mach)
* (self.Af / self.ref_area)
* np.cos(self.gamma_c)
) / (
2
+ planform_correlation_parameter(mach)
* np.sqrt(1 + (2 / planform_correlation_parameter(mach)) ** 2)
)
self.clalpha_single_fin = Function(
lift_source,
"Mach",
"Lift coefficient derivative for a single fin",
)
# Lift coefficient derivative for n fins corrected with Fin-Body interference
self.clalpha_multiple_fins = (
self.lift_interference_factor
* self.fin_num_correction(self.n)
* self.clalpha_single_fin
) # Function of mach number
self.clalpha_multiple_fins.set_inputs("Mach")
self.clalpha_multiple_fins.set_outputs(
f"Lift coefficient derivative for {self.n:.0f} fins"
)
self.clalpha = self.clalpha_multiple_fins
# Cl = clalpha * alpha
self.cl = Function(
lambda alpha, mach: alpha * self.clalpha_multiple_fins(mach),
["Alpha (rad)", "Mach"],
"Lift coefficient",
)
return self.cl
[docs]
def evaluate_roll_parameters(self):
"""Calculates and returns the fin set's roll coefficients.
The roll coefficients are saved in a list.
Returns
-------
self.roll_parameters : list
List containing the roll moment lift coefficient, the
roll moment damping coefficient and the cant angle in
radians
"""
self.cant_angle_rad = np.radians(self.cant_angle)
clf_delta = (
self.roll_forcing_interference_factor
* self.n
* (self.Yma + self.rocket_radius)
* self.clalpha_single_fin
/ self.d
) # Function of mach number
clf_delta.set_inputs("Mach")
clf_delta.set_outputs("Roll moment forcing coefficient derivative")
clf_delta.set_title(None)
cld_omega = (
2
* self.roll_damping_interference_factor
* self.n
* self.clalpha_single_fin
* np.cos(self.cant_angle_rad)
* self.roll_geometrical_constant
/ (self.ref_area * self.d**2)
) # Function of mach number
cld_omega.set_inputs("Mach")
cld_omega.set_outputs("Roll moment damping coefficient derivative")
cld_omega.set_title(None)
self.roll_parameters = [clf_delta, cld_omega, self.cant_angle_rad]
return self.roll_parameters
[docs]
@staticmethod
def fin_num_correction(n):
"""Calculates a correction factor for the lift coefficient of multiple
fins.
The specifics values are documented at:
Niskanen, S. (2013). “OpenRocket technical documentation”.
In: Development of an Open Source model rocket simulation software.
Parameters
----------
n : int
Number of fins.
Returns
-------
Corrector factor : int
Factor that accounts for the number of fins.
"""
corrector_factor = [2.37, 2.74, 2.99, 3.24]
if 5 <= n <= 8:
return corrector_factor[n - 5]
else:
return n / 2
[docs]
def compute_forces_and_moments(
self,
stream_velocity,
stream_speed,
stream_mach,
rho,
cp,
omega,
*args,
): # pylint: disable=arguments-differ
"""Computes the forces and moments acting on the aerodynamic surface.
Parameters
----------
stream_velocity : tuple of float
The velocity of the airflow relative to the surface.
stream_speed : float
The magnitude of the airflow speed.
stream_mach : float
The Mach number of the airflow.
rho : float
Air density.
cp : Vector
Center of pressure coordinates in the body frame.
omega: tuple[float, float, float]
Tuple containing angular velocities around the x, y, z axes.
Returns
-------
tuple of float
The aerodynamic forces (lift, side_force, drag) and moments
(pitch, yaw, roll) in the body frame.
"""
R1, R2, R3, M1, M2, _ = super().compute_forces_and_moments(
stream_velocity,
stream_speed,
stream_mach,
rho,
cp,
)
clf_delta, cld_omega, cant_angle_rad = self.roll_parameters
M3_forcing = (
(1 / 2 * rho * stream_speed**2)
* self.reference_area
* self.reference_length
* clf_delta.get_value_opt(stream_mach)
* cant_angle_rad
)
M3_damping = (
(1 / 2 * rho * stream_speed)
* self.reference_area
* (self.reference_length) ** 2
* cld_omega.get_value_opt(stream_mach)
* omega[2]
/ 2
)
M3 = M3_forcing - M3_damping
return R1, R2, R3, M1, M2, M3
def to_dict(self, **kwargs):
if self.airfoil:
if kwargs.get("discretize", False):
lower = -np.pi / 6 if self.airfoil[1] == "radians" else -30
upper = np.pi / 6 if self.airfoil[1] == "radians" else 30
airfoil = (
self.airfoil_cl.set_discrete(lower, upper, 50, mutate_self=False),
self.airfoil[1],
)
else:
airfoil = (self.airfoil_cl, self.airfoil[1]) if self.airfoil else None
else:
airfoil = None
data = {
"n": self.n,
"root_chord": self.root_chord,
"span": self.span,
"rocket_radius": self.rocket_radius,
"cant_angle": self.cant_angle,
"airfoil": airfoil,
"name": self.name,
}
if kwargs.get("include_outputs", False):
cl = self.cl
if kwargs.get("discretize", False):
cl = cl.set_discrete(
(-np.pi / 6, 0), (np.pi / 6, 2), (10, 10), mutate_self=False
)
data.update(
{
"cp": self.cp,
"cl": cl,
"roll_parameters": self.roll_parameters,
"d": self.d,
"ref_area": self.ref_area,
}
)
return data
[docs]
def draw(self, *, filename=None):
"""Draw the fin shape along with some important information, including
the center line, the quarter line and the center of pressure position.
Parameters
----------
filename : str | None, optional
The path the plot should be saved to. By default None, in which case
the plot will be shown instead of saved. Supported file endings are:
eps, jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff
and webp (these are the formats supported by matplotlib).
Returns
-------
None
"""
self.plots.draw(filename=filename)