Blade Aerodynamics

From BEMT, it is evident that the overall rotor performance is a cumulative performance of each section of the rotor blades. Indeed, real rotor performance does depend on the aerodynamic performance parameters (Cl, Cd, Cm)1 of the airfoil sections that make up the rotor blades. It is then helpful to revisit how these fundamental performance characterisitics of an airfoil vary with flow conditions encountered on a real rotor.

Airfoil Tables

Airfoil tables, as they are called in rotorcraft analyses jargon, refer to a complete database of airfoil performance parameters - Cl, Cd and Cm - over a range of Mach numbers and angles of attack. It is worth noting that some of the most popular softwares/codes used for analysing rotorcraft performance loads such as CAMRAD II, RCAS, CHARM, Dymore etc. use an airfoil tables-based solution strategy for rotor analyses. This means that the blade section aerodynamics is solved using these airfoil tables similar to the BEMT formulation. There are of course additional models that each of these codes implements in order to solve a high-fidelity formulation of the physical rotor system, but this should re-emphasise the relevance of BEMT analyses in obtaining the blade section loads. Such airfoil aerodynamic performance data can be generated either using CFD analyses or using experimental measurements. The former approach is usually adopted since it is the more economically cheaper option.

The airfoil performance over the whole spectrum of Mach numbers and angles of attack, that could possibly be encountered in flight, is contained within these airfoil tables. Typically, the Mach number range used is 0 to 1.0 and the angle of attack range used is -180° to 180°.

CFD

The figures below graphically show the data contained within a typical airfoil table dataset - in this case the airfoil tables used for analysis of the Bo 105 helicopter. The variation of airfoil Cl, Cd and Cm as a function of the angle of attack and Mach number for the NACA23012 are shown. These are based on the work of Ref. [AR19] 2.

import plotly.graph_objects as go
import glob
import matplotlib.pyplot as plt
import plotly.express as px
import pandas as pd
from plotly.graph_objects import Layout
import os
import plotly.io as pio

def pathformach(filepath=f"MCC0.00/"):
    all_files = glob.glob(filepath + "**/00_coeff00.dat")
    li = [pd.read_csv(filename, index_col=None, header=0, sep='\s+') for filename in all_files]
    df = pd.concat(li , axis=0, ignore_index=True)
    col_names = {'MACH':'M', 'ALPHA':'Alpha', 'C-lift':'Cl','C-drag': 'Cd', 'C-my':'Cm'}
    df.rename(columns=col_names, inplace=True)
    df.sort_values(['M', 'Alpha','Cd'], inplace=True)
    return(df)

def machplot(filepath,coef):
    df=pathformach(filepath)
    pio.templates.default = "simple_white"

    fig= px.line(df, x="Alpha", y=coef, color="M",color_discrete_sequence= px.colors.sequential.Plasma_r,)
    pio.templates.default = "simple_white"

    fig.update_layout(
    title= coef + " vs. Alpha",
    xaxis_title="Alpha [deg]", 
    yaxis_title= coef)
    fig.update_xaxes(range=[-15, 15])

    fig.update_traces(textposition='top center')
    fig.update_xaxes(showline=True, linewidth=1, linecolor='black',
                 mirror=False, showgrid=True, gridwidth=1, gridcolor='Gray', zeroline=True, 
                 zerolinewidth=2, zerolinecolor='black')
    fig.update_yaxes(showline=True, linewidth=1, linecolor='black', 
                 showgrid=True, gridwidth=0.5, gridcolor='Gray', mirror=False,zeroline=True,
                 zerolinewidth=1, zerolinecolor='Gray')
    return fig.show()

machplot(f"MCC0.00/","Cl")
machplot(f"MCC0.00/","Cd")
machplot(f"MCC0.00/","Cm")

Airfoil polars are generated for a limited range of interest where the rotor blade sections predominantly operate. This range is then extended to the range of angles of attack from -180° to 180° using empirical results. This is a valid excercise since conventional helicopter rotor blade sections, when they encounter such extreme operating conditions, generally do not encounter large dynamic pressure. So the rough estimate of airfoil characteristic provided by the empirical relations is acceptable.

