def design_F8745D4_prop(): prop = SUAVE.Components.Energy.Converters.Propeller() prop.tag = 'F8745_D4_Propeller' prop.tip_radius = 2.03/2 prop.hub_radius = prop.tip_radius*0.20 prop.number_of_blades = 2 r_R_data = np.array([ 0.2,0.300,0.450,0.601,0.747,0.901,0.950,0.975,0.998 ]) t_c_data = np.array([ 0.3585,0.1976,0.1148,0.0834,0.0648,0.0591,0.0562,0.0542,0.0533 ]) b_R_data = np.array([0.116,0.143,0.163,0.169,0.166,0.148,0.135,0.113,0.075 ]) beta_data = np.array([ 0.362,0.286,0.216,0.170,0.135,0.112,0.105,0.101,0.098])* 100 dim = 20 new_radius_distribution = np.linspace(0.2,0.98 ,dim) func_twist_distribution = interp1d(r_R_data, (beta_data)* Units.degrees , kind='cubic') func_chord_distribution = interp1d(r_R_data, b_R_data * prop.tip_radius , kind='cubic') func_radius_distribution = interp1d(r_R_data, r_R_data * prop.tip_radius , kind='cubic') func_max_thickness_distribution = interp1d(r_R_data, t_c_data * b_R_data , kind='cubic') prop.twist_distribution = func_twist_distribution(new_radius_distribution) prop.chord_distribution = func_chord_distribution(new_radius_distribution) prop.radius_distribution = func_radius_distribution(new_radius_distribution) prop.max_thickness_distribution = func_max_thickness_distribution(new_radius_distribution) prop.thickness_to_chord = prop.max_thickness_distribution/prop.chord_distribution ospath = os.path.abspath(__file__) separator = os.path.sep rel_path = ospath.split('noise_fidelity_one' + separator + 'propeller_noise.py')[0] + 'Vehicles/Airfoils' + separator prop.airfoil_geometry = [ rel_path +'Clark_y.txt'] prop.airfoil_polars = [[rel_path +'Polars/Clark_y_polar_Re_50000.txt' ,rel_path +'Polars/Clark_y_polar_Re_100000.txt',rel_path +'Polars/Clark_y_polar_Re_200000.txt', rel_path +'Polars/Clark_y_polar_Re_500000.txt',rel_path +'Polars/Clark_y_polar_Re_1000000.txt']] airfoil_polar_stations = np.zeros(dim) prop.airfoil_polar_stations = list(airfoil_polar_stations.astype(int)) airfoil_polars = compute_airfoil_polars(prop.airfoil_geometry, prop.airfoil_polars) airfoil_cl_surs = airfoil_polars.lift_coefficient_surrogates airfoil_cd_surs = airfoil_polars.drag_coefficient_surrogates prop.airfoil_flag = True prop.airfoil_cl_surrogates = airfoil_cl_surs prop.airfoil_cd_surrogates = airfoil_cd_surs prop.mid_chord_alignment = np.zeros_like(prop.chord_distribution) prop.number_of_airfoil_section_points = 102 prop.airfoil_data = import_airfoil_geometry(prop.airfoil_geometry, npoints = prop.number_of_airfoil_section_points) return prop
def propeller_design(prop,number_of_stations=20): """ Optimizes propeller chord and twist given input parameters. Inputs: Either design power or thrust prop_attributes. hub radius [m] tip radius [m] rotation rate [rad/s] freestream velocity [m/s] number of blades number of stations design lift coefficient airfoil data Outputs: Twist distribution [array of radians] Chord distribution [array of meters] Assumptions/ Source: Based on Design of Optimum Propellers by Adkins and Liebeck """ # Unpack N = number_of_stations # this number determines the discretization of the propeller into stations B = prop.number_blades R = prop.tip_radius Rh = prop.hub_radius omega = prop.angular_velocity # Rotation Rate in rad/s V = prop.freestream_velocity # Freestream Velocity Cl = prop.design_Cl # Design Lift Coefficient alt = prop.design_altitude Thrust = prop.design_thrust Power = prop.design_power a_geo = prop.airfoil_geometry a_pol = prop.airfoil_polars 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 prop.rotation == None: prop.rotation = list(np.ones(int(B))) # Calculate atmospheric properties atmosphere = SUAVE.Analyses.Atmospheric.US_Standard_1976() atmo_data = atmosphere.compute_values(alt) p = atmo_data.pressure[0] 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 J = V/(D*n) c = 0.2 * np.ones_like(chi) # if user defines airfoil, check dimension of stations if a_pol != None and a_loc != None: if len(a_loc) != N: raise AssertionError('\nDimension of airfoil sections must be equal to number of stations on propeller') else: # Import Airfoil from regression print('\nNo airfoils specified for propeller or rotor airfoil specified. \nDefaulting to NACA 4412 airfoils that will provide conservative estimates.') import os ospath = os.path.abspath(__file__) separator = os.path.sep path = ospath.replace('\\','/').split('trunk/SUAVE/Methods/Propulsion/propeller_design.py')[0] \ + 'regression' + separator + 'scripts' + separator + 'Vehicles' + separator a_geo = [ path + 'NACA_4412.txt'] a_pol = [[path + 'NACA_4412_polar_Re_50000.txt' , path + 'NACA_4412_polar_Re_100000.txt' , path + 'NACA_4412_polar_Re_200000.txt' , path + 'NACA_4412_polar_Re_500000.txt' , path + 'NACA_4412_polar_Re_1000000.txt' ]] # airfoil polars for at different reynolds numbers # 0 represents the first airfoil, 1 represents the second airfoil etc. a_loc = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] prop.airfoil_geometry = a_geo prop.airfoil_polars = a_pol prop.airfoil_polar_stations = a_loc while diff>tol: # assign chord distribution prop.chord_distribution = c # compute airfoil polars for airfoils airfoil_polars = compute_airfoil_polars(prop, a_geo, a_pol) airfoil_cl_surs = airfoil_polars.lift_coefficient_surrogates airfoil_cd_surs = airfoil_polars.drag_coefficient_surrogates #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 #Step 4, determine epsilon and alpha from airfoil data alpha = np.zeros_like(RE) Cdval = np.zeros_like(RE) for i in range(N): AoA_old_guess = 0.01 cl_diff = 1 broke = False ii = 0 # Newton Raphson Iteration while cl_diff > 1E-3: Cl_guess = airfoil_cl_surs[a_geo[a_loc[i]]](RE[i],AoA_old_guess,grid=False) - Cl # central difference derivative dx = 1E-5 dCL = (airfoil_cl_surs[a_geo[a_loc[i]]](RE[i],AoA_old_guess + dx,grid=False) - airfoil_cl_surs[a_geo[a_loc[i]]](RE[i],AoA_old_guess- dx,grid=False))/ (2*dx) # update AoA guess AoA_new_guess = AoA_old_guess - Cl_guess/dCL AoA_old_guess = AoA_new_guess # compute difference for tolerance check cl_diff = abs(Cl_guess) ii+=1 if ii>10000: # maximum iterations is 10000 print('Propeller/Rotor section is not converging to solution') broke = True break alpha[i] = AoA_old_guess Cdval[i] = airfoil_cd_surs[a_geo[a_loc[i]]](RE[i],alpha[i],grid=False) #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)) aprime = (zeta/(2.*x))*np.cos(phi)*np.sin(phi)*(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.) dR = (r[1]-r[0])*np.ones_like(Jprime1) 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) airfoil_geometry_data = import_airfoil_geometry(a_geo) for i in range(N): t_c[i] = airfoil_geometry_data.thickness_to_chord[a_loc[i]] t_max[i] = airfoil_geometry_data.max_thickness[a_loc[i]]*c[i] # Nondimensional thrust if prop.design_power == None: prop.design_power = Power[0] elif prop.design_thrust == None: prop.design_thrust = Thrust[0] # approximate thickness to chord ratio t_c_at_70_percent = t_c[int(N*0.7)] # blade solidity r = chi*R # Radial coordinate blade_area = sp.integrate.cumtrapz(B*c, r-r[0]) sigma = blade_area[-1]/(np.pi*R**2) prop.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_blades = int(B) prop.design_power_coefficient = Cp prop.design_thrust_coefficient = Ct prop.mid_chord_aligment = MCA prop.thickness_to_chord = t_c_at_70_percent prop.blade_solidity = sigma prop.airfoil_cl_surrogates = airfoil_cl_surs prop.airfoil_cd_surrogates = airfoil_cd_surs return prop
def spin(self,conditions): """Analyzes a propeller given geometry and operating conditions. Assumptions: per source Source: Drela, M. "Qprop Formulation", MIT AeroAstro, June 2006 http://web.mit.edu/drela/Public/web/qprop/qprop_theory.pdf Inputs: self.inputs.omega [radian/s] conditions.freestream. density [kg/m^3] dynamic_viscosity [kg/(m-s)] speed_of_sound [m/s] temperature [K] conditions.frames. body.transform_to_inertial (rotation matrix) inertial.velocity_vector [m/s] conditions.propulsion. throttle [-] Outputs: conditions.propulsion.acoustic_outputs. number_sections [-] r0 [m] airfoil_chord [m] blades_number [-] propeller_diameter [m] drag_coefficient [-] lift_coefficient [-] omega [radian/s] velocity [m/s] thrust [N] power [W] mid_chord_aligment [m] (distance from the mid chord to the line axis out of the center of the blade) conditions.propulsion.etap [-] thrust [N] torque [Nm] power [W] Cp [-] (coefficient of power) Properties Used: self. number_blades [-] tip_radius [m] hub_radius [m] twist_distribution [radians] chord_distribution [m] mid_chord_aligment [m] (distance from the mid chord to the line axis out of the center of the blade) thrust_angle [radians] """ #Unpack B = self.number_blades R = self.tip_radius Rh = self.hub_radius beta = self.twist_distribution c = self.chord_distribution chi = self.radius_distribution omega = self.inputs.omega a_geo = self.airfoil_geometry a_pol = self.airfoil_polars a_loc = self.airfoil_polar_stations rho = conditions.freestream.density[:,0,None] mu = conditions.freestream.dynamic_viscosity[:,0,None] Vv = conditions.frames.inertial.velocity_vector Vh = self.induced_hover_velocity a = conditions.freestream.speed_of_sound[:,0,None] T = conditions.freestream.temperature[:,0,None] theta = self.thrust_angle tc = self.thickness_to_chord sigma = self.blade_solidity BB = B*B BBB = BB*B # Velocity in the Body frame T_body2inertial = conditions.frames.body.transform_to_inertial T_inertial2body = orientation_transpose(T_body2inertial) V_body = orientation_product(T_inertial2body,Vv) body2thrust = np.array([[np.cos(theta), 0., np.sin(theta)],[0., 1., 0.], [-np.sin(theta), 0., np.cos(theta)]]) T_body2thrust = orientation_transpose(np.ones_like(T_body2inertial[:])*body2thrust) V_thrust = orientation_product(T_body2thrust,V_body) # Now just use the aligned velocity V = V_thrust[:,0,None] ua = np.zeros_like(V) ut = np.zeros_like(V) nu = mu/rho tol = 1e-5 # Convergence tolerance #Things that don't change with iteration N = len(c) # Number of stations if a_pol != None and a_loc != None: airfoil_polars = Data() # check dimension of section if len(a_loc) != N: raise AssertionError('Dimension of airfoil sections must be equal to number of stations on propeller') # compute airfoil polars for airfoils airfoil_polars = compute_airfoil_polars(self, a_geo, a_pol) airfoil_cl = airfoil_polars.lift_coefficients airfoil_cd = airfoil_polars.drag_coefficients AoA_sweep = airfoil_polars.angle_of_attacks if self.radius_distribution is None: 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 pi = np.pi pi2 = pi*pi x = r*np.multiply(omega,1/V) # Nondimensional distance n = omega/(2.*pi) # Cycles per second J = V/(2.*R*n) omegar = np.outer(omega,r) Ua = np.outer((V + ua),np.ones_like(r)) Ut = omegar - ut U = np.sqrt(Ua*Ua + Ut*Ut) #Things that will change with iteration size = (len(a),N) Cl = np.zeros((1,N)) #Setup a Newton iteration psi = np.ones(size) psiold = np.zeros(size) diff = 1. ii = 0 broke = False while (diff>tol): sin_psi = np.sin(psi) cos_psi = np.cos(psi) Wa = 0.5*Ua + 0.5*U*sin_psi Wt = 0.5*Ut + 0.5*U*cos_psi va = Wa - Ua vt = Ut - Wt alpha = beta - np.arctan2(Wa,Wt) W = (Wa*Wa + Wt*Wt)**0.5 Ma = (W)/a #a is the speed of sound lamdaw = r*Wa/(R*Wt) # Limiter to keep from Nan-ing lamdaw[lamdaw<0.] = 0. f = (B/2.)*(1.-r/R)/lamdaw piece = np.exp(-f) arccos_piece = np.arccos(piece) F = 2.*arccos_piece/pi Gamma = vt*(4.*pi*r/B)*F*(1.+(4.*lamdaw*R/(pi*B*r))*(4.*lamdaw*R/(pi*B*r)))**0.5 # Estimate Cl max Re = (W*c)/nu Cl_max_ref = -0.0009*tc**3 + 0.0217*tc**2 - 0.0442*tc + 0.7005 Re_ref = 9.*10**6 Cl1maxp = Cl_max_ref * ( Re / Re_ref ) **0.1 # Compute blade CL distribution from the airfoil data if a_pol != None and a_loc != None: for k in range(N): Cl[0,k] = np.interp(alpha[0,k],AoA_sweep,airfoil_cl[a_loc[k]]) else: # If not airfoil polar provided, use 2*pi as lift curve slope Cl = 2.*pi*alpha # By 90 deg, it's totally stalled. Cl[Cl>Cl1maxp] = Cl1maxp[Cl>Cl1maxp] # This line of code is what changed the regression testing Cl[alpha>=pi/2] = 0. # Scale for Mach, this is Karmen_Tsien Cl[Ma[:,:]<1.] = Cl[Ma[:,:]<1.]/((1-Ma[Ma[:,:]<1.]*Ma[Ma[:,:]<1.])**0.5+((Ma[Ma[:,:]<1.]*Ma[Ma[:,:]<1.])/(1+(1-Ma[Ma[:,:]<1.]*Ma[Ma[:,:]<1.])**0.5))*Cl[Ma<1.]/2) # If the blade segments are supersonic, don't scale Cl[Ma[:,:]>=1.] = Cl[Ma[:,:]>=1.] Rsquiggly = Gamma - 0.5*W*c*Cl #An analytical derivative for dR_dpsi, this is derived by taking a derivative of the above equations #This was solved symbolically in Matlab and exported f_wt_2 = 4*Wt*Wt f_wa_2 = 4*Wa*Wa Ucospsi = U*cos_psi Usinpsi = U*sin_psi Utcospsi = Ut*cos_psi Uasinpsi = Ua*sin_psi UapUsinpsi = (Ua + Usinpsi) utpUcospsi = (Ut + Ucospsi) utpUcospsi2 = utpUcospsi*utpUcospsi UapUsinpsi2 = UapUsinpsi*UapUsinpsi dR_dpsi = ((4.*U*r*arccos_piece*sin_psi*((16.*UapUsinpsi2)/(BB*pi2*f_wt_2) + 1.)**(0.5))/B - (pi*U*(Ua*cos_psi - Ut*sin_psi)*(beta - np.arctan((Wa+Wa)/(Wt+Wt))))/(2.*(f_wt_2 + f_wa_2)**(0.5)) + (pi*U*(f_wt_2 +f_wa_2)**(0.5)*(U + Utcospsi + Uasinpsi))/(2.*(f_wa_2/(f_wt_2) + 1.)*utpUcospsi2) - (4.*U*piece*((16.*UapUsinpsi2)/(BB*pi2*f_wt_2) + 1.)**(0.5)*(R - r)*(Ut/2. - (Ucospsi)/2.)*(U + Utcospsi + Uasinpsi ))/(f_wa_2*(1. - np.exp(-(B*(Wt+Wt)*(R - r))/(r*(Wa+Wa))))**(0.5)) + (128.*U*r*arccos_piece*(Wa+Wa)*(Ut/2. - (Ucospsi)/2.)*(U + Utcospsi + Uasinpsi ))/(BBB*pi2*utpUcospsi*utpUcospsi2*((16.*f_wa_2)/(BB*pi2*f_wt_2) + 1.)**(0.5))) dR_dpsi[np.isnan(dR_dpsi)] = 0.1 dpsi = -Rsquiggly/dR_dpsi psi = psi + dpsi diff = np.max(abs(psiold-psi)) psiold = psi # If its really not going to converge if np.any(psi>pi/2) and np.any(dpsi>0.0): break ii+=1 if ii>2000: broke = True break #There is also RE scaling #This is an atrocious fit of DAE51 data at RE=50k for Cd Cdval = (0.108*(Cl*Cl*Cl*Cl)-0.2612*(Cl*Cl*Cl)+0.181*(Cl*Cl)-0.0139*Cl+0.0278)*((50000./Re)**0.2) Cdval[alpha>=pi/2] = 2. #More Cd scaling from Mach from AA241ab notes for turbulent skin friction Tw_Tinf = 1. + 1.78*(Ma*Ma) Tp_Tinf = 1. + 0.035*(Ma*Ma) + 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 epsilon = Cd/Cl epsilon[epsilon==np.inf] = 10. deltar = (r[1]-r[0]) thrust = rho*B*(np.sum(Gamma*(Wt-epsilon*Wa)*deltar,axis=1)[:,None]) torque = rho*B*np.sum(Gamma*(Wa+epsilon*Wt)*r*deltar,axis=1)[:,None] D = 2*R Ct = thrust/(rho*(n*n)*(D*D*D*D)) Ct[Ct<0] = 0. #prevent things from breaking kappa = self.induced_power_factor Cd0 = self.profile_drag_coefficient Cp = np.zeros_like(Ct) power = np.zeros_like(Ct) for i in range(len(Vv)): if -1. <Vv[i][0] <1.: # vertical/axial flight Cp[i] = (kappa*(Ct[i]**1.5)/(2**.5))+sigma*Cd0/8. power[i] = Cp[i]*(rho[i]*(n[i]*n[i]*n[i])*(D*D*D*D*D)) torque[i] = power[i]/omega[i] else: power[i] = torque[i]*omega[i] Cp[i] = power[i]/(rho[i]*(n[i]*n[i]*n[i])*(D*D*D*D*D)) # torque coefficient Cq = torque/(rho*(n*n)*(D*D*D*D)*R) thrust[conditions.propulsion.throttle[:,0] <=0.0] = 0.0 power[conditions.propulsion.throttle[:,0] <=0.0] = 0.0 torque[conditions.propulsion.throttle[:,0] <=0.0] = 0.0 thrust[omega<0.0] = - thrust[omega<0.0] etap = V*thrust/power conditions.propulsion.etap = etap # store data results_conditions = Data outputs = results_conditions( num_blades = B, rotor_radius = R, rotor_diameter = D, number_sections = N, radius_distribution = np.linspace(Rh ,R, N), chord_distribution = c, twist_distribution = beta, normalized_radial_distribution = r, thrust_angle = theta, speed_of_sound = conditions.freestream.speed_of_sound, density = conditions.freestream.density, velocity = Vv, tangential_velocity_distribution = vt, axial_velocity_distribution = va, drag_coefficient = Cd, lift_coefficient = Cl, omega = omega, dT_dR = rho*(Gamma*(Wt-epsilon*Wa)), dT_dr = rho*(Gamma*(Wt-epsilon*Wa))*R, thrust_distribution = rho*(Gamma*(Wt-epsilon*Wa))*deltar, thrust_per_blade = thrust/B, thrust_coefficient = Ct, dQ_dR = rho*(Gamma*(Wa+epsilon*Wt)*r), dQ_dr = rho*(Gamma*(Wa+epsilon*Wt)*r)*R, torque_distribution = rho*(Gamma*(Wa+epsilon*Wt)*r)*deltar, torque_per_blade = torque/B, torque_coefficient = Cq, power = power, power_coefficient = Cp, mid_chord_aligment = self.mid_chord_aligment ) return thrust, torque, power, Cp, outputs , etap
def plot_airfoil_polar_files(airfoil_path, airfoil_polar_paths, line_color = 'k-', use_surrogate = False, display_plot = False, save_figure = False, save_filename = "Airfoil_Polars", file_type = ".png"): """This plots all airfoil polars in the list "airfoil_polar_paths" Assumptions: None Source: None Inputs: airfoil_polar_paths [list of strings] Outputs: Plots Properties Used: N/A """ shape = np.shape(airfoil_polar_paths) n_airfoils = shape[0] n_Re = shape[1] if use_surrogate: # Compute airfoil surrogates a_data = compute_airfoil_polars(airfoil_path, airfoil_polar_paths, use_pre_stall_data=False) CL_sur = a_data.lift_coefficient_surrogates CD_sur = a_data.drag_coefficient_surrogates alpha = np.asarray(a_data.aoa_from_polar) n_alpha = len(alpha.T) alpha = np.reshape(alpha,(n_airfoils,1,n_alpha)) alpha = np.repeat(alpha, n_Re, axis=1) Re = a_data.re_from_polar Re = np.reshape(Re,(n_airfoils,n_Re,1)) Re = np.repeat(Re, n_alpha, axis=2) CL = np.zeros_like(Re) CD = np.zeros_like(Re) else: # Use the raw data polars airfoil_polar_data = import_airfoil_polars(airfoil_polar_paths) CL = airfoil_polar_data.lift_coefficients CD = airfoil_polar_data.drag_coefficients n_alpha = np.shape(CL)[2] Re = airfoil_polar_data.reynolds_number Re = np.reshape(Re, (n_airfoils,n_Re,1)) Re = np.repeat(Re, n_alpha, axis=2) for i in range(n_airfoils): airfoil_name = os.path.basename(airfoil_path[i][0]) if use_surrogate: CL[i,:,:] = CL_sur[airfoil_path[i]](Re[i,:,:],alpha[i,:,:],grid=False) CD[i,:,:] = CD_sur[airfoil_path[i]](Re[i,:,:],alpha[i,:,:],grid=False) # plot all Reynolds number polars for ith airfoil fig = plt.figure(save_filename +'_'+ str(i)) fig.set_size_inches(10, 4) axes = fig.add_subplot(1,1,1) axes.set_title(airfoil_name) for j in range(n_Re): Re_val = str(round(Re[i,j,0])) axes.plot(CD[i,j,:], CL[i,j,:], label='Re='+Re_val) axes.set_xlabel('$C_D$') axes.set_ylabel('$C_L$') axes.legend(bbox_to_anchor=(1,1), loc='upper left', ncol=1) if save_figure: plt.savefig(save_filename +'_' + str(i) + file_type) if display_plot: plt.show() return
def propeller_design(prop, number_of_stations=20): """ Optimizes propeller chord and twist given input parameters. Inputs: Either design power or thrust prop_attributes. hub radius [m] tip radius [m] rotation rate [rad/s] freestream velocity [m/s] number of blades number of stations design lift coefficient airfoil data Outputs: Twist distribution [array of radians] Chord distribution [array of meters] Assumptions/ Source: Based on Design of Optimum Propellers by Adkins and Liebeck """ print('\nDesigning', prop.tag) # 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.angular_velocity # Rotation Rate in rad/s V = prop.freestream_velocity # Freestream Velocity Cl = prop.design_Cl # Design Lift Coefficient alt = prop.design_altitude Thrust = prop.design_thrust Power = prop.design_power a_geo = prop.airfoil_geometry a_pol = prop.airfoil_polars 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 = SUAVE.Analyses.Atmospheric.US_Standard_1976() atmo_data = atmosphere.compute_values(alt) p = atmo_data.pressure[0] 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 J = V / (D * n) c = 0.2 * np.ones_like(chi) # if user defines airfoil, check dimension of stations if a_pol != None and a_loc != None: if len(a_loc) != N: raise AssertionError( '\nDimension of airfoil sections must be equal to number of stations on propeller' ) airfoil_flag = True else: print('\nDefaulting to scaled DAE51') airfoil_flag = False airfoil_cl_surs = None airfoil_cd_surs = None # Step 4, determine epsilon and alpha from airfoil data if airfoil_flag: # compute airfoil polars for airfoils airfoil_polars = compute_airfoil_polars(a_geo, a_pol) airfoil_cl_surs = airfoil_polars.lift_coefficient_surrogates airfoil_cd_surs = airfoil_polars.drag_coefficient_surrogates 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 airfoil_flag: # assign initial values alpha0 = np.ones(N) * 0.05 # solve for optimal alpha to meet design Cl target sol = root(objective, x0=alpha0, args=(airfoil_cl_surs, RE, a_geo, a_loc, Cl, N)) alpha = sol.x # query surrogate for sectional Cls at stations Cdval = np.zeros_like(RE) for j in range(len(airfoil_cd_surs)): Cdval_af = airfoil_cd_surs[a_geo[j]](RE, alpha, grid=False) 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)) aprime = (zeta / (2. * x)) * np.cos(phi) * np.sin(phi) * ( 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.) dR = (r[1] - r[0]) * np.ones_like(Jprime1) 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 airfoil_flag: airfoil_geometry_data = import_airfoil_geometry(a_geo) t_max = np.take(airfoil_geometry_data.max_thickness, a_loc, axis=0) * c t_c = np.take(airfoil_geometry_data.thickness_to_chord, a_loc, axis=0) 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.design_power == None: prop.design_power = Power[0] elif prop.design_thrust == None: prop.design_thrust = Thrust[0] # blade solidity r = chi * R # Radial coordinate blade_area = sp.integrate.cumtrapz(B * c, r - r[0]) sigma = blade_area[-1] / (np.pi * R**2) prop.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.design_power_coefficient = Cp prop.design_thrust_coefficient = Ct prop.mid_chord_alignment = MCA prop.thickness_to_chord = t_c prop.blade_solidity = sigma prop.airfoil_cl_surrogates = airfoil_cl_surs prop.airfoil_cd_surrogates = airfoil_cd_surs prop.airfoil_flag = airfoil_flag return prop
def propeller_geometry(): # -------------------------------------------------------------------------------------------------- # Propeller Geometry: # -------------------------------------------------------------------------------------------------- prop = SUAVE.Components.Energy.Converters.Propeller() prop.tag = "APC 10x7 Propeller" prop.tip_radius = 5 * Units.inches prop.number_of_blades = 2 prop.hub_radius = prop.tip_radius * 0.1 prop.inputs = Data() prop.inputs.omega = np.array([[4500 * Units.rpm]]) r_R = np.array([ 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, ]) c_R = np.array([ 0.138, 0.154, 0.175, 0.190, 0.198, 0.202, 0.200, 0.195, 0.186, 0.174, 0.161, 0.145, 0.129, 0.112, 0.096, 0.081, 0.061, ]) beta = np.array([ 37.86, 45.82, 44.19, 38.35, 33.64, 29.90, 27.02, 24.67, 22.62, 20.88, 19.36, 17.98, 16.74, 15.79, 14.64, 13.86, 12.72, ]) prop.pitch_command = 0.0 * Units.deg prop.twist_distribution = beta * Units.deg prop.chord_distribution = c_R * prop.tip_radius prop.radius_distribution = r_R * prop.tip_radius prop.max_thickness_distribution = 0.12 prop.number_azimuthal_stations = 24 prop.number_radial_stations = len(r_R) # Distance from mid chord to the line axis out of the center of the blade - In this case the 1/4 chords are all aligned MCA = prop.chord_distribution / 4. - prop.chord_distribution[0] / 4. prop.mid_chord_alignment = MCA airfoils_path = os.path.join(os.path.dirname(__file__), "../Airfoils/") polars_path = os.path.join(os.path.dirname(__file__), "../Airfoils/Polars/") prop.airfoil_geometry = [airfoils_path + "Clark_y.txt"] prop.airfoil_polars = [ [ polars_path + "Clark_y_polar_Re_50000.txt", polars_path + "Clark_y_polar_Re_100000.txt", polars_path + "Clark_y_polar_Re_200000.txt", polars_path + "Clark_y_polar_Re_500000.txt", polars_path + "Clark_y_polar_Re_1000000.txt", ], ] prop.airfoil_polar_stations = np.zeros(len(r_R)) prop.airfoil_polar_stations = list(prop.airfoil_polar_stations.astype(int)) airfoil_polars = compute_airfoil_polars(prop.airfoil_geometry, prop.airfoil_polars) airfoil_cl_surs = airfoil_polars.lift_coefficient_surrogates airfoil_cd_surs = airfoil_polars.drag_coefficient_surrogates prop.airfoil_cl_surrogates = airfoil_cl_surs prop.airfoil_cd_surrogates = airfoil_cd_surs results = Data() results.lift_coefficient_surrogates = airfoil_polars.lift_coefficient_surrogates results.drag_coefficient_surrogates = airfoil_polars.drag_coefficient_surrogates results.cl_airfoiltools = airfoil_polars.lift_coefficients_from_polar results.cd_airfoiltools = airfoil_polars.drag_coefficients_from_polar results.re_airfoiltools = airfoil_polars.re_from_polar results.aoa_airfoiltools = airfoil_polars.aoa_from_polar return prop