A NACA airfoil is an airfoil shape completely described by a series of digits following the word "NACA". The airfoil cross-section can be generated, and its properties calculated, by entering the parameters from the digits into a series of equations.
This notebook makes an interactive 4-digit NACA airfoil cross-section generator. 5- and 6-digit airfoils may be added in the future. I'm hoping that this notebook can also be used as a tutorial for using IPython widgets, so I'll detail everything step by step.
First, we import packages necessary for interactivity.
In [16]:
from IPython.html import widgets
from IPython.display import display
Then, we import numpy for helping us do the calculations, and matplotlib for plotting. %matplotlib inline is used to keep the render the plot on the page, as opposed to opening a separate window.
In [2]:
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
Next, we create the airfoil generation function. It is based completely on the Wikipedia page. The docstring and comments are self-documenting.
In [171]:
def make_airfoil(naca_number=None, thickness=None, chord_length=1000,
increment=1, plot_to_length=None, is_sharp=True,
show_legend=False, show_thickness=False):
"""Plot an airfoil.
Any airfoil with a 4-digit NACA number, or a symmetric airfoil with a
specific thickness, can be used. Either the NACA number or the thickness
can be used at any given time.
All other arguments are optional.
Args:
naca_number: A 4-digit NACA number, entered as a string.
thickness: The desired thickness of a symmetric airfoil. The thickest
part will be at 30% of the chord.
chord_length: The length of the chord. Default is 1000.
increment: The size of each step. Default is 1.
plot_to_length: The length to plot until.
is_sharp: Whether the trailing edge will have zero thickness. Default
is True.
show_legend: Whether to display the legend when the airfoil is
asymmetric. Default is False.
show_thickness: Whether to display the non-cambered profile when the
airfoil is asymmetric. Default is False.
"""
# We first make sure that either the NACA number or the thickness is being
# used. If both are being used at the same time, raise an error. If
# neither, then work with the assumption that thickness is zero.
if naca_number and thickness:
raise ValueError("Only one type of input is allowed.")
elif not naca_number and not thickness:
thickness = 0
# Set the maximum length plotted to the full chord length if the stop point
# is not defined. Also set the length to which we calculate.
if not plot_to_length:
plot_to_length = chord_length
calculate_to = min(plot_to_length, chord_length)
# Define the coefficients and powers used for the equation.
coeffs = [0.2969, -0.126, -0.3516, 0.2843,
-0.1036 if is_sharp else -0.1015]
powers = [0.5, 1, 2, 3, 4]
# Interpret the NACA number
if naca_number:
try:
if len(naca_number) != 4:
raise NotImplementedError("Only 4-digit NACA numbers are \
currently supported.")
max_camber = float(naca_number[0]) / 100 # Airfoil cambered if 0.
camber_position = float(naca_number[1]) / 10
relative_thickness = float(naca_number[2:]) / 100
except ValueError:
raise ValueError("Invalid NACA number")
except TypeError:
raise TypeError("Please input the NACA number as a string.")
thickness = relative_thickness * chord_length
is_symmetric = not max_camber
else: # Thickness was defined.
relative_thickness = thickness / chord_length
is_symmetric = True
# Set up the arrays to hold points.
xs = np.arange(0, calculate_to + increment, increment) # Centreline
x_tops = np.zeros_like(xs)
x_bots = np.zeros_like(xs)
y_tops = np.zeros_like(xs)
y_bots = np.zeros_like(xs)
if not is_symmetric:
camber_lines = np.zeros_like(xs)
if show_thickness:
half_thicknesses = np.zeros_like(xs)
for point, x in enumerate(xs):
relative_position = x / chord_length
half_thickness = 5 * relative_thickness * chord_length * \
sum(coeffs[i] * relative_position ** powers[i] for i in range(5))
# Go through the formula.
if is_symmetric:
x_top = x_bot = x
y_top = half_thickness
y_bot = -y_top
else:
if x < camber_position * chord_length:
camber_line = max_camber * (x / (camber_position**2)) * \
(2 * camber_position - relative_position)
slope = 2 * (max_camber / (camber_position**2)) * \
(camber_position - relative_position)
else:
camber_line = max_camber * \
((chord_length - x) / ((1 - camber_position)**2)) * \
(1 + relative_position - 2*camber_position)
slope = 2 * max_camber / ((1 - camber_position)**2) * \
(camber_position - relative_position)
angle = np.arctan(slope)
x_top = x - half_thickness * np.sin(angle)
x_bot = x + half_thickness * np.sin(angle)
y_top = camber_line + half_thickness * np.cos(angle)
y_bot = camber_line - half_thickness * np.cos(angle)
camber_lines[point] = camber_line
if show_thickness:
half_thicknesses[point] = half_thickness
x_tops[point] = x_top
x_bots[point] = x_bot
y_tops[point] = y_top
y_bots[point] = y_bot
# Plot the airfoil
plt.axis("equal")
plt.xlim([0, plot_to_length])
if naca_number:
plt.title("NACA {} Airfoil".format(naca_number))
else:
plt.title("Airfoil with thickness of {}".format(thickness))
plt.plot(x_tops, y_tops, "b")
plt.plot(x_bots, y_bots, "b")
# Allow showing thickness or the legend if airfoil is cambered.
if not is_symmetric:
plt.plot(xs, camber_lines, "r", label="Camber line")
if show_thickness:
plt.plot(xs, half_thicknesses, "g", label="Thickness")
if show_legend:
plt.legend(loc="best")
plt.show()
Now, we get to look at the interactivity.
I want to have a dropdown menu to select whether the user wants to define a NACA airfoil, or define an airfoil thickness, with the thickest point at 30% of the length of the airfoil. Changing the mode displays different information. In addition, if the airfoil is cambered, we allow the user to show the legend and the thickness. We also allow the user to specify whether they want to cut the airfoil at a particular position.
First, we create the input form as a container widget, and we create a variable to store the last known values of our parameters.
In [172]:
form = widgets.ContainerWidget()
last_value = {"naca_number": "0012", "thickness": 120, "plot_to_length": 1000}
Next, we create the dropdown menu, and the widgets allowing the user to write an arbitrary NACA number, or an arbitrary thickness.
We also create a function so that, when the dropdown selection changes, the following happens:
False.Finally, we register the callback to the function whenever input_types gets a new value.
I would love to be able to set the description of the widgets to something other than the parameter name. For now, though, make sure that they are identical.
In [192]:
input_types = widgets.DropdownWidget(values =
{"Specify NACA airfoil": "naca_number",
"Specify thickness": "thickness"},
value="naca_number")
# Set the NACA number visible by default.
naca_number = widgets.TextWidget(description="naca_number", value="0012")
naca_widgets = widgets.ContainerWidget(visible=True, children=[naca_number])
# And the thickness selection invisible by default.
thickness = widgets.FloatTextWidget(description="thickness")
thickness_widgets = widgets.ContainerWidget(visible=False,
children=[thickness])
def on_type_change(name, value):
global last_value
if value == "naca_number":
naca_widgets.visible = True
thickness_widgets.visible = False
last_value["thickness"] = thickness.value
thickness.value = 0
naca_number.value = last_value["naca_number"]
if value == "thickness":
naca_widgets.visible = False
thickness_widgets.visible = True
last_value["naca_number"] = naca_number.value
naca_number.value = ""
thickness.value = last_value["thickness"]
input_types.on_trait_change(on_type_change, "value")
Now, we make a slider in order to select the length to plot to. The slider should only be visible and in effect when the cut_plot checkbox is checked. If it is unchecked, the value is stored, and set to 0. The stored value is then recovered when cut_plot is checked again.
In [193]:
cut_plot = widgets.CheckboxWidget(description="cut_plot", value=False)
plot_to_length = widgets.IntSliderWidget(description="plot_to_length",
min=0, max=chord_length.value,
step=increment.value)
cut_plot_widgets = widgets.ContainerWidget(visible=False,
children=[plot_to_length])
def on_plot_cut_change(name, value):
global last_value
if value:
cut_plot_widgets.visible = True
plot_to_length.value = last_value["plot_to_length"]
else:
cut_plot_widgets.visible = False
last_value["plot_to_length"] = plot_to_length.value
plot_to_length.value = 0
cut_plot.on_trait_change(on_plot_cut_change, "value")
We want the maximum value of the plot_to_length slider to change with the chord length, and the step to change with the increment.
In [194]:
chord_length = widgets.FloatTextWidget(description="chord_length", value=1000)
def on_chord_length_change(name, value):
plot_to_length.max = value
chord_length.on_trait_change(on_chord_length_change, "value")
In [195]:
increment = widgets.FloatTextWidget(description="increment", value=1)
def on_increment_change(name, value):
plot_to_length.step = value
increment.on_trait_change(on_increment_change, "value")
Then, we make it so that options for showing the legend and thickness are only displayed when the airfoil is asymmetrical.
In [196]:
show_thickness = widgets.CheckboxWidget(description="show_thickness",
value=False)
show_legend = widgets.CheckboxWidget(description="show_legend", value=False)
plot_elements = widgets.ContainerWidget(visible=False,
children=[show_thickness, show_legend])
def on_naca_number_change(name, value):
global last_value
if value and value[0] != "0": # Cambered airfoil
plot_elements.visible = True
else:
plot_elements.visible = False
naca_number.on_trait_change(on_naca_number_change, "value")
We're almost done. Now, we add the is_sharp option, and create the form.
In [197]:
is_sharp = widgets.CheckboxWidget(description="is_sharp", value=True)
In [198]:
form.children = [input_types, naca_widgets, plot_elements,
thickness_widgets,
chord_length, increment, is_sharp,
cut_plot, cut_plot_widgets]
Now that all parameters are accounted for, we have to display the form. Next, we run the generation function once so we can see the plot. And finally, we use the interactive function to give functionality to the form.
In [199]:
display(form)
make_airfoil(naca_number="0012")
v = widgets.interactive(make_airfoil,
naca_number=naca_number,
thickness=thickness,
chord_length=chord_length,
plot_to_length=plot_to_length,
increment=increment,
is_sharp=is_sharp,
show_thickness=show_thickness,
show_legend=show_legend)
I enjoyed making this, and I hope you find it useful.