Source code for RCAIDE.Library.Methods.Powertrain.Converters.Rotor.design_propeller

# RCAIDE/Methods/Energy/Propulsors/design_propeller.py
# 
# 
# Created:  Jul 2023, M. Clarke 

# ----------------------------------------------------------------------------------------------------------------------
#  IMPORT
# ----------------------------------------------------------------------------------------------------------------------  
import RCAIDE
from RCAIDE.Framework.Core   import interp2d
from RCAIDE.Library.Methods.Geometry.Airfoil    import compute_airfoil_properties, compute_naca_4series, import_airfoil_geometry

# package imports 
import numpy as np
import scipy as sp 
from scipy.optimize import root 

# ----------------------------------------------------------------------------------------------------------------------  
#  Design Propeller
# ----------------------------------------------------------------------------------------------------------------------   
[docs] def design_propeller(prop, number_of_stations=20): """ Optimizes propeller chord and twist distribution given input parameters. Parameters ---------- prop : RCAIDE.Library.Components.Powertrain.Converters.Rotor Propeller component with the following attributes: - fidelity : str Analysis fidelity level - number_of_blades : int Number of blades on the propeller - tip_radius : float Tip radius of the propeller [m] - hub_radius : float Hub radius of the propeller [m] - cruise : Data Cruise conditions - design_angular_velocity : float Rotation rate [rad/s] - design_freestream_velocity : float Freestream velocity [m/s] - design_Cl : float Design lift coefficient - design_altitude : float Design altitude [m] - design_thrust : float, optional Design thrust [N] (specify either thrust or power) - design_power : float, optional Design power [W] (specify either thrust or power) - airfoils : dict Dictionary of airfoil objects - airfoil_polar_stations : list Indices of airfoil polars for each radial station number_of_stations : int, optional Number of radial stations for blade discretization, default 20 Returns ------- prop : RCAIDE.Library.Components.Powertrain.Converters.Rotor Propeller with optimized parameters: - cruise.design_power : float Design power [W] - cruise.design_thrust : float Design thrust [N] - cruise.design_torque : float Design torque [N·m] - max_thickness_distribution : array_like Maximum thickness distribution [m] - twist_distribution : array_like Blade twist distribution [rad] - chord_distribution : array_like Blade chord distribution [m] - radius_distribution : array_like Radial station positions [m] - cruise.design_power_coefficient : float Design power coefficient - cruise.design_thrust_coefficient : float Design thrust coefficient - mid_chord_alignment : array_like Mid-chord alignment [m] - thickness_to_chord : array_like Thickness-to-chord ratio - blade_solidity : float Blade solidity Notes ----- This function implements the design methodology from "Design of Optimum Propellers" by Adkins and Liebeck to optimize propeller chord and twist distributions. It iteratively solves for the optimal circulation distribution that minimizes induced losses. The design process follows these steps: 1. Calculate atmospheric properties at the design altitude 2. Compute non-dimensional thrust or power coefficient 3. Initialize the wake skew angle (zeta) 4. Iteratively solve for the optimal circulation distribution: a. Compute the Prandtl momentum loss factor b. Determine the product of relative velocity and chord (Wc) c. Calculate Reynolds number and Mach number at each station d. Compute optimal angle of attack and drag coefficient e. Calculate the efficiency parameter (epsilon = Cd/Cl) f. Determine axial and tangential induction factors g. Compute chord and blade twist angle h. Calculate derivatives for thrust and power integrals i. Update the wake skew angle (zeta) 5. Calculate final performance parameters (thrust, power, efficiency) 6. Compute thickness distribution and blade solidity **Major Assumptions** * Either design thrust or design power must be specified (not both) * The design is optimized for a single operating condition * Airfoil performance is based on 2D characteristics * The wake is modeled with a constant skew angle **Theory** The method is based on the classical blade element momentum theory with modifications to account for wake rotation. The key parameter in the optimization is the wake skew angle (zeta), which relates to the induced velocities. The optimal circulation distribution minimizes induced losses while satisfying the thrust or power constraint. The method iteratively solves for this distribution by updating the wake skew angle until convergence. References ---------- [1] Adkins, C.N. and Liebeck, R.H., "Design of Optimum Propellers", Journal of Propulsion and Power, Vol. 10, No. 5, 1994, pp. 676-682 See Also -------- RCAIDE.Library.Methods.Geometry.Airfoil.compute_airfoil_properties RCAIDE.Library.Methods.Geometry.Airfoil.compute_naca_4series RCAIDE.Library.Methods.Geometry.Airfoil.import_airfoil_geometry """ if prop.fidelity == 'Blade_Element_Momentum_Theory_Helmholtz_Wake': # Unpack N = number_of_stations # this number determines the discretization of the propeller into stations B = prop.number_of_blades R = prop.tip_radius Rh = prop.hub_radius omega = prop.cruise.design_angular_velocity # Rotation Rate in rad/s V = prop.cruise.design_freestream_velocity # Freestream Velocity Cl = prop.cruise.design_Cl # Design Lift Coefficient alt = prop.cruise.design_altitude Thrust = prop.cruise.design_thrust Power = prop.cruise.design_power airfoils = prop.airfoils a_loc = prop.airfoil_polar_stations if (Thrust == None) and (Power== None): raise AssertionError('Specify either design thrust or design power!') elif (Thrust!= None) and (Power!= None): raise AssertionError('Specify either design thrust or design power!') if V == 0.0: V = 1E-6 # Calculate atmospheric properties atmosphere = RCAIDE.Framework.Analyses.Atmospheric.US_Standard_1976() atmo_data = atmosphere.compute_values(alt) T = atmo_data.temperature[0] rho = atmo_data.density[0] speed_of_sound = atmo_data.speed_of_sound[0] mu = atmo_data.dynamic_viscosity[0] nu = mu/rho # Nondimensional thrust if (Thrust!= None) and (Power == None): Tc = 2.*Thrust/(rho*(V*V)*np.pi*(R*R)) Pc = 0.0 elif (Thrust== None) and (Power != None): Tc = 0.0 Pc = 2.*Power/(rho*(V*V*V)*np.pi*(R*R)) tol = 1e-10 # Convergence tolerance # Step 1, assume a zeta zeta = 0.1 # Assume to be small initially # Step 2, determine F and phi at each blade station chi0 = Rh/R # Where the propeller blade actually starts chi = np.linspace(chi0,1,N+1) # Vector of nondimensional radii chi = chi[0:N] lamda = V/(omega*R) # Speed ratio r = chi*R # Radial coordinate x = omega*r/V # Nondimensional distance diff = 1.0 # Difference between zetas n = omega/(2*np.pi) # Cycles per second D = 2.*R c = 0.2 * np.ones_like(chi) # if user defines airfoil, check dimension of stations num_airfoils = len(airfoils.keys()) if num_airfoils>0: if len(a_loc) != N: raise AssertionError('\nDimension of airfoil sections must be equal to number of stations on propeller') for _,airfoil in enumerate(airfoils): if airfoil.geometry == None: # first, if airfoil geometry data not defined, import from geoemtry files if type(airfoil) == RCAIDE.Library.Components.Airfoils.NACA_4_Series_Airfoil: # check if naca 4 series of airfoil from datafile airfoil.geometry = compute_naca_4series(airfoil.NACA_4_Series_code,airfoil.number_of_points) else: airfoil.geometry = import_airfoil_geometry(airfoil.coordinate_file,airfoil.number_of_points) if airfoil.polars == None: # compute airfoil polars for airfoils airfoil.polars = compute_airfoil_properties(airfoil.geometry, airfoil_polar_files= airfoil.polar_files) else: print('\nDefaulting to scaled DAE51') while diff>tol: # assign chord distribution prop.chord_distribution = c #Things that need a loop Tcnew = Tc tanphit = lamda*(1.+zeta/2.) # Tangent of the flow angle at the tip phit = np.arctan(tanphit) # Flow angle at the tip tanphi = tanphit/chi # Flow angle at every station f = (B/2.)*(1.-chi)/np.sin(phit) F = (2./np.pi)*np.arccos(np.exp(-f)) #Prandtl momentum loss factor phi = np.arctan(tanphi) # Flow angle at every station #Step 3, determine the product Wc, and RE G = F*x*np.cos(phi)*np.sin(phi) #Circulation function Wc = 4.*np.pi*lamda*G*V*R*zeta/(Cl*B) Ma = Wc/speed_of_sound RE = Wc/nu if num_airfoils>0: # assign initial values alpha0 = np.ones(N)*0.05 # solve for optimal alpha to meet design Cl target sol = root(objective, x0 = alpha0 , args=(airfoils,a_loc,RE,Cl,N)) alpha = sol.x # query surrogate for sectional Cls at stations Cdval = np.zeros_like(RE) for j,airfoil in enumerate(airfoils): pd = airfoil.polars Cdval_af = interp2d(RE,alpha,pd.reynolds_numbers, pd.angle_of_attacks, pd.drag_coefficients) locs = np.where(np.array(a_loc) == j ) Cdval[locs] = Cdval_af[locs] else: Cdval = (0.108*(Cl**4)-0.2612*(Cl**3)+0.181*(Cl**2)-0.0139*Cl+0.0278)*((50000./RE)**0.2) alpha = Cl/(2.*np.pi) #More Cd scaling from Mach from AA241ab notes for turbulent skin friction Tw_Tinf = 1. + 1.78*(Ma**2) Tp_Tinf = 1. + 0.035*(Ma**2) + 0.45*(Tw_Tinf-1.) Tp = Tp_Tinf*T Rp_Rinf = (Tp_Tinf**2.5)*(Tp+110.4)/(T+110.4) Cd = ((1/Tp_Tinf)*(1/Rp_Rinf)**0.2)*Cdval #Step 5, change Cl and repeat steps 3 and 4 until epsilon is minimized epsilon = Cd/Cl #Step 6, determine a and a', and W a = (zeta/2.)*(np.cos(phi)**2.)*(1.-epsilon*np.tan(phi)) W = V*(1.+a)/np.sin(phi) #Step 7, compute the chord length and blade twist angle c = Wc/W beta = alpha + phi # Blade twist angle #Step 8, determine 4 derivatives in I and J Iprime1 = 4.*chi*G*(1.-epsilon*np.tan(phi)) Iprime2 = lamda*(Iprime1/(2.*chi))*(1.+epsilon/np.tan(phi) )*np.sin(phi)*np.cos(phi) Jprime1 = 4.*chi*G*(1.+epsilon/np.tan(phi)) Jprime2 = (Jprime1/2.)*(1.-epsilon*np.tan(phi))*(np.cos(phi)**2.) dchi = (chi[1]-chi[0])*np.ones_like(Jprime1) #Integrate derivatives from chi=chi0 to chi=1 I1 = np.dot(Iprime1,dchi) I2 = np.dot(Iprime2,dchi) J1 = np.dot(Jprime1,dchi) J2 = np.dot(Jprime2,dchi) #Step 9, determine zeta and and Pc or zeta and Tc if (Pc==0.)&(Tc!=0.): #First Case, Thrust is given #Check to see if Tc is feasible, otherwise try a reasonable number if Tcnew>=I2*(I1/(2.*I2))**2.: Tcnew = I2*(I1/(2.*I2))**2. zetan = (I1/(2.*I2)) - ((I1/(2.*I2))**2.-Tcnew/I2)**0.5 elif (Pc!=0.)&(Tc==0.): #Second Case, Thrust is given zetan = -(J1/(J2*2.)) + ((J1/(J2*2.))**2.+Pc/J2)**0.5 #Step 10, repeat starting at step 2 with the new zeta diff = abs(zeta-zetan) zeta = zetan # Step 11, determine propeller efficiency etc... if (Pc==0.)&(Tc!=0.): if Tcnew>=I2*(I1/(2.*I2))**2.: Tcnew = I2*(I1/(2.*I2))**2. print('Tc infeasible, reset to:') print(Tcnew) #First Case, Thrust is given zeta = (I1/(2.*I2)) - ((I1/(2.*I2))**2.-Tcnew/I2)**0.5 Pc = J1*zeta + J2*(zeta**2.) Tc = I1*zeta - I2*(zeta**2.) elif (Pc!=0.)&(Tc==0.): #Second Case, Thrust is given zeta = -(J1/(2.*J2)) + ((J1/(2.*J2))**2.+Pc/J2)**0.5 Tc = I1*zeta - I2*(zeta**2.) Pc = J1*zeta + J2*(zeta**2.) # Calculate mid-chord alignment angle, MCA # This is the distance from the mid chord to the line axis out of the center of the blade # In this case the 1/4 chords are all aligned MCA = c/4. - c[0]/4. Thrust = Tc*rho*(V**2)*np.pi*(R**2)/2 Power = Pc*rho*(V**3)*np.pi*(R**2)/2 Ct = Thrust/(rho*(n*n)*(D*D*D*D)) Cp = Power/(rho*(n*n*n)*(D*D*D*D*D)) # compute max thickness distribution t_max = np.zeros(N) t_c = np.zeros(N) if num_airfoils>0: for j,airfoil in enumerate(airfoils): a_geo = airfoil.geometry locs = np.where(np.array(a_loc) == j ) t_max[locs] = a_geo.max_thickness*c[locs] t_c[locs] = a_geo.thickness_to_chord else: c_blade = np.repeat(np.atleast_2d(np.linspace(0,1,N)),N, axis = 0)* np.repeat(np.atleast_2d(c).T,N, axis = 1) t = (5*c_blade)*(0.2969*np.sqrt(c_blade) - 0.1260*c_blade - 0.3516*(c_blade**2) + 0.2843*(c_blade**3) - 0.1015*(c_blade**4)) # local thickness distribution t_max = np.max(t,axis = 1) t_c = np.max(t,axis = 1) /c # Nondimensional thrust if prop.cruise.design_power == None: prop.cruise.design_power = Power[0] elif prop.cruise.design_thrust == None: prop.cruise.design_thrust = Thrust[0] # blade solidity r = chi*R blade_area = sp.integrate.cumulative_trapezoid(B*c, r-r[0]) sigma = blade_area[-1]/(np.pi*R**2) prop.cruise.design_torque = Power[0]/omega prop.max_thickness_distribution = t_max prop.twist_distribution = beta prop.chord_distribution = c prop.radius_distribution = r prop.number_of_blades = int(B) prop.cruise.design_power_coefficient = Cp prop.cruise.design_thrust_coefficient = Ct prop.mid_chord_alignment = MCA prop.thickness_to_chord = t_c prop.blade_solidity = sigma return prop
[docs] def objective(x,airfoils,a_loc,RE,Cl,N): # query surrogate for sectional Cls at stations Cl_vals = np.zeros(N) for j,airfoil in enumerate(airfoils): pd = airfoil.polars Cl_af = interp2d(RE,x,pd.reynolds_numbers, pd.angle_of_attacks, pd.lift_coefficients) locs = np.where(np.array(a_loc) == j ) Cl_vals[locs] = Cl_af[locs] # compute Cl residual Cl_residuals = Cl_vals - Cl return Cl_residuals