import plotly.graph_objects as go
import glob
import matplotlib.pyplot as plt
import plotly.express as px
import pandas as pd
import numpy as np
from plotly.graph_objects import Layout

path = f"MCC0.00/Ma040"
all_files = glob.glob(path + "/00_coeff00.dat")
#all_files = glob.glob(path + "/**/**/*.dat")
li = [pd.read_csv(filename, index_col=None, header=0, sep='\s+') for filename in all_files]
df = pd.concat(li , axis=0, ignore_index=True)
df
col_names = {'MACH':'mach', 'ALPHA':'alpha', 'C-lift':'Cl','C-drag':'Cd', 'C-my':'Cm'}


df.rename(columns=col_names, inplace=True)
df.sort_values(['alpha'], inplace=True)

       
n=len(df)
a= df.iloc[0:1, :]['alpha']          
b= df.iloc[n-1:n, :]['alpha']
df2 = pd.read_csv('./alpha.csv', index_col=None, header=0, sep='\s+') 
alpha_0= -1.2 #degree

df2['alpha_dif']= (df2['alpha']- alpha_0)

def emp_C_l(x):
    return 1.175*np.sin(np.deg2rad(2*x))
def emp_C_m(x):
    return -0.5*np.sin(np.deg2rad(x))+0.11*np.sin(np.deg2rad(2*x))
def emp_C_d(x):
    return 1.135-1.05*np.cos(np.deg2rad(2*x))

df2['Cl']=df2['alpha_dif'].apply(emp_C_l)
df2['Cm']=df2['alpha_dif'].apply(emp_C_m)
df2['Cd']=df2['alpha_dif'].apply(emp_C_d)

df2.sort_values(['alpha'], inplace=True)
df2

mask = df2['alpha'] < int(a)
df3 = df2[mask]
mask2 = df2['alpha'] > int(b)  
df4 = df2[mask2]

layout = Layout(plot_bgcolor='rgba(0,0.5,0,0)')
fig = go.Figure(layout=layout)
fig.add_trace(go.Scatter(x=df3['alpha'], y=df3['Cl'],
                    mode='lines',
                    name='Empirical curve-fit', line=dict(color="#0000ff")))
fig.add_trace(go.Scatter(x=df4['alpha'], y=df4['Cl'],mode='lines', showlegend=False, marker=dict(color="#0000ff")))
fig.add_trace(go.Scatter(x=df['alpha'], y=df['Cl'],
                    mode='markers', name='CFD', line=dict(color="#0000ff")))

fig.update_layout(
title="Cl vs Alpha",
xaxis_title="Alpha [deg]", 
yaxis_title="Cl")
fig.update_xaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig.update_yaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='Gray')
fig.update_yaxes(showgrid=True, gridwidth=0.5, gridcolor='Gray')
fig.update_xaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig.update_yaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig.show()
layout = Layout(plot_bgcolor='rgba(0,0.5,0,0)')
fig2 = go.Figure(layout=layout)
fig2.add_trace(go.Scatter(x=df3['alpha'], y=df3['Cm'],
                    mode='lines',
                    name='Empirical curve-fit', line=dict(color="#0000ff")))
fig2.add_trace(go.Scatter(x=df4['alpha'], y=df4['Cm'],mode='lines', showlegend=False, marker=dict(color="#0000ff")))
fig2.add_trace(go.Scatter(x=df['alpha'], y=df['Cm'],
                    mode='markers', name='CFD', line=dict(color="#0000ff")))

fig2.update_layout(
title="Cm vs Alpha",
xaxis_title="Alpha [deg]", 
yaxis_title="Cm")
fig2.update_xaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig2.update_yaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig2.update_xaxes(showgrid=True, gridwidth=1, gridcolor='Gray')
fig2.update_yaxes(showgrid=True, gridwidth=0.5, gridcolor='Gray')
fig2.update_xaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig2.update_yaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig2.show()
layout = Layout(plot_bgcolor='rgba(0,0.5,0,0)')
fig3 = go.Figure(layout=layout)
fig3.add_trace(go.Scatter(x=df3['alpha'], y=df3['Cd'],
                    mode='lines',
                    name='Empirical curve-fit', line=dict(color="#0000ff")))
