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:

  • Make the corresponding item visible, and the other item invisible.
  • Store the old value of the invisible item.
  • Set the old value to something that generates a False.
  • Recover the old value of the current selection.

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.