# RCAIDE/Library/Methods/Geometry/Two_Dimensional/Airfoil/compute_airfoil_properties.py
#
#
# Created: Jul 2024, M. Clarke
# ----------------------------------------------------------------------------------------------------------------------
# IMPORT
# ----------------------------------------------------------------------------------------------------------------------
# RCAIDE imports
import RCAIDE
from RCAIDE.Framework.Core import Data , Units
from RCAIDE.Library.Methods.Aerodynamics.Airfoil_Panel_Method.airfoil_analysis import airfoil_analysis
from RCAIDE.Library.Methods.Geometry.Airfoil.import_airfoil_polars import import_airfoil_polars
from RCAIDE.Library.Methods.Geometry.Airfoil.compute_naca_4series import compute_naca_4series
from RCAIDE.Library.Methods.Aerodynamics.AERODAS.pre_stall_coefficients import pre_stall_coefficients
from RCAIDE.Library.Methods.Aerodynamics.AERODAS.post_stall_coefficients import post_stall_coefficients
# numpy imports
import numpy as np
from scipy.interpolate import RegularGridInterpolator
from RCAIDE.Framework.Core import interp2d
# ----------------------------------------------------------------------------------------------------------------------
# compute_airfoil_properties
# ----------------------------------------------------------------------------------------------------------------------
[docs]
def compute_airfoil_properties(airfoil_geometry, airfoil_polar_files = None,use_pre_stall_data=True):
"""This computes the aerodynamic properties and coefficients of an airfoil in stall regimes using pre-stall
characterstics and AERODAS formation for post stall characteristics. This is useful for
obtaining a more accurate prediction of wing and blade loading as well as aeroacoustics. Pre stall characteristics
are obtained in the form of a text file of airfoil polar data obtained from airfoiltools.com
Assumptions:
None
Source
None
Inputs:
airfoil_geometry <data_structure>
airfoil_polar_files <string>
boundary_layer_files <string>
use_pre_stall_data [Boolean]
Outputs:
airfoil_data.
cl_polars [unitless]
cd_polars [unitless]
aoa_sweep [unitless]
Properties Used:
N/A
"""
Airfoil_Data = Data()
# ----------------------------------------------------------------------------------------
# Compute airfoil boundary layers properties
# ----------------------------------------------------------------------------------------
Airfoil_Data = compute_boundary_layer_properties(airfoil_geometry,Airfoil_Data)
num_polars = len(Airfoil_Data.re_from_polar)
# ----------------------------------------------------------------------------------------
# Compute extended cl and cd polars
# ----------------------------------------------------------------------------------------
if airfoil_polar_files != None :
# check number of polars per airfoil in batch
num_polars = 0
n_p = len(airfoil_polar_files)
if n_p < 3:
raise AttributeError('Provide three or more airfoil polars to compute surrogate')
num_polars = max(num_polars, n_p)
# read in polars from files overwrite panel code
airfoil_file_data = import_airfoil_polars(airfoil_polar_files)
Airfoil_Data.aoa_from_polar = airfoil_file_data.aoa_from_polar
Airfoil_Data.re_from_polar = airfoil_file_data.re_from_polar
Airfoil_Data.lift_coefficients = airfoil_file_data.lift_coefficients
Airfoil_Data.drag_coefficients = airfoil_file_data.drag_coefficients
# Get all of the coefficients for AERODAS wings
AoA_sweep_deg = np.linspace(-14,90,105)
AoA_sweep_rad = AoA_sweep_deg*Units.degrees
# Create an infinite aspect ratio wing
geometry = RCAIDE.Library.Components.Wings.Wing()
geometry.aspect_ratio = np.inf
geometry.section = Data()
# AERODAS
CL = np.zeros((num_polars,len(AoA_sweep_deg)))
CD = np.zeros((num_polars,len(AoA_sweep_deg)))
for j in range(num_polars):
airfoil_cl = Airfoil_Data.lift_coefficients[j,:]
airfoil_cd = Airfoil_Data.drag_coefficients[j,:]
airfoil_aoa = Airfoil_Data.aoa_from_polar[j,:]/Units.degrees
# compute airfoil cl and cd for extended AoA range
CL[j,:],CD[j,:] = compute_extended_polars(airfoil_cl,airfoil_cd,airfoil_aoa,AoA_sweep_deg,geometry,use_pre_stall_data)
# ----------------------------------------------------------------------------------------
# Store data
# ----------------------------------------------------------------------------------------
Airfoil_Data.reynolds_numbers = Airfoil_Data.re_from_polar
Airfoil_Data.angle_of_attacks = AoA_sweep_rad
Airfoil_Data.lift_coefficients = CL
Airfoil_Data.drag_coefficients = CD
return Airfoil_Data
[docs]
def compute_extended_polars(airfoil_cl,airfoil_cd,airfoil_aoa,AoA_sweep_deg,geometry,use_pre_stall_data):
""" Computes the aerodynamic polars of an airfoil over an extended angle of attack range
Assumptions:
Uses AERODAS formulation for post stall characteristics
Source:
Models of Lift and Drag Coefficients of Stalled and Unstalled Airfoils in Wind Turbines and Wind Tunnels
by D Spera, 2008
Inputs:
airfoil_cl [unitless]
airfoil_cd [unitless]
airfoil_aoa [degrees]
AoA_sweep_deg [unitless]
geometry [N/A]
use_pre_stall_data [boolean]
Outputs:
CL [unitless]
CD [unitless]
Properties Used:
N/A
"""
# Create dummy settings and state
settings = Data()
state = Data()
state.conditions = Data()
state.conditions.aerodynamics = Data()
state.conditions.aerodynamics.pre_stall_coefficients = Data()
state.conditions.aerodynamics.post_stall_coefficients = Data()
# computing approximate zero lift aoa
AoA_sweep_radians = AoA_sweep_deg*Units.degrees
airfoil_cl_plus = airfoil_cl[airfoil_cl>0]
idx_zero_lift = np.where(airfoil_cl == min(airfoil_cl_plus))[0][0]
airfoil_cl_crossing = airfoil_cl[idx_zero_lift-1:idx_zero_lift+1]
airfoil_aoa_crossing = airfoil_aoa[idx_zero_lift-1:idx_zero_lift+1]
try:
A0 = np.interp(0,airfoil_cl_crossing, airfoil_aoa_crossing)* Units.deg
except:
A0 = airfoil_aoa[idx_zero_lift] * Units.deg
# max lift coefficent and associated aoa
CL1max = np.max(airfoil_cl)
idx_aoa_max_prestall_cl = np.where(airfoil_cl == CL1max)[0][0]
ACL1 = airfoil_aoa[idx_aoa_max_prestall_cl] * Units.degrees
# computing approximate lift curve slope
linear_idxs = [int(np.where(airfoil_aoa==0)[0]),int(np.where(airfoil_aoa==4)[0])]
cl_range = airfoil_cl[linear_idxs]
aoa_range = airfoil_aoa[linear_idxs] * Units.degrees
S1 = (cl_range[1]-cl_range[0])/(aoa_range[1]-aoa_range[0])
# max drag coefficent and associated aoa
CD1max = np.max(airfoil_cd)
idx_aoa_max_prestall_cd = np.where(airfoil_cd == CD1max)[0][0]
ACD1 = airfoil_aoa[idx_aoa_max_prestall_cd] * Units.degrees
# Find the point of lowest drag and the CD
idx_CD_min = np.where(airfoil_cd==min(airfoil_cd))[0][0]
ACDmin = airfoil_aoa[idx_CD_min] * Units.degrees
CDmin = airfoil_cd[idx_CD_min]
# Setup data structures for this run
ones = np.ones_like(AoA_sweep_radians)
settings.section_zero_lift_angle_of_attack = A0
state.conditions.aerodynamics.angle_of_attack = AoA_sweep_radians* ones
geometry.section.angle_attack_max_prestall_lift = ACL1 * ones
geometry.pre_stall_maximum_drag_coefficient_angle = ACD1 * ones
geometry.pre_stall_maximum_lift_coefficient = CL1max * ones
geometry.pre_stall_maximum_lift_drag_coefficient = CD1max * ones
geometry.section.minimum_drag_coefficient = CDmin * ones
geometry.section.minimum_drag_coefficient_angle_of_attack = ACDmin
geometry.pre_stall_lift_curve_slope = S1
# Get prestall coefficients
CL1, CD1 = pre_stall_coefficients(state,settings,geometry)
# Get poststall coefficents
CL2, CD2 = post_stall_coefficients(state,settings,geometry)
# Take the maxes
CL_ij = np.fmax(CL1,CL2)
CL_ij[AoA_sweep_radians<=A0] = np.fmin(CL1[AoA_sweep_radians<=A0],CL2[AoA_sweep_radians<=A0])
CD_ij = np.fmax(CD1,CD2)
# Pack this loop
CL = CL_ij
CD = CD_ij
if use_pre_stall_data == True:
CL, CD = apply_pre_stall_data(AoA_sweep_deg, airfoil_aoa, airfoil_cl, airfoil_cd, CL, CD)
return CL,CD
[docs]
def apply_pre_stall_data(AoA_sweep_deg, airfoil_aoa, airfoil_cl, airfoil_cd, CL, CD):
'''Applies prestall data to lift and drag curve slopes
Assumptions:
None
Source:
None
Inputs:
AoA_sweep_deg [degrees]
airfoil_aoa [degrees]
airfoil_cl [unitless]
airfoil_cd [unitless]
CL [unitless]
CD [unitless]
Outputs
CL [unitless]
CD [unitless]
Properties Used:
N/A
'''
# Coefficients in pre-stall regime taken from experimental data:
aoa_locs = (AoA_sweep_deg>=airfoil_aoa[0]) * (AoA_sweep_deg<=airfoil_aoa[-1])
aoa_in_data = AoA_sweep_deg[aoa_locs]
# if the data is within experimental use it, if not keep the surrogate values
CL[aoa_locs] = airfoil_cl[abs(aoa_in_data[:,None] - airfoil_aoa[None,:]).argmin(axis=-1)]
CD[aoa_locs] = airfoil_cd[abs(aoa_in_data[:,None] - airfoil_aoa[None,:]).argmin(axis=-1)]
# remove kinks/overlap between pre- and post-stall
data_lb = np.where(CD == airfoil_cd[0])[0][0]
data_ub = np.where(CD == airfoil_cd[-1])[0][-1]
CD[0:data_lb] = np.maximum(CD[0:data_lb], CD[data_lb]*np.ones_like(CD[0:data_lb]))
CD[data_ub:] = np.maximum(CD[data_ub:], CD[data_ub]*np.ones_like(CD[data_ub:]))
return CL, CD
[docs]
def compute_boundary_layer_properties(airfoil_geometry,Airfoil_Data):
'''Computes the boundary layer properties of an airfoil for a sweep of Reynolds numbers
and angle of attacks.
Source:
None
Assumptions:
None
Inputs:
airfoil_geometry <data_structure>
Airfoil_Data <data_structure>
Outputs:
Airfoil_Data <data_structure>
Properties Used:
N/A
'''
if airfoil_geometry == None:
print('No airfoil defined, NACA 0012 surrogates will be used')
a_names = ['0012']
airfoil_geometry = compute_naca_4series(a_names, npoints= 100)
AoA_sweep = np.array([-4,0,2,4,8,10,14])*Units.degrees
Re_sweep = np.array([1,5,10,30,50,75,100])*1E4
AoA_vals = np.tile(AoA_sweep[None,:],(len(Re_sweep) ,1))
Re_vals = np.tile(Re_sweep[:,None],(1, len(AoA_sweep)))
# run airfoil analysis
af_res = airfoil_analysis(airfoil_geometry,AoA_vals,Re_vals)
# store data
Airfoil_Data.aoa_from_polar = np.tile(AoA_sweep[None,:],(len(Re_sweep),1))
Airfoil_Data.re_from_polar = Re_sweep
Airfoil_Data.lift_coefficients = af_res.cl_invisc
Airfoil_Data.drag_coefficients = af_res.cd_invisc
Airfoil_Data.cm = af_res.cm_invisc
Airfoil_Data.boundary_layer = Data()
Airfoil_Data.boundary_layer.angle_of_attacks = AoA_sweep
Airfoil_Data.boundary_layer.reynolds_numbers = Re_sweep
fL = np.transpose(af_res.fL, axes=[1,2,0])
fD = np.transpose(af_res.fD, axes=[1,2,0])
Airfoil_Data.lift_distribution_func = RegularGridInterpolator((AoA_sweep,Re_sweep),fL,method = 'linear', bounds_error=False, fill_value=None)
Airfoil_Data.drag_distribution_func = RegularGridInterpolator((AoA_sweep,Re_sweep),fD,method = 'linear', bounds_error=False, fill_value=None)
Airfoil_Data.lift_distribution = fL
Airfoil_Data.drag_distribution = fD
return Airfoil_Data