fig3.add_trace(go.Scatter(x=df4['alpha'], y=df4['Cd'],mode='lines', showlegend=False, marker=dict(color="#0000ff")))
fig3.add_trace(go.Scatter(x=df['alpha'], y=df['Cd'],
                    mode='markers', name='CFD', line=dict(color="#0000ff")))

fig3.update_layout(
title="Cd vs Alpha",
xaxis_title="Alpha [deg]", 
yaxis_title="Cd")
fig3.update_xaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig3.update_yaxes(showline=True, linewidth=2, linecolor='black', mirror=False)
fig3.update_xaxes(showgrid=True, gridwidth=1, gridcolor='Gray')
fig3.update_yaxes(showgrid=True, gridwidth=0.5, gridcolor='Gray')
fig3.update_xaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig3.update_yaxes(zeroline=True, zerolinewidth=1, zerolinecolor='Gray')
fig3.show()

The airfoil L/D provides a clearer picture of the range of flow parameters where the rotor blade sections would perform better. As a rule of thumb the goal is for each blade section to be operating at or close to the the angle of attack that leads to best L/D.

import pandas as pd
import glob
import os
import plotly.graph_objects as go
import plotly
import numpy as np
from scipy.interpolate import griddata

import plotly.express as px
import math


path = f"MCC0.00/"
all_files = glob.glob(path +"**/00_coeff00.dat")
#all_files = glob.glob(path + "/**/**/*.dat")
li = [pd.read_csv(filename, index_col=None, header=0, sep='\s+') for filename in all_files]
df = pd.concat(li , axis=0, ignore_index=True)

col_names = {'MACH':'mach', 'ALPHA':'alpha', 'C-lift':'c_lift','C-drag':'c_drag', 'C-my':'c_my'}
df.rename(columns=col_names, inplace=True)
df['l_over_d'] = df['c_lift']/df['c_drag']
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_colwidth', None)

S = 100
N = int(len(df)/S)
frames = [ df.iloc[i*S:(i+1)*S].copy() for i in range(N)]
df_new=pd.DataFrame()
z=[frames[i]['l_over_d'].idxmax() for i in range(len(frames))]
df_2 = df.iloc[df.index.isin(z)]
df_2 = df_2.sort_values(by=['mach'])

xi = np.linspace(min(df['mach']), max(df['mach']), num=500)
yi = np.linspace(min(df['alpha']), max(df['alpha']), num=500)
x_grid, y_grid = np.meshgrid(xi,yi)
z_grid = griddata((df['mach'],df['alpha']),df['l_over_d'],(x_grid,y_grid),method='linear')
fig_line = go.Scatter3d(x=df_2['mach'], y=df_2['alpha'], z=df_2['l_over_d'], marker=dict(
        size=3,
        color="darkblue"
    ))
fig = go.Figure(data=[go.Surface(x=x_grid,y=y_grid,z=z_grid,
                       colorscale='viridis', opacity=.8),fig_line])
fig.update_layout(title='NACA23012', autosize=True,
                  margin=dict(l=65, r=50, b=65, t=90))
fig.update_layout(scene = dict(
                    xaxis_title='Mach Number',
                    yaxis_title='Alpha [deg]',
                    zaxis_title='L/D'),
                    width=700,
                    margin=dict(r=20, b=5, l=10, t=50))
fig.show()

Wind-tunnel measurements

In the wind-tunnel a wing with a constant airfoil cross-section is placed between the walls of a test section. The flow within the wind-tunnel section can be set at various Mach numbers and the wing section can be simultaneously set to different angles of attack to record Cl, Cd and Cm for a sweep of (Mach number,angle of attack) combinations. In case these results are desired for an airfoil section with a trailing-edge flap (TEF), for e.g., then Cl, Cd and Cm are recorded for a sweep of (Mach number,angle of attack, trailing-edge deflection angle) combinations.


1

The Plotly library used here for generating the plots is a great tool for interactively visualising data. However, it isn’t compatible with mathematical notation used to write down symbols. For this reason, the use of symbols on this page has been avoided.

2

These results were generated by solving the RANS equations using the CFD solver TAU by Amine Abdelmoula as part of his PhD.