def main(): ospath = os.path.abspath(__file__) separator = os.path.sep airfoils_path = ospath.split('airfoil_import' + separator + 'airfoil_interpolation_test.py')[0] + 'Vehicles/Airfoils' + separator a_labels = ["Clark_y", "E63"] nairfoils = 4 # number of total airfoils a1 = airfoils_path + a_labels[0]+ ".txt" a2 = airfoils_path + a_labels[1]+ ".txt" new_files = generate_interpolated_airfoils(a1, a2, nairfoils, save_filename="Transition") plt.show() # import the new airfoil geometries and compare to the regression: airfoil_data_1 = import_airfoil_geometry([new_files['a_1'].name]) airfoil_data_2 = import_airfoil_geometry([new_files['a_2'].name]) airfoil_data_1_r = import_airfoil_geometry(["Transition1_regression.txt"]) airfoil_data_2_r = import_airfoil_geometry(["Transition2_regression.txt"]) # ensure coordinates are the same: assert( max(abs(airfoil_data_1.x_coordinates[0] - airfoil_data_1_r.x_coordinates[0])) < 1e-5) assert( max(abs(airfoil_data_2.x_coordinates[0] - airfoil_data_2_r.x_coordinates[0])) < 1e-5) assert( max(abs(airfoil_data_1.y_coordinates[0] - airfoil_data_1_r.y_coordinates[0])) < 1e-5) assert( max(abs(airfoil_data_2.y_coordinates[0] - airfoil_data_2_r.y_coordinates[0])) < 1e-5) return
def main(): airfoil_polar_names = ['airfoil_polar_1.txt','airfoil_polar_2.txt'] airfoil_polar_data = import_airfoil_polars(airfoil_polar_names) airfoil_geometry_names = ['airfoil_geometry_1.txt','airfoil_geometry_2.txt', 'airfoil_geometry_2-selig.txt'] airfoil_geometry_data = import_airfoil_geometry(airfoil_geometry_names) # Actual t/c values airfoil_tc_actual = [0.11806510344827587, 0.11175207317073171, 0.11175207317073171] # Check t/c calculation against previously calculated values for i in range(0, len(airfoil_geometry_names)): assert(np.abs(airfoil_tc_actual[i]-airfoil_geometry_data.thickness_to_chord[i]) < 1E-8 ) # Check that camber line comes back the same for the Lednicer and Selig formats for j in range(0, len(airfoil_geometry_data.camber_coordinates[1])): assert( np.abs(airfoil_geometry_data.camber_coordinates[1][j] - airfoil_geometry_data.camber_coordinates[2][j]) < 1E-8 ) plot_airfoil(airfoil_geometry_names) return
def make_airfoil_text(vsp_bem,prop): """This function writes the airfoil geometry into the vsp file Assumptions: None Source: None Inputs: vsp_bem - OpenVSP .bem file prop - SUAVE propeller data structure Outputs: NA Properties Used: N/A """ N = len(prop.radius_distribution) airfoil_data = import_airfoil_geometry(prop.airfoil_geometry) a_sec = prop.airfoil_polar_stations for i in range(N): airfoil_station_header = '\nSection ' + str(i) + ' X, Y\n' vsp_bem.write(airfoil_station_header) airfoil_x = airfoil_data.x_coordinates[int(a_sec[i])] airfoil_y = airfoil_data.y_coordinates[int(a_sec[i])] for j in range(len(airfoil_x)): section_text = format(airfoil_x[j], '.7f')+ ", " + format(airfoil_y[j], '.7f') + "\n" vsp_bem.write(section_text) return
def plot_airfoil(airfoil_names, line_color='k-', overlay=False, save_figure=False, save_filename="Airfoil_Geometry", file_type=".png"): """This plots all airfoil defined in the list "airfoil_names" Assumptions: None Source: None Inputs: airfoil_geometry_files <list of strings> Outputs: Plots Properties Used: N/A """ # get airfoil coordinate geometry airfoil_data = import_airfoil_geometry(airfoil_names) if overlay: name = save_filename fig = plt.figure(name) fig.set_size_inches(10, 4) axes = fig.add_subplot(1, 1, 1) for i in range(len(airfoil_names)): # separate x and y coordinates airfoil_x = airfoil_data.x_coordinates[i] airfoil_y = airfoil_data.y_coordinates[i] axes.plot(airfoil_x, airfoil_y, label=airfoil_names[i]) axes.set_title("Airfoil Geometry") axes.legend() if save_figure: plt.savefig(name + file_type) else: for i in range(len(airfoil_names)): # separate x and y coordinates airfoil_x = airfoil_data.x_coordinates[i] airfoil_y = airfoil_data.y_coordinates[i] name = save_filename + '_' + str(i) fig = plt.figure(name) axes = fig.add_subplot(1, 1, 1) axes.set_title(airfoil_names[i]) axes.plot(airfoil_x, airfoil_y, line_color) axes.axis('equal') if save_figure: plt.savefig(name + file_type) return
def main(): airfoil_polar_names = ['airfoil_polar_1.txt','airfoil_polar_2.txt'] airfoil_polar_data = import_airfoil_polars(airfoil_polar_names) airfoil_geometry_names = ['airfoil_geometry_1.txt','airfoil_geometry_2.txt'] airfoil_geometry_data = import_airfoil_geometry (airfoil_geometry_names) return
def main(): ospath = os.path.abspath(__file__) separator = os.path.sep rel_path = ospath.split( 'airfoil_import' + separator + 'airfoil_import_test.py' )[0] + 'Vehicles' + separator + 'Airfoils' + separator airfoil_geometry_with_selig = [ rel_path + 'NACA_4412.txt', 'airfoil_geometry_2.txt', 'airfoil_geometry_2-selig.txt' ] airfoil_geometry = [rel_path + 'NACA_4412.txt'] airfoil_polar_names = [[ rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_50000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_100000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_200000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_500000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_1000000.txt' ]] # plot airfoil polar data with and without surrogate plot_airfoil_polar_files(airfoil_geometry, airfoil_polar_names, display_plot=True) plot_airfoil_polar_files(airfoil_geometry, airfoil_polar_names, use_surrogate=True, display_plot=True) airfoil_polar_data = import_airfoil_polars(airfoil_polar_names) airfoil_geometry_data = import_airfoil_geometry( airfoil_geometry_with_selig) # Actual t/c values airfoil_tc_actual = [ 0.12031526401402462, 0.11177063928743779, 0.11177063928743779 ] # Check t/c calculation against previously calculated values for i in range(0, len(airfoil_geometry_with_selig)): assert (np.abs(airfoil_tc_actual[i] - airfoil_geometry_data.thickness_to_chord[i]) < 1E-8) # Check that camber line comes back the same for the Lednicer and Selig formats for j in range(0, len(airfoil_geometry_data.camber_coordinates[1])): assert (np.abs(airfoil_geometry_data.camber_coordinates[1][j] - airfoil_geometry_data.camber_coordinates[2][j]) < 1E-8) plot_airfoil(airfoil_geometry_with_selig) return
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 main(): ospath = os.path.abspath(__file__) separator = os.path.sep rel_path = ospath.split( 'airfoil_import' + separator + 'airfoil_import_test.py' )[0] + 'Vehicles' + separator + 'Airfoils' + separator airfoil_polar_names = [[ rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_50000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_100000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_200000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_500000.txt', rel_path + 'Polars' + separator + 'NACA_4412_polar_Re_1000000.txt' ]] airfoil_polar_data = import_airfoil_polars(airfoil_polar_names) airfoil_geometry_names = [ rel_path + 'NACA_4412.txt', 'airfoil_geometry_2.txt', 'airfoil_geometry_2-selig.txt' ] airfoil_geometry_data = import_airfoil_geometry(airfoil_geometry_names) # Actual t/c values airfoil_tc_actual = [ 0.12012222222222223, 0.11171495959595959, 0.11171495959595959 ] # Check t/c calculation against previously calculated values for i in range(0, len(airfoil_geometry_names)): assert (np.abs(airfoil_tc_actual[i] - airfoil_geometry_data.thickness_to_chord[i]) < 1E-8) # Check that camber line comes back the same for the Lednicer and Selig formats for j in range(0, len(airfoil_geometry_data.camber_coordinates[1])): assert (np.abs(airfoil_geometry_data.camber_coordinates[1][j] - airfoil_geometry_data.camber_coordinates[2][j]) < 1E-8) plot_airfoil(airfoil_geometry_names) 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 """ # 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 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!') # 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) while diff > tol: #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 #This is an atrocious fit of DAE51 data at RE=50k for Cd #There is also RE scaling Cdval = (0.108 * (Cl**4) - 0.2612 * (Cl**3) + 0.181 * (Cl**2) - 0.0139 * Cl + 0.0278) * ((50000. / RE)**0.2) #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 alpha = Cl / (2. * np.pi) epsilon = Cd / Cl #Step 5, change Cl and repeat steps 3 and 4 until epsilon is minimized #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.) else: print('Power and thrust are both specified!') # 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 Cp = Power / (rho * (n**3) * (D**5)) # compute max thickness distribution using NACA 4 series eqn t_max = np.zeros(20) for idx in range(20): c_blade = np.linspace(0, c[idx], 20) # local chord 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[idx] = np.max(t) # 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 = t_max / c 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.power_coefficient = Cp prop.mid_chord_aligment = MCA prop.thickness_to_chord = t_c_at_70_percent prop.blade_solidity = sigma # compute airfoil sections if given if a_geo != None: airfoil_geometry = Data() prop.airfoil_data = import_airfoil_geometry(a_geo) return prop
def plot_propeller_geometry(prop, line_color='bo-', save_figure=False, save_filename="Propeller_Geometry", file_type=".png"): """This plots the geoemtry of a propeller or rotor Assumptions: None Source: None Inputs: SUAVE.Components.Energy.Converters.Propeller() Outputs: Plots Properties Used: N/A """ # unpack Rt = prop.tip_radius Rh = prop.hub_radius num_B = prop.number_blades a_sec = prop.airfoil_geometry a_secl = prop.airfoil_polar_stations beta = prop.twist_distribution b = prop.chord_distribution r = prop.radius_distribution t = prop.max_thickness_distribution # prepare plot parameters dim = len(b) theta = np.linspace(0, 2 * np.pi, num_B + 1) fig = plt.figure(save_filename) fig.set_size_inches(10, 8) axes = plt.axes(projection='3d') axes.set_zlim3d(-1, 1) axes.set_ylim3d(-1, 1) axes.set_xlim3d(-1, 1) chord = np.outer(np.linspace(0, 1, 10), b) if r == None: r = np.linspace(Rh, Rt, len(b)) for i in range(num_B): # plot propeller planfrom surf_x = np.cos(theta[i]) * (chord * np.cos(beta)) - np.sin( theta[i]) * (r) surf_y = np.sin(theta[i]) * (chord * np.cos(beta)) + np.cos( theta[i]) * (r) surf_z = chord * np.sin(beta) axes.plot_surface(surf_x, surf_y, surf_z, color='gray') if a_sec != None and a_secl != None: # check dimension of section dim_sec = len(a_secl) if dim_sec != dim: raise AssertionError( "Number of sections not equal to number of stations") # get airfoil coordinate geometry airfoil_data = import_airfoil_geometry(a_sec) #plot airfoils for j in range(dim): airfoil_max_t = airfoil_data.thickness_to_chord[a_secl[j]] airfoil_xp = b[j] - airfoil_data.x_coordinates[ a_secl[j]] * b[j] airfoil_yp = r[j] * np.ones_like(airfoil_xp) airfoil_zp = airfoil_data.y_coordinates[a_secl[j]] * b[j] * ( t[j] / (airfoil_max_t * b[j])) transformation_1 = [[np.cos(beta[j]), 0, -np.sin(beta[j])], [0, 1, 0], [np.sin(beta[j]), 0, np.cos(beta[j])]] transformation_2 = [[np.cos(theta[i]), -np.sin(theta[i]), 0], [np.sin(theta[i]), np.cos(theta[i]), 0], [0, 0, 1]] transformation = np.matmul(transformation_2, transformation_1) airfoil_x = np.zeros(len(airfoil_yp)) airfoil_y = np.zeros(len(airfoil_yp)) airfoil_z = np.zeros(len(airfoil_yp)) for k in range(len(airfoil_yp)): vec_1 = [[airfoil_xp[k]], [airfoil_yp[k]], [airfoil_zp[k]]] vec_2 = np.matmul(transformation, vec_1) airfoil_x[k] = vec_2[0] airfoil_y[k] = vec_2[1] airfoil_z[k] = vec_2[2] axes.plot3D(airfoil_x, airfoil_y, airfoil_z, color='gray') if save_figure: plt.savefig(save_filename + file_type) return
def plot_propeller_geometry(axes,prop,network,network_name,prop_face_color='red',prop_edge_color='darkred',prop_alpha=1): """ This plots a 3D surface of the propeller Assumptions: None Source: None Inputs: network - network data structure Properties Used: N/A """ # unpack num_B = prop.number_of_blades a_sec = prop.airfoil_geometry a_secl = prop.airfoil_polar_stations beta = prop.twist_distribution a_o = prop.azimuthal_offset_angle b = prop.chord_distribution r = prop.radius_distribution MCA = prop.mid_chord_alignment t = prop.max_thickness_distribution origin = prop.origin n_points = 20 af_pts = n_points-1 dim = len(b) theta = np.linspace(0,2*np.pi,num_B+1)[:-1] # create empty data structure for storing geometry G = Data() rot = prop.rotation flip_1 = (np.pi/2) flip_2 = (np.pi/2) MCA_2d = np.repeat(np.atleast_2d(MCA).T,n_points,axis=1) b_2d = np.repeat(np.atleast_2d(b).T ,n_points,axis=1) t_2d = np.repeat(np.atleast_2d(t).T ,n_points,axis=1) r_2d = np.repeat(np.atleast_2d(r).T ,n_points,axis=1) shift = np.repeat(np.atleast_2d(np.ones_like(b)*b[0]).T ,n_points,axis=1) for i in range(num_B): # get airfoil coordinate geometry if a_sec != None: airfoil_data = import_airfoil_geometry(a_sec,npoints=n_points) xpts = np.take(airfoil_data.x_coordinates,a_secl,axis=0) zpts = np.take(airfoil_data.y_coordinates,a_secl,axis=0) max_t = np.take(airfoil_data.thickness_to_chord,a_secl,axis=0) else: camber = 0.02 camber_loc = 0.4 thickness = 0.10 airfoil_data = compute_naca_4series(camber, camber_loc, thickness,(n_points - 2)) xpts = np.repeat(np.atleast_2d(airfoil_data.x_coordinates) ,dim,axis=0) zpts = np.repeat(np.atleast_2d(airfoil_data.y_coordinates) ,dim,axis=0) max_t = np.repeat(airfoil_data.thickness_to_chord,dim,axis=0) # store points of airfoil in similar format as Vortex Points (i.e. in vertices) max_t2d = np.repeat(np.atleast_2d(max_t).T ,n_points,axis=1) xp = (- MCA_2d + xpts*b_2d - shift/2 ) # x-coord of airfoil yp = r_2d*np.ones_like(xp) # radial location zp = zpts*(t_2d/max_t2d) # former airfoil y coord matrix = np.zeros((len(zp),n_points,3)) # radial location, airfoil pts (same y) matrix[:,:,0] = xp*rot matrix[:,:,1] = yp matrix[:,:,2] = zp # ROTATION MATRICES FOR INNER SECTION # rotation about y axis to create twist and position blade upright trans_1 = np.zeros((dim,3,3)) trans_1[:,0,0] = np.cos(flip_1 - rot*beta) trans_1[:,0,2] = -np.sin(flip_1 - rot*beta) trans_1[:,1,1] = 1 trans_1[:,2,0] = np.sin(flip_1 - rot*beta) trans_1[:,2,2] = np.cos(flip_1 - rot*beta) # rotation about x axis to create azimuth locations trans_2 = np.array([[1 , 0 , 0], [0 , np.cos(theta[i] + a_o + flip_2 ), -np.sin(theta[i] +a_o + flip_2)], [0,np.sin(theta[i] + a_o + flip_2), np.cos(theta[i] + a_o + flip_2)]]) trans_2 = np.repeat(trans_2[ np.newaxis,:,: ],dim,axis=0) # rotation about y to orient propeller/rotor to thrust angle trans_3 = prop.prop_vel_to_body() trans_3 = np.repeat(trans_3[ np.newaxis,:,: ],dim,axis=0) # rotation 180 degrees trans_4 = np.zeros((dim,3,3)) trans_4[:,0,0] = np.cos(np.pi) trans_4[:,0,2] = -np.sin(np.pi) trans_4[:,1,1] = 1 trans_4[:,2,0] = np.sin(np.pi) trans_4[:,2,2] = np.cos(np.pi) trans = np.matmul(trans_4,np.matmul(trans_3,np.matmul(trans_2,trans_1))) rot_mat = np.repeat(trans[:, np.newaxis,:,:],n_points,axis=1) # --------------------------------------------------------------------------------------------- # ROTATE POINTS mat = np.matmul(rot_mat,matrix[...,None]).squeeze() # --------------------------------------------------------------------------------------------- # store points G.XA1 = mat[:-1,:-1,0] + origin[0][0] G.YA1 = mat[:-1,:-1,1] + origin[0][1] G.ZA1 = mat[:-1,:-1,2] + origin[0][2] G.XA2 = mat[:-1,1:,0] + origin[0][0] G.YA2 = mat[:-1,1:,1] + origin[0][1] G.ZA2 = mat[:-1,1:,2] + origin[0][2] G.XB1 = mat[1:,:-1,0] + origin[0][0] G.YB1 = mat[1:,:-1,1] + origin[0][1] G.ZB1 = mat[1:,:-1,2] + origin[0][2] G.XB2 = mat[1:,1:,0] + origin[0][0] G.YB2 = mat[1:,1:,1] + origin[0][1] G.ZB2 = mat[1:,1:,2] + origin[0][2] # ------------------------------------------------------------------------ # Plot Propeller Blade # ------------------------------------------------------------------------ for sec in range(dim-1): for loc in range(af_pts): X = [G.XA1[sec,loc], G.XB1[sec,loc], G.XB2[sec,loc], G.XA2[sec,loc]] Y = [G.YA1[sec,loc], G.YB1[sec,loc], G.YB2[sec,loc], G.YA2[sec,loc]] Z = [G.ZA1[sec,loc], G.ZB1[sec,loc], G.ZB2[sec,loc], G.ZA2[sec,loc]] prop_verts = [list(zip(X, Y, Z))] prop_collection = Poly3DCollection(prop_verts) prop_collection.set_facecolor(prop_face_color) prop_collection.set_edgecolor(prop_edge_color) prop_collection.set_alpha(prop_alpha) axes.add_collection3d(prop_collection) return
def generate_nacelle_points(nac,tessellation = 24): """ This generates the coordinate points on the surface of the nacelle Assumptions: None Source: None Inputs: Properties Used: N/A """ num_nac_segs = len(nac.Segments.keys()) theta = np.linspace(0,2*np.pi,tessellation) n_points = 20 if num_nac_segs == 0: num_nac_segs = int(n_points/2) nac_pts = np.zeros((num_nac_segs,tessellation,3)) naf = nac.Airfoil if naf.naca_4_series_airfoil != None: # use mean camber surface of airfoil camber = float(naf.naca_4_series_airfoil[0])/100 camber_loc = float(naf.naca_4_series_airfoil[1])/10 thickness = float(naf.naca_4_series_airfoil[2:])/100 airfoil_data = compute_naca_4series(camber, camber_loc, thickness,(n_points - 2)) xpts = np.repeat(np.atleast_2d(airfoil_data.x_lower_surface).T,tessellation,axis = 1)*nac.length zpts = np.repeat(np.atleast_2d(airfoil_data.camber_coordinates[0]).T,tessellation,axis = 1)*nac.length elif naf.coordinate_file != None: a_sec = naf.coordinate_file a_secl = [0] airfoil_data = import_airfoil_geometry(a_sec,npoints=num_nac_segs) xpts = np.repeat(np.atleast_2d(np.take(airfoil_data.x_coordinates,a_secl,axis=0)).T,tessellation,axis = 1)*nac.length zpts = np.repeat(np.atleast_2d(np.take(airfoil_data.y_coordinates,a_secl,axis=0)).T,tessellation,axis = 1)*nac.length else: # if no airfoil defined, use super ellipse as default a = nac.length/2 b = (nac.diameter - nac.inlet_diameter)/2 b = np.maximum(b,1E-3) # ensure xpts = np.repeat(np.atleast_2d(np.linspace(-a,a,num_nac_segs)).T,tessellation,axis = 1) zpts = (np.sqrt((b**2)*(1 - (xpts**2)/(a**2) )))*nac.length xpts = (xpts+a)*nac.length if nac.flow_through: zpts = zpts + nac.inlet_diameter/2 # create geometry theta_2d = np.repeat(np.atleast_2d(theta),num_nac_segs,axis =0) nac_pts[:,:,0] = xpts nac_pts[:,:,1] = zpts*np.cos(theta_2d) nac_pts[:,:,2] = zpts*np.sin(theta_2d) else: nac_pts = np.zeros((num_nac_segs,tessellation,3)) for i_seg in range(num_nac_segs): a = nac.Segments[i_seg].width/2 b = nac.Segments[i_seg].height/2 r = np.sqrt((b*np.sin(theta))**2 + (a*np.cos(theta))**2) nac_ypts = r*np.cos(theta) nac_zpts = r*np.sin(theta) nac_pts[i_seg,:,0] = nac.Segments[i_seg].percent_x_location*nac.length nac_pts[i_seg,:,1] = nac_ypts + nac.Segments[i_seg].percent_y_location*nac.length nac_pts[i_seg,:,2] = nac_zpts + nac.Segments[i_seg].percent_z_location*nac.length # rotation about y to orient propeller/rotor to thrust angle rot_trans = nac.nac_vel_to_body() rot_trans = np.repeat( np.repeat(rot_trans[ np.newaxis,:,: ],tessellation,axis=0)[ np.newaxis,:,:,: ],num_nac_segs,axis=0) NAC_PTS = np.matmul(rot_trans,nac_pts[...,None]).squeeze() # translate to body NAC_PTS[:,:,0] = NAC_PTS[:,:,0] + nac.origin[0][0] NAC_PTS[:,:,1] = NAC_PTS[:,:,1] + nac.origin[0][1] NAC_PTS[:,:,2] = NAC_PTS[:,:,2] + nac.origin[0][2] return NAC_PTS
def plot_vehicle(vehicle, save_figure=False, plot_control_points=True, save_filename="Vehicle_Geometry"): """This plots vortex lattice panels created when Fidelity Zero Aerodynamics Routine is initialized Assumptions: None Source: None Inputs: vehicle Outputs: Plots Properties Used: N/A """ # unpack vortex distribution VD = vehicle.vortex_distribution face_color = 'grey' edge_color = 'grey' alpha_val = 0.5 # initalize figure fig = plt.figure(save_filename) fig.set_size_inches(8, 8) axes = Axes3D(fig) axes.view_init(elev=30, azim=210) n_cp = VD.n_cp for i in range(n_cp): X = [VD.XA1[i], VD.XB1[i], VD.XB2[i], VD.XA2[i]] Y = [VD.YA1[i], VD.YB1[i], VD.YB2[i], VD.YA2[i]] Z = [VD.ZA1[i], VD.ZB1[i], VD.ZB2[i], VD.ZA2[i]] verts = [list(zip(X, Y, Z))] collection = Poly3DCollection(verts) collection.set_facecolor(face_color) collection.set_edgecolor(edge_color) collection.set_alpha(alpha_val) axes.add_collection3d(collection) max_range = np.array([ VD.X.max() - VD.X.min(), VD.Y.max() - VD.Y.min(), VD.Z.max() - VD.Z.min() ]).max() / 2.0 mid_x = (VD.X.max() + VD.X.min()) * 0.5 mid_y = (VD.Y.max() + VD.Y.min()) * 0.5 mid_z = (VD.Z.max() + VD.Z.min()) * 0.5 axes.set_xlim(mid_x - max_range, mid_x + max_range) axes.set_ylim(mid_y - max_range, mid_y + max_range) axes.set_zlim(mid_z - max_range, mid_z + max_range) if plot_control_points: axes.scatter(VD.XC, VD.YC, VD.ZC, c='r', marker='o') if 'Wake' in VD: face_color = 'white' edge_color = 'blue' alpha = 0.5 num_prop = len(VD.Wake.XA1[:, 0, 0, 0]) nts = len(VD.Wake.XA1[0, :, 0, 0]) num_B = len(VD.Wake.XA1[0, 0, :, 0]) dim_R = len(VD.Wake.XA1[0, 0, 0, :]) for p_idx in range(num_prop): for t_idx in range(nts): for B_idx in range(num_B): for loc in range(dim_R): X = [ VD.Wake.XA1[p_idx, t_idx, B_idx, loc], VD.Wake.XB1[p_idx, t_idx, B_idx, loc], VD.Wake.XB2[p_idx, t_idx, B_idx, loc], VD.Wake.XA2[p_idx, t_idx, B_idx, loc] ] Y = [ VD.Wake.YA1[p_idx, t_idx, B_idx, loc], VD.Wake.YB1[p_idx, t_idx, B_idx, loc], VD.Wake.YB2[p_idx, t_idx, B_idx, loc], VD.Wake.YA2[p_idx, t_idx, B_idx, loc] ] Z = [ VD.Wake.ZA1[p_idx, t_idx, B_idx, loc], VD.Wake.ZB1[p_idx, t_idx, B_idx, loc], VD.Wake.ZB2[p_idx, t_idx, B_idx, loc], VD.Wake.ZA2[p_idx, t_idx, B_idx, loc] ] verts = [list(zip(X, Y, Z))] collection = Poly3DCollection(verts) collection.set_facecolor(face_color) collection.set_edgecolor(edge_color) collection.set_alpha(alpha) axes.add_collection3d(collection) if np.all(VD.FUS_SURF_PTS) != None: face_color = 'grey' edge_color = 'black' alpha = 1 num_fus_segs = len(VD.FUS_SURF_PTS[:, 0, 0]) tesselation = len(VD.FUS_SURF_PTS[0, :, 0]) for i_seg in range(num_fus_segs - 1): for i_tes in range(tesselation - 1): X = [ VD.FUS_SURF_PTS[i_seg, i_tes, 0], VD.FUS_SURF_PTS[i_seg, i_tes + 1, 0], VD.FUS_SURF_PTS[i_seg + 1, i_tes + 1, 0], VD.FUS_SURF_PTS[i_seg + 1, i_tes, 0] ] Y = [ VD.FUS_SURF_PTS[i_seg, i_tes, 1], VD.FUS_SURF_PTS[i_seg, i_tes + 1, 1], VD.FUS_SURF_PTS[i_seg + 1, i_tes + 1, 1], VD.FUS_SURF_PTS[i_seg + 1, i_tes, 1] ] Z = [ VD.FUS_SURF_PTS[i_seg, i_tes, 2], VD.FUS_SURF_PTS[i_seg, i_tes + 1, 2], VD.FUS_SURF_PTS[i_seg + 1, i_tes + 1, 2], VD.FUS_SURF_PTS[i_seg + 1, i_tes, 2] ] verts = [list(zip(X, Y, Z))] collection = Poly3DCollection(verts) collection.set_facecolor(face_color) collection.set_edgecolor(edge_color) collection.set_alpha(alpha) axes.add_collection3d(collection) for propulsor in vehicle.propulsors: if ('propeller' in propulsor.keys()) or ('rotor' in propulsor.keys()): try: prop = propulsor.propeller except: prop = propulsor.rotor # ------------------------------------------------------------------------ # Generate Propeller Geoemtry # ------------------------------------------------------------------------ # unpack Rt = prop.tip_radius Rh = prop.hub_radius num_B = prop.number_blades a_sec = prop.airfoil_geometry a_secl = prop.airfoil_polar_stations beta = prop.twist_distribution b = prop.chord_distribution r = prop.radius_distribution MCA = prop.mid_chord_aligment t = prop.max_thickness_distribution ta = -propulsor.thrust_angle n_points = 10 af_pts = (2 * n_points) - 1 dim = len(b) num_props = len(prop.origin) theta = np.linspace(0, 2 * np.pi, num_B + 1)[:-1] # create empty arrays for storing geometry G = Data() G.XA1 = np.zeros((dim - 1, af_pts)) G.YA1 = np.zeros_like(G.XA1) G.ZA1 = np.zeros_like(G.XA1) G.XA2 = np.zeros_like(G.XA1) G.YA2 = np.zeros_like(G.XA1) G.ZA2 = np.zeros_like(G.XA1) G.XB1 = np.zeros_like(G.XA1) G.YB1 = np.zeros_like(G.XA1) G.ZB1 = np.zeros_like(G.XA1) G.XB2 = np.zeros_like(G.XA1) G.YB2 = np.zeros_like(G.XA1) G.ZB2 = np.zeros_like(G.XA1) for n_p in range(num_props): rot = prop.rotation[n_p] a_o = 0 flip_1 = (np.pi / 2) flip_2 = (np.pi / 2) for i in range(num_B): # get airfoil coordinate geometry airfoil_data = import_airfoil_geometry(a_sec, npoints=n_points) # store points of airfoil in similar format as Vortex Points (i.e. in vertices) for j in range(dim - 1): # loop through each radial station # -------------------------------------------- # INNER SECTION # -------------------------------------------- # INNER SECTION POINTS ixpts = airfoil_data.x_coordinates[a_secl[j]] izpts = airfoil_data.y_coordinates[a_secl[j]] iba_max_t = airfoil_data.thickness_to_chord[a_secl[j]] iba_xp = rot * (-MCA[j] + ixpts * b[j] ) # x coord of airfoil iba_yp = r[j] * np.ones_like(iba_xp) # radial location iba_zp = izpts * (t[j] / iba_max_t ) # former airfoil y coord iba_matrix = np.zeros((len(iba_zp), 3)) iba_matrix[:, 0] = iba_xp iba_matrix[:, 1] = iba_yp iba_matrix[:, 2] = iba_zp # ROTATION MATRICES FOR INNER SECTION # rotation about y axis to create twist and position blade upright iba_trans_1 = [[ np.cos(rot * flip_1 - rot * beta[j]), 0, -np.sin(rot * flip_1 - rot * beta[j]) ], [0, 1, 0], [ np.sin(rot * flip_1 - rot * beta[j]), 0, np.cos(rot * flip_1 - rot * beta[j]) ]] # rotation about x axis to create azimuth locations iba_trans_2 = [ [1, 0, 0], [ 0, np.cos(theta[i] + rot * a_o + flip_2), np.sin(theta[i] + rot * a_o + flip_2) ], [ 0, np.sin(theta[i] + rot * a_o + flip_2), np.cos(theta[i] + rot * a_o + flip_2) ] ] # roation about y to orient propeller/rotor to thrust angle iba_trans_3 = [[np.cos(ta), 0, -np.sin(ta)], [0, 1, 0], [np.sin(ta), 0, np.cos(ta)]] iba_trans = np.matmul( iba_trans_3, np.matmul(iba_trans_2, iba_trans_1)) irot_mat = np.repeat(iba_trans[np.newaxis, :, :], len(iba_yp), axis=0) # -------------------------------------------- # OUTER SECTION # -------------------------------------------- # OUTER SECTION POINTS oxpts = airfoil_data.x_coordinates[a_secl[j + 1]] ozpts = airfoil_data.y_coordinates[a_secl[j + 1]] oba_max_t = airfoil_data.thickness_to_chord[a_secl[j + 1]] oba_xp = -MCA[j + 1] + oxpts * b[ j + 1] # x coord of airfoil oba_yp = r[j + 1] * np.ones_like( oba_xp) # radial location oba_zp = ozpts * (t[j + 1] / oba_max_t ) # former airfoil y coord oba_matrix = np.zeros((len(oba_zp), 3)) oba_matrix[:, 0] = oba_xp oba_matrix[:, 1] = oba_yp oba_matrix[:, 2] = oba_zp # ROTATION MATRICES FOR OUTER SECTION # rotation about y axis to create twist and position blade upright oba_trans_1 = [ [ np.cos(rot * flip_1 - rot * beta[j + 1]), 0, -np.sin(rot * flip_1 - rot * beta[j + 1]) ], [0, 1, 0], [ np.sin(rot * flip_1 - rot * beta[j + 1]), 0, np.cos(rot * flip_1 - rot * beta[j + 1]) ] ] # rotation about x axis to create azimuth locations oba_trans_2 = [ [1, 0, 0], [ 0, np.cos(theta[i] + rot * a_o + flip_2), np.sin(theta[i] + rot * a_o + flip_2) ], [ 0, np.sin(theta[i] + rot * a_o + flip_2), np.cos(theta[i] + rot * a_o + flip_2) ] ] # roation about y to orient propeller/rotor to thrust angle oba_trans_3 = [[np.cos(ta), 0, -np.sin(ta)], [0, 1, 0], [np.sin(ta), 0, np.cos(ta)]] oba_trans = np.matmul( oba_trans_3, np.matmul(oba_trans_2, oba_trans_1)) orot_mat = np.repeat(oba_trans[np.newaxis, :, :], len(oba_yp), axis=0) # --------------------------------------------------------------------------------------------- # ROTATE POINTS iba_mat = orientation_product(irot_mat, iba_matrix) oba_mat = orientation_product(orot_mat, oba_matrix) # --------------------------------------------------------------------------------------------- # store points G.XA1[j, :] = iba_mat[:-1, 0] + prop.origin[n_p][0] G.YA1[j, :] = iba_mat[:-1, 1] + prop.origin[n_p][1] G.ZA1[j, :] = iba_mat[:-1, 2] + prop.origin[n_p][2] G.XA2[j, :] = iba_mat[1:, 0] + prop.origin[n_p][0] G.YA2[j, :] = iba_mat[1:, 1] + prop.origin[n_p][1] G.ZA2[j, :] = iba_mat[1:, 2] + prop.origin[n_p][2] G.XB1[j, :] = oba_mat[:-1, 0] + prop.origin[n_p][0] G.YB1[j, :] = oba_mat[:-1, 1] + prop.origin[n_p][1] G.ZB1[j, :] = oba_mat[:-1, 2] + prop.origin[n_p][2] G.XB2[j, :] = oba_mat[1:, 0] + prop.origin[n_p][0] G.YB2[j, :] = oba_mat[1:, 1] + prop.origin[n_p][1] G.ZB2[j, :] = oba_mat[1:, 2] + prop.origin[n_p][2] # ------------------------------------------------------------------------ # Plot Propeller Blade # ------------------------------------------------------------------------ prop_face_color = 'red' prop_edge_color = 'red' prop_alpha = 1 for sec in range(dim - 1): for loc in range(af_pts): X = [ G.XA1[sec, loc], G.XB1[sec, loc], G.XB2[sec, loc], G.XA2[sec, loc] ] Y = [ G.YA1[sec, loc], G.YB1[sec, loc], G.YB2[sec, loc], G.YA2[sec, loc] ] Z = [ G.ZA1[sec, loc], G.ZB1[sec, loc], G.ZB2[sec, loc], G.ZA2[sec, loc] ] prop_verts = [list(zip(X, Y, Z))] prop_collection = Poly3DCollection(prop_verts) prop_collection.set_facecolor(prop_face_color) prop_collection.set_edgecolor(prop_edge_color) prop_collection.set_alpha(prop_alpha) axes.add_collection3d(prop_collection) plt.axis('off') plt.grid(None) return
def generate_interpolated_airfoils(a1, a2, nairfoils, npoints=200, save_filename="Transition"): """ Takes in two airfoils, interpolates between their coordinates to generate new airfoil geometries and saves new airfoil files. Assumptions: Linear geometric transition between airfoils Source: None Inputs: a1 first airfoil [ airfoil ] a2 second airfoil [ airfoil ] nairfoils number of airfoils [ unitless ] """ # import airfoil geometry for the two airfoils airfoil_geo_files = [a1, a2] a1_name = os.path.basename(a1) a2_name = os.path.basename(a2) a_geo = import_airfoil_geometry(airfoil_geo_files, npoints) # identify x and y coordinates of the two airfoils x_upper = a_geo.x_upper_surface y_upper = a_geo.y_upper_surface x_lower = a_geo.x_lower_surface y_lower = a_geo.y_lower_surface # identify points on airfoils to interpolate between yairfoils_upper = np.array(y_upper).T yairfoils_lower = np.array(y_lower).T xairfoils_upper = np.array(x_upper).T xairfoils_lower = np.array(x_lower).T # for each point around the airfoil, interpolate between the two given airfoil coordinates z = np.linspace(0, 1, nairfoils) y_u_lb = yairfoils_upper[:, 0] y_u_ub = yairfoils_upper[:, 1] y_l_lb = yairfoils_lower[:, 0] y_l_ub = yairfoils_lower[:, 1] x_u_lb = xairfoils_upper[:, 0] x_u_ub = xairfoils_upper[:, 1] x_l_lb = xairfoils_lower[:, 0] x_l_ub = xairfoils_lower[:, 1] # broadcasting interpolation y_n_upper = (z[None, ...] * (y_u_ub[..., None] - y_u_lb[..., None]) + (y_u_lb[..., None])).T y_n_lower = (z[None, ...] * (y_l_ub[..., None] - y_l_lb[..., None]) + (y_l_lb[..., None])).T x_n_upper = (z[None, ...] * (x_u_ub[..., None] - x_u_lb[..., None]) + (x_u_lb[..., None])).T x_n_lower = (z[None, ...] * (x_l_ub[..., None] - x_l_lb[..., None]) + (x_l_lb[..., None])).T # save new airfoil geometry files: new_files = {'a_{}'.format(i + 1): [] for i in range(nairfoils - 2)} airfoil_files = [] for k in range(nairfoils - 2): # create new files and write title block for each new airfoil title_block = "Airfoil Transition " + str( k + 1) + " between " + a1_name + " and " + a2_name + "\n 61. 61.\n\n" file = 'a_' + str(k + 1) new_files[file] = open(save_filename + str(k + 1) + ".txt", "w+") new_files[file].write(title_block) y_n_u = np.reshape(y_n_upper[k + 1], (npoints // 2, 1)) y_n_l = np.reshape(y_n_lower[k + 1], (npoints // 2, 1)) x_n_u = np.reshape(x_n_upper[k + 1], (npoints // 2, 1)) x_n_l = np.reshape(x_n_lower[k + 1], (npoints // 2, 1)) airfoil_files.append(new_files[file].name) upper_data = np.append(x_n_u, y_n_u, axis=1) lower_data = np.append(x_n_l, y_n_l, axis=1) # write lines to files for lines in upper_data: #upper_data[file]: line = str(lines[0]) + " " + str(lines[1]) + "\n" new_files[file].write(line) new_files[file].write("\n") for lines in lower_data: #[file]: line = str(lines[0]) + " " + str(lines[1]) + "\n" new_files[file].write(line) new_files[file].close() # plot new and original airfoils: airfoil_files.insert(0, a1) airfoil_files.append(a2) plot_airfoil(airfoil_files, overlay=True) plot_airfoil(airfoil_files, overlay=False) return new_files
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 generate_propeller_wake_distribution(props, identical, m, VD, init_timestep_offset, time, number_of_wake_timesteps, conditions): """ This generates the propeller wake control points used to compute the influence of the wake Assumptions: None Source: None Inputs: identical - if all props are identical [Bool] m - control point [Unitless] VD - vortex distribution prop - propeller/rotor data structure init_timestep_offset - intial time step [Unitless] time - time [s] number_of_wake_timesteps - number of wake timesteps [Unitless] conditions. noise.sources.propellers - propeller noise sources data structure Properties Used: N/A """ num_prop = len(props) if identical: # All props are identical in geometry, so only the first one is unpacked prop_keys = list(props.keys()) prop_key = prop_keys[0] prop = props[prop_key] prop_outputs = conditions.noise.sources.propellers[prop_key] Bmax = int(prop.number_of_blades) nmax = int(prop_outputs.number_radial_stations - 1) else: # Props are unique, must find required matrix sizes to fit all vortex distributions prop_keys = list(props.keys()) B_list = np.ones(len(prop_keys)) Nr_list = np.ones(len(prop_keys)) for i in range(len(prop_keys)): p_key = list(props.keys())[i] p = props[p_key] p_out = conditions.noise.sources.propellers[p_key] B_list[i] = p.number_of_blades Nr_list[i] = p_out.number_radial_stations # Identify max indices for pre-allocating vortex wake distribution matrices Bmax = int(max(B_list)) nmax = int(max(Nr_list) - 1) # Add additional time step to include lifting line panel on rotor nts = number_of_wake_timesteps # Initialize empty arrays with required sizes VD, WD, Wmid = initialize_distributions(nmax, Bmax, nts, num_prop, m, VD) # for each propeller, unpack and compute for i, propi in enumerate(props): propi_key = list(props.keys())[i] if identical: propi_outputs = prop_outputs else: propi_outputs = conditions.noise.sources.propellers[propi_key] # Unpack R = propi.tip_radius r = propi.radius_distribution c = propi.chord_distribution MCA = propi.mid_chord_alignment B = propi.number_of_blades Na = propi_outputs.number_azimuthal_stations Nr = propi_outputs.number_radial_stations omega = propi_outputs.omega va = propi_outputs.disc_axial_induced_velocity V_inf = propi_outputs.velocity gamma = propi_outputs.disc_circulation blade_angles = np.linspace(0, 2 * np.pi, B + 1)[:-1] dt = time / number_of_wake_timesteps ts = np.linspace(0, time, number_of_wake_timesteps) t0 = dt * init_timestep_offset start_angle = omega[0] * t0 propi.start_angle = start_angle[0] # compute lambda and mu mean_induced_velocity = np.mean(np.mean(va, axis=1), axis=1) alpha = propi.orientation_euler_angles[1] rots = np.array([[np.cos(alpha), 0, np.sin(alpha)], [0, 1, 0], [-np.sin(alpha), 0, np.cos(alpha)]]) lambda_tot = np.atleast_2d( (np.dot(V_inf, rots[0]) + mean_induced_velocity)).T / ( omega * R) # inflow advance ratio (page 99 Leishman) mu_prop = np.atleast_2d(np.dot(V_inf, rots[2])).T / ( omega * R) # rotor advance ratio (page 99 Leishman) V_prop = np.atleast_2d( np.sqrt((V_inf[:, 0] + mean_induced_velocity)**2 + (V_inf[:, 2])**2)).T # wake skew angle wake_skew_angle = -np.arctan(mu_prop / lambda_tot) # reshape gamma to find the average between stations gamma_new = np.zeros( (m, (Nr - 1), Na) ) # [control points, Nr-1, Na ] one less radial station because ring gamma_new = (gamma[:, :-1, :] + gamma[:, 1:, :]) * 0.5 num = int(Na / B) time_idx = np.arange(nts) t_idx = np.atleast_2d(time_idx).T B_idx = np.arange(B) B_loc = (B_idx * num + t_idx) % Na Gamma = gamma_new[:, :, B_loc] Gamma = Gamma.transpose(0, 3, 1, 2) # -------------------------------------------------------------------------------------------------------------- # ( control point , blade number , radial location on blade , time step ) # -------------------------------------------------------------------------------------------------------------- sx_inf0 = np.multiply(V_prop * np.cos(wake_skew_angle), np.atleast_2d(ts)) sx_inf = np.repeat(np.repeat(sx_inf0[:, None, :], B, axis=1)[:, :, None, :], Nr, axis=2) sy_inf0 = np.multiply(np.atleast_2d(V_inf[:, 1]).T, np.atleast_2d(ts)) # = zero since no crosswind sy_inf = np.repeat(np.repeat(sy_inf0[:, None, :], B, axis=1)[:, :, None, :], Nr, axis=2) sz_inf0 = np.multiply(V_prop * np.sin(wake_skew_angle), np.atleast_2d(ts)) sz_inf = np.repeat(np.repeat(sz_inf0[:, None, :], B, axis=1)[:, :, None, :], Nr, axis=2) angle_offset = np.repeat(np.repeat(np.multiply( omega, np.atleast_2d(ts))[:, None, :], B, axis=1)[:, :, None, :], Nr, axis=2) blade_angle_loc = np.repeat(np.repeat(np.tile( np.atleast_2d(blade_angles), (m, 1))[:, :, None], Nr, axis=2)[:, :, :, None], number_of_wake_timesteps, axis=3) start_angle_offset = np.repeat(np.repeat( np.atleast_2d(start_angle)[:, None, :], B, axis=1)[:, :, None, :], Nr, axis=2) total_angle_offset = angle_offset - start_angle_offset if (propi.rotation != None) and (propi.rotation == 1): total_angle_offset = -total_angle_offset azi_y = np.sin(blade_angle_loc + total_angle_offset) azi_z = np.cos(blade_angle_loc + total_angle_offset) # extract airfoil trailing edge coordinates for initial location of vortex wake a_sec = propi.airfoil_geometry a_secl = propi.airfoil_polar_stations airfoil_data = import_airfoil_geometry(a_sec, npoints=100) # trailing edge points in airfoil coordinates xupper = np.take(airfoil_data.x_upper_surface, a_secl, axis=0) yupper = np.take(airfoil_data.y_upper_surface, a_secl, axis=0) # Align the quarter chords of the airfoils (zero sweep) airfoil_le_offset = (c[0] / 2 - c / 2) xte_airfoils = xupper[:, -1] * c + airfoil_le_offset yte_airfoils = yupper[:, -1] * c xle_airfoils = xupper[:, 0] * c + airfoil_le_offset yle_airfoils = yupper[:, 0] * c x_c_4_airfoils = (xle_airfoils - xte_airfoils) / 4 - airfoil_le_offset y_c_4_airfoils = (yle_airfoils - yte_airfoils) / 4 x_cp_airfoils = 1 * (xle_airfoils - xte_airfoils) / 2 - airfoil_le_offset y_cp_airfoils = 1 * (yle_airfoils - yte_airfoils) / 2 # apply blade twist rotation along rotor radius beta = propi.twist_distribution xte_twisted = np.cos(beta) * xte_airfoils - np.sin(beta) * yte_airfoils yte_twisted = np.sin(beta) * xte_airfoils + np.cos(beta) * yte_airfoils x_c_4_twisted = np.cos(beta) * x_c_4_airfoils - np.sin( beta) * y_c_4_airfoils y_c_4_twisted = np.sin(beta) * x_c_4_airfoils + np.cos( beta) * y_c_4_airfoils x_cp_twisted = np.cos(beta) * x_cp_airfoils - np.sin( beta) * y_cp_airfoils y_cp_twisted = np.sin(beta) * x_cp_airfoils + np.cos( beta) * y_cp_airfoils # transform coordinates from airfoil frame to rotor frame xte = np.tile(np.atleast_2d(yte_twisted), (B, 1)) xte_rotor = np.tile(xte[None, :, :, None], (m, 1, 1, number_of_wake_timesteps)) yte_rotor = -np.tile(xte_twisted[None, None, :, None], (m, B, 1, 1)) * np.cos(blade_angle_loc + total_angle_offset) zte_rotor = np.tile( xte_twisted[None, None, :, None], (m, B, 1, 1)) * np.sin(blade_angle_loc + total_angle_offset) r_4d = np.tile(r[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) x0 = 0 y0 = r_4d * azi_y z0 = r_4d * azi_z x_pts0 = x0 + xte_rotor y_pts0 = y0 + yte_rotor z_pts0 = z0 + zte_rotor x_c_4_rotor = x0 - np.tile(y_c_4_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) y_c_4_rotor = y0 + np.tile( x_c_4_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) * np.cos(blade_angle_loc + total_angle_offset) z_c_4_rotor = z0 - np.tile( x_c_4_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) * np.sin(blade_angle_loc + total_angle_offset) x_cp_rotor = x0 - np.tile(y_cp_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) y_cp_rotor = y0 + np.tile( x_cp_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) * np.cos(blade_angle_loc + total_angle_offset) z_cp_rotor = z0 - np.tile( x_cp_twisted[None, None, :, None], (m, B, 1, number_of_wake_timesteps)) * np.sin(blade_angle_loc + total_angle_offset) # compute wake contraction, apply to y-z plane X_pts0 = x_pts0 + sx_inf wake_contraction = compute_wake_contraction_matrix( i, propi, Nr, m, number_of_wake_timesteps, X_pts0, propi_outputs) Y_pts0 = y_pts0 * wake_contraction + sy_inf Z_pts0 = z_pts0 * wake_contraction + sz_inf # Rotate wake by thrust angle rot_to_body = propi.prop_vel_to_body( ) # rotate points into the body frame: [Z,Y,X]' = R*[Z,Y,X] # append propeller wake to each of its repeated origins X_pts = propi.origin[0][0] + X_pts0 * rot_to_body[ 2, 2] + Z_pts0 * rot_to_body[2, 0] Y_pts = propi.origin[0][1] + Y_pts0 * rot_to_body[1, 1] Z_pts = propi.origin[0][2] + Z_pts0 * rot_to_body[ 0, 0] + X_pts0 * rot_to_body[0, 2] #------------------------------------------------------ # Account for lifting line panels #------------------------------------------------------ rots = np.array([[np.cos(alpha), 0, np.sin(alpha)], [0, 1, 0], [-np.sin(alpha), 0, np.cos(alpha)]]) # rotate rotor points to incidence angle x_c_4 = x_c_4_rotor * rots[0, 0] + y_c_4_rotor * rots[ 0, 1] + z_c_4_rotor * rots[0, 2] y_c_4 = x_c_4_rotor * rots[1, 0] + y_c_4_rotor * rots[ 1, 1] + z_c_4_rotor * rots[1, 2] z_c_4 = x_c_4_rotor * rots[2, 0] + y_c_4_rotor * rots[ 2, 1] + z_c_4_rotor * rots[2, 2] # prepend points at quarter chord to account for rotor lifting line X_pts = np.append(x_c_4[:, :, :, 0][:, :, :, None], X_pts, axis=3) Y_pts = np.append(y_c_4[:, :, :, 0][:, :, :, None], Y_pts, axis=3) Z_pts = np.append(z_c_4[:, :, :, 0][:, :, :, None], Z_pts, axis=3) #------------------------------------------------------ # Store points #------------------------------------------------------ # ( control point, prop , blade number , location on blade, time step ) if (propi.rotation != None) and (propi.rotation == -1): Wmid.WD_XA1[:, i, 0:B, :, :] = X_pts[:, :, :-1, :-1] Wmid.WD_YA1[:, i, 0:B, :, :] = Y_pts[:, :, :-1, :-1] Wmid.WD_ZA1[:, i, 0:B, :, :] = Z_pts[:, :, :-1, :-1] Wmid.WD_XA2[:, i, 0:B, :, :] = X_pts[:, :, :-1, 1:] Wmid.WD_YA2[:, i, 0:B, :, :] = Y_pts[:, :, :-1, 1:] Wmid.WD_ZA2[:, i, 0:B, :, :] = Z_pts[:, :, :-1, 1:] Wmid.WD_XB1[:, i, 0:B, :, :] = X_pts[:, :, 1:, :-1] Wmid.WD_YB1[:, i, 0:B, :, :] = Y_pts[:, :, 1:, :-1] Wmid.WD_ZB1[:, i, 0:B, :, :] = Z_pts[:, :, 1:, :-1] Wmid.WD_XB2[:, i, 0:B, :, :] = X_pts[:, :, 1:, 1:] Wmid.WD_YB2[:, i, 0:B, :, :] = Y_pts[:, :, 1:, 1:] Wmid.WD_ZB2[:, i, 0:B, :, :] = Z_pts[:, :, 1:, 1:] else: Wmid.WD_XA1[:, i, 0:B, :, :] = X_pts[:, :, 1:, :-1] Wmid.WD_YA1[:, i, 0:B, :, :] = Y_pts[:, :, 1:, :-1] Wmid.WD_ZA1[:, i, 0:B, :, :] = Z_pts[:, :, 1:, :-1] Wmid.WD_XA2[:, i, 0:B, :, :] = X_pts[:, :, 1:, 1:] Wmid.WD_YA2[:, i, 0:B, :, :] = Y_pts[:, :, 1:, 1:] Wmid.WD_ZA2[:, i, 0:B, :, :] = Z_pts[:, :, 1:, 1:] Wmid.WD_XB1[:, i, 0:B, :, :] = X_pts[:, :, :-1, :-1] Wmid.WD_YB1[:, i, 0:B, :, :] = Y_pts[:, :, :-1, :-1] Wmid.WD_ZB1[:, i, 0:B, :, :] = Z_pts[:, :, :-1, :-1] Wmid.WD_XB2[:, i, 0:B, :, :] = X_pts[:, :, :-1, 1:] Wmid.WD_YB2[:, i, 0:B, :, :] = Y_pts[:, :, :-1, 1:] Wmid.WD_ZB2[:, i, 0:B, :, :] = Z_pts[:, :, :-1, 1:] Wmid.WD_GAMMA[:, i, 0:B, :, :] = Gamma # store points for plotting VD.Wake.XA1[:, i, 0:B, :, :] = X_pts[:, :, :-1, :-1] VD.Wake.YA1[:, i, 0:B, :, :] = Y_pts[:, :, :-1, :-1] VD.Wake.ZA1[:, i, 0:B, :, :] = Z_pts[:, :, :-1, :-1] VD.Wake.XA2[:, i, 0:B, :, :] = X_pts[:, :, :-1, 1:] VD.Wake.YA2[:, i, 0:B, :, :] = Y_pts[:, :, :-1, 1:] VD.Wake.ZA2[:, i, 0:B, :, :] = Z_pts[:, :, :-1, 1:] VD.Wake.XB1[:, i, 0:B, :, :] = X_pts[:, :, 1:, :-1] VD.Wake.YB1[:, i, 0:B, :, :] = Y_pts[:, :, 1:, :-1] VD.Wake.ZB1[:, i, 0:B, :, :] = Z_pts[:, :, 1:, :-1] VD.Wake.XB2[:, i, 0:B, :, :] = X_pts[:, :, 1:, 1:] VD.Wake.YB2[:, i, 0:B, :, :] = Y_pts[:, :, 1:, 1:] VD.Wake.ZB2[:, i, 0:B, :, :] = Z_pts[:, :, 1:, 1:] # Append wake geometry and vortex strengths to each individual propeller propi.Wake_VD.XA1 = VD.Wake.XA1[:, i, 0:B, :, :] propi.Wake_VD.YA1 = VD.Wake.YA1[:, i, 0:B, :, :] propi.Wake_VD.ZA1 = VD.Wake.ZA1[:, i, 0:B, :, :] propi.Wake_VD.XA2 = VD.Wake.XA2[:, i, 0:B, :, :] propi.Wake_VD.YA2 = VD.Wake.YA2[:, i, 0:B, :, :] propi.Wake_VD.ZA2 = VD.Wake.ZA2[:, i, 0:B, :, :] propi.Wake_VD.XB1 = VD.Wake.XB1[:, i, 0:B, :, :] propi.Wake_VD.YB1 = VD.Wake.YB1[:, i, 0:B, :, :] propi.Wake_VD.ZB1 = VD.Wake.ZB1[:, i, 0:B, :, :] propi.Wake_VD.XB2 = VD.Wake.XB2[:, i, 0:B, :, :] propi.Wake_VD.YB2 = VD.Wake.YB2[:, i, 0:B, :, :] propi.Wake_VD.ZB2 = VD.Wake.ZB2[:, i, 0:B, :, :] propi.Wake_VD.GAMMA = Wmid.WD_GAMMA[:, i, 0:B, :, :] # append trailing edge locations propi.Wake_VD.Xblades_te = X_pts[0, :, :, 0] propi.Wake_VD.Yblades_te = Y_pts[0, :, :, 0] propi.Wake_VD.Zblades_te = Z_pts[0, :, :, 0] # append quarter chord lifting line point locations propi.Wake_VD.Xblades_c_4 = x_c_4_rotor propi.Wake_VD.Yblades_c_4 = y_c_4_rotor propi.Wake_VD.Zblades_c_4 = z_c_4_rotor # append three-quarter chord evaluation point locations propi.Wake_VD.Xblades_cp = x_cp_rotor # propi.Wake_VD.Yblades_cp = y_cp_rotor # propi.Wake_VD.Zblades_cp = z_cp_rotor # propi.Wake_VD.Xblades_cp2 = X_pts[0, :, :, 0] + (X_pts[0, :, :, 0] - X_pts[0, :, :, 1]) / 2 propi.Wake_VD.Yblades_cp2 = Y_pts[0, :, :, 0] + (Y_pts[0, :, :, 0] - Y_pts[0, :, :, 1]) / 2 propi.Wake_VD.Zblades_cp2 = Z_pts[0, :, :, 0] + (Z_pts[0, :, :, 0] - Z_pts[0, :, :, 1]) / 2 # Compress Data into 1D Arrays mat6_size = (m, num_prop * nts * Bmax * nmax) WD.XA1 = np.reshape(Wmid.WD_XA1, mat6_size) WD.YA1 = np.reshape(Wmid.WD_YA1, mat6_size) WD.ZA1 = np.reshape(Wmid.WD_ZA1, mat6_size) WD.XA2 = np.reshape(Wmid.WD_XA2, mat6_size) WD.YA2 = np.reshape(Wmid.WD_YA2, mat6_size) WD.ZA2 = np.reshape(Wmid.WD_ZA2, mat6_size) WD.XB1 = np.reshape(Wmid.WD_XB1, mat6_size) WD.YB1 = np.reshape(Wmid.WD_YB1, mat6_size) WD.ZB1 = np.reshape(Wmid.WD_ZB1, mat6_size) WD.XB2 = np.reshape(Wmid.WD_XB2, mat6_size) WD.YB2 = np.reshape(Wmid.WD_YB2, mat6_size) WD.ZB2 = np.reshape(Wmid.WD_ZB2, mat6_size) WD.GAMMA = np.reshape(Wmid.WD_GAMMA, mat6_size) return WD, dt, ts, B, Nr
def write_avl_airfoil_file(suave_airfoil_filename): """ This function writes the standard airfoil file format from Airfoil tools to avl file format Assumptions: None Source: Inputs: filename Outputs: airfoil.dat Properties Used: N/A """ # unpack avl_inputs avl_airfoil_filename = suave_airfoil_filename.split(".")[-2].split( "/")[-1] + '.dat' # purge file purge_files([avl_airfoil_filename]) # read airfoil file header origin = os.getcwd() os_path = os.path.split(origin)[0] f_path = os_path + '/' + suave_airfoil_filename f = open(f_path) data_block = f.readlines() f.close() airfoil_name = data_block[0].strip() # import airfoil coordinates airfoil_geometry_data = import_airfoil_geometry([f_path]) dim = len(airfoil_geometry_data.x_coordinates[0]) # write file with open(avl_airfoil_filename, 'w') as afile: afile.write(airfoil_name + "\n") for i in range(dim - 1): if i == int(dim / 2): pass elif airfoil_geometry_data.y_coordinates[0][i] < 0.0: case_text = '\t' + format( airfoil_geometry_data.x_coordinates[0][i], '.7f') + " " + format( airfoil_geometry_data.y_coordinates[0][i], '.7f') + "\n" afile.write(case_text) else: case_text = '\t' + format( airfoil_geometry_data.x_coordinates[0][i], '.7f') + " " + format( airfoil_geometry_data.y_coordinates[0][i], '.7f') + "\n" afile.write(case_text) afile.close() return avl_airfoil_filename
def main(): # Define Panelization npanel = 200 # ----------------------------------------------- # Batch analysis of single airfoil - NACA 2410 # ----------------------------------------------- Re_batch = np.atleast_2d(np.array([1E5, 2E5])).T AoA_batch = np.atleast_2d(np.linspace(-5, 10, 4) * Units.degrees).T airfoil_geometry = compute_naca_4series(0.02, 0.4, 0.1, npoints=npanel) airfoil_properties_1 = airfoil_analysis(airfoil_geometry, AoA_batch, Re_batch, npanel, batch_analysis=True) # Plots plot_airfoil_analysis_polars(airfoil_properties_1, show_legend=True) plot_airfoil_analysis_surface_forces(airfoil_properties_1, show_legend=True) plot_airfoil_analysis_boundary_layer_properties(airfoil_properties_1, show_legend=True) # XFOIL Validation - Source : xfoil_data_Cl = 0.7922 xfoil_data_Cd = 0.01588 xfoil_data_Cm = -0.0504 diff_CL = np.abs(airfoil_properties_1.Cl[2, 0] - xfoil_data_Cl) expected_Cl_error = -0.01801472136594917 print('\nCL difference') print(diff_CL) assert np.abs( ((airfoil_properties_1.Cl[2, 0] - expected_Cl_error) - xfoil_data_Cl) / xfoil_data_Cl) < 1e-6 diff_CD = np.abs(airfoil_properties_1.Cd[2, 0] - xfoil_data_Cd) expected_Cd_error = -0.00012125071270768784 print('\nCD difference') print(diff_CD) assert np.abs( ((airfoil_properties_1.Cd[2, 0] - expected_Cd_error) - xfoil_data_Cd) / xfoil_data_Cd) < 1e-6 diff_CM = np.abs(airfoil_properties_1.Cm[2, 0] - xfoil_data_Cm) expected_Cm_error = -0.002782281182096287 print('\nCM difference') print(diff_CM) assert np.abs( ((airfoil_properties_1.Cm[2, 0] - expected_Cm_error) - xfoil_data_Cm) / xfoil_data_Cm) < 1e-6 # ----------------------------------------------- # Single Condition Analysis of multiple airfoils # ----------------------------------------------- ospath = os.path.abspath(__file__) separator = os.path.sep rel_path = ospath.split( 'airfoil_analysis' + separator + 'airfoil_panel_method_test.py' )[0] + 'Vehicles' + separator + 'Airfoils' + separator Re_vals = np.atleast_2d(np.array([1E5, 1E5, 1E5, 1E5, 1E5, 1E5])).T AoA_vals = np.atleast_2d(np.array([2, 2, 2, 2, 2, 2]) * Units.degrees).T airfoil_stations = [0, 1, 0, 1, 0, 1] airfoils = [rel_path + 'NACA_4412.txt', rel_path + 'Clark_y.txt'] airfoil_geometry = import_airfoil_geometry(airfoils, npoints=(npanel + 2)) airfoil_properties_2 = airfoil_analysis(airfoil_geometry, AoA_vals, Re_vals, npanel, batch_analysis=False, airfoil_stations=airfoil_stations) True_Cls = np.array([[0.68788905], [0.60906619], [0.68788905], [0.60906619], [0.68788905], [0.60906619]]) True_Cds = np.array([[0.01015395], [0.00846174], [0.01015395], [0.00846174], [0.01015395], [0.00846174]]) True_Cms = np.array([[-0.10478782], [-0.08727453], [-0.10478782], [-0.08727453], [-0.10478782], [-0.08727453]]) print('\n\nSingle Point Validation') print('\nCL difference') print(np.sum(np.abs((airfoil_properties_2.Cl - True_Cls) / True_Cls))) assert np.sum(np.abs( (airfoil_properties_2.Cl - True_Cls) / True_Cls)) < 1e-5 print('\nCD difference') print(np.sum(np.abs((airfoil_properties_2.Cd - True_Cds) / True_Cds))) assert np.sum(np.abs( (airfoil_properties_2.Cd - True_Cds) / True_Cds)) < 1e-5 print('\nCM difference') print(np.sum(np.abs((airfoil_properties_2.Cm - True_Cms) / True_Cms))) assert np.sum(np.abs( (airfoil_properties_2.Cm - True_Cms) / True_Cms)) < 1e-5 return
def generate_lofted_propeller_points(prop): """ Generates nodes on the lofted propeller. Inputs: prop Data structure of SUAVE propeller [Unitless] Outputs: N/A Properties Used: N/A Assumptions: Quad cell structures for mesh Source: None """ num_B = prop.number_of_blades a_sec = prop.airfoil_geometry a_secl = prop.airfoil_polar_stations beta = prop.twist_distribution b = prop.chord_distribution r = prop.radius_distribution MCA = prop.mid_chord_alignment t = prop.max_thickness_distribution ta = prop.orientation_euler_angles[1] origin = prop.origin try: a_o = -prop.start_angle[0] except: # default is no azimuthal offset (blade 1 starts vertical) a_o = 0.0 n_r = len(b) # number radial points n_a_loft = prop.number_points_around_airfoil # number points around airfoil n_a_cw = n_a_loft//2 # number of airfoil chordwise points theta = np.linspace(0,2*np.pi,num_B+1)[:-1] # azimuthal stations # create empty data structure for storing propeller geometries G = Data() Gprops = Data() Gprops.n_af = n_a_loft rot = prop.rotation flip_1 = (np.pi/2) flip_2 = (np.pi/2) MCA_2d = np.repeat(np.atleast_2d(MCA).T,n_a_loft,axis=1) b_2d = np.repeat(np.atleast_2d(b).T ,n_a_loft,axis=1) t_2d = np.repeat(np.atleast_2d(t).T ,n_a_loft,axis=1) r_2d = np.repeat(np.atleast_2d(r).T ,n_a_loft,axis=1) for i in range(num_B): Gprops[i] = Data() # get airfoil coordinate geometry if a_sec != None: airfoil_data = import_airfoil_geometry(a_sec,npoints=n_a_loft) xpts = np.take(airfoil_data.x_coordinates,a_secl,axis=0) zpts = np.take(airfoil_data.y_coordinates,a_secl,axis=0) max_t = np.take(airfoil_data.thickness_to_chord,a_secl,axis=0) else: camber = 0.02 camber_loc = 0.4 thickness = 0.10 airfoil_data = compute_naca_4series(camber, camber_loc, thickness,(n_a_loft - 2)) xpts = np.repeat(np.atleast_2d(airfoil_data.x_coordinates) ,n_r,axis=0) zpts = np.repeat(np.atleast_2d(airfoil_data.y_coordinates) ,n_r,axis=0) max_t = np.repeat(airfoil_data.thickness_to_chord,n_r,axis=0) # store points of airfoil in similar format as Vortex Points (i.e. in vertices) max_t2d = np.repeat(np.atleast_2d(max_t).T ,n_a_loft,axis=1) airfoil_le_offset = (b[0]/2 - np.repeat(b[:,None], n_a_loft, axis=1)/2 ) # no sweep xp = rot*(- MCA_2d + xpts*b_2d + airfoil_le_offset) # x coord of airfoil yp = r_2d*np.ones_like(xp) # radial location zp = zpts*(t_2d/max_t2d) # former airfoil y coord matrix = np.zeros((n_r,n_a_loft,3)) # radial location, airfoil pts (same y) matrix[:,:,0] = xp matrix[:,:,1] = yp matrix[:,:,2] = zp # ROTATION MATRICES FOR INNER SECTION # rotation about y axis to create twist and position blade upright trans_1 = np.zeros((n_r,3,3)) trans_1[:,0,0] = np.cos(rot*flip_1 - rot*beta) trans_1[:,0,2] = -np.sin(rot*flip_1 - rot*beta) trans_1[:,1,1] = 1 trans_1[:,2,0] = np.sin(rot*flip_1 - rot*beta) trans_1[:,2,2] = np.cos(rot*flip_1 - rot*beta) # rotation about x axis to create azimuth locations trans_2 = np.array([[1 , 0 , 0], [0 , np.cos(theta[i] + rot*a_o + flip_2 ), -np.sin(theta[i] + rot*a_o + flip_2)], [0,np.sin(theta[i] + rot*a_o + flip_2), np.cos(theta[i] + rot*a_o + flip_2)] ]) trans_2 = np.repeat(trans_2[ np.newaxis,:,: ],n_r,axis=0) # rotation about y to orient propeller/rotor to thrust angle trans_3 = prop.body_to_prop_vel() #prop.prop_vel_to_body() trans_3 = np.repeat(trans_3[ np.newaxis,:,: ],n_r,axis=0) trans = np.matmul(trans_3,np.matmul(trans_2,trans_1)) rot_mat = np.repeat(trans[:, np.newaxis,:,:],n_a_loft,axis=1) # --------------------------------------------------------------------------------------------- # ROTATE POINTS mat = np.matmul(rot_mat,matrix[...,None]).squeeze() # --------------------------------------------------------------------------------------------- # store node points G.X = mat[:,:,0] + origin[0][0] G.Y = mat[:,:,1] + origin[0][1] G.Z = mat[:,:,2] + origin[0][2] # store cell points G.XA1 = mat[:-1,:-1,0] + origin[0][0] G.YA1 = mat[:-1,:-1,1] + origin[0][1] G.ZA1 = mat[:-1,:-1,2] + origin[0][2] G.XA2 = mat[:-1,1:,0] + origin[0][0] G.YA2 = mat[:-1,1:,1] + origin[0][1] G.ZA2 = mat[:-1,1:,2] + origin[0][2] G.XB1 = mat[1:,:-1,0] + origin[0][0] G.YB1 = mat[1:,:-1,1] + origin[0][1] G.ZB1 = mat[1:,:-1,2] + origin[0][2] G.XB2 = mat[1:,1:,0] + origin[0][0] G.YB2 = mat[1:,1:,1] + origin[0][1] G.ZB2 = mat[1:,1:,2] + origin[0][2] # Store G for this blade: Gprops[i] = copy.deepcopy(G) return Gprops
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 generate_propeller_geometry(prop, angle_offset=0): """This plots the geoemtry of a propeller or rotor Assumptions: None Source: None Inputs: SUAVE.Components.Energy.Converters.Propeller() Outputs: Plots Properties Used: N/A """ # unpack # ------------------------------------------------------------------------ # Generate Propeller Geoemtry # ------------------------------------------------------------------------ # unpack Rt = prop.tip_radius Rh = prop.hub_radius num_B = prop.number_blades a_sec = prop.airfoil_geometry a_secl = prop.airfoil_polar_stations beta = prop.twist_distribution b = prop.chord_distribution r = prop.radius_distribution MCA = prop.mid_chord_aligment t = prop.max_thickness_distribution airfoil_pts = 20 dim = len(b) num_props = len(prop.origin) theta = np.linspace(0, 2 * np.pi, num_B + 1)[:-1] if any(r) == None: r = np.linspace(Rh, Rt, len(b)) # create empty arrays for storing geometry G = Data() G.XA1 = np.zeros((num_props, num_B, dim - 1, (2 * airfoil_pts) - 1)) G.YA1 = np.zeros_like(G.XA1) G.ZA1 = np.zeros_like(G.XA1) G.XA2 = np.zeros_like(G.XA1) G.YA2 = np.zeros_like(G.XA1) G.ZA2 = np.zeros_like(G.XA1) G.XB1 = np.zeros_like(G.XA1) G.YB1 = np.zeros_like(G.XA1) G.ZB1 = np.zeros_like(G.XA1) G.XB2 = np.zeros_like(G.XA1) G.YB2 = np.zeros_like(G.XA1) G.ZB2 = np.zeros_like(G.XA1) for n_p in range(num_props): a_o = prop.rotation[n_p] * angle_offset flip_1 = (np.pi / 2) * prop.rotation[n_p] flip_2 = (np.pi / 2) * prop.rotation[n_p] if prop.rotation[n_p] == -1: flip_3 = -np.pi else: flip_3 = 0 for i in range(num_B): # check if airfoils are defined if a_sec != None and a_secl != None: # check dimension of section dim_sec = len(a_secl) if dim_sec != dim: raise AssertionError( "Number of sections not equal to number of stations") # get airfoil coordinate geometry airfoil_data = import_airfoil_geometry(a_sec, npoints=airfoil_pts) # if no airfoil defined, use NACA 0012 else: airfoil_data = compute_naca_4series(0, 0, 12, npoints=(airfoil_pts * 2) - 2) a_secl = np.zeros(dim).astype(int) # store points of airfoil in similar format as Vortex Points (i.e. in vertices) for j in range(dim - 1): # loop through each radial station # iba - imboard airfoil section iba_max_t = airfoil_data.thickness_to_chord[a_secl[j]] iba_xp = b[j] - MCA[j] - airfoil_data.x_coordinates[ a_secl[j]] * b[j] # x coord of airfoil iba_yp = r[j] * np.ones_like(iba_xp) # radial location iba_zp = airfoil_data.y_coordinates[a_secl[j]] * b[j] * ( t[j] / iba_max_t) # former airfoil y coord iba_trans_1 = [ [np.cos(beta[j] + flip_3), 0, -np.sin(beta[j] + flip_3)], [0, 1, 0], [np.sin(beta[j] + flip_3), 0, np.cos(beta[j] + flip_3)] ] iba_trans_2 = [[ np.cos(theta[i] + a_o), -np.sin(theta[i] + a_o), 0 ], [np.sin(theta[i] + a_o), np.cos(theta[i] + a_o), 0], [0, 0, 1]] iba_trans_3 = [[1, 0, 0], [0, np.cos(flip_1), np.sin(flip_1)], [0, np.sin(flip_1), np.cos(flip_1)]] iba_trans_4 = [[np.cos(flip_2), -np.sin(flip_2), 0], [np.sin(flip_2), np.cos(flip_2), 0], [0, 0, 1]] iba_trans = np.matmul( iba_trans_4, np.matmul(iba_trans_3, np.matmul(iba_trans_2, iba_trans_1))) # oba - outboard airfoil section oba_max_t = airfoil_data.thickness_to_chord[a_secl[j + 1]] oba_xp = b[j + 1] - MCA[j + 1] - airfoil_data.x_coordinates[ a_secl[j + 1]] * b[j + 1] # x coord of airfoil oba_yp = r[j + 1] * np.ones_like(oba_xp) # radial location oba_zp = airfoil_data.y_coordinates[a_secl[j + 1]] * b[ j + 1] * (t[j + 1] / oba_max_t) # former airfoil y coord oba_trans_1 = [[ np.cos(beta[j + 1] + flip_3), 0, -np.sin(beta[j + 1] + flip_3) ], [0, 1, 0], [ np.sin(beta[j + 1] + flip_3), 0, np.cos(beta[j + 1] + flip_3) ]] oba_trans_2 = [[ np.cos(theta[i] + a_o), -np.sin(theta[i] + a_o), 0 ], [np.sin(theta[i] + a_o), np.cos(theta[i] + a_o), 0], [0, 0, 1]] oba_trans_3 = [[1, 0, 0], [0, np.cos(flip_1), np.sin(flip_1)], [0, np.sin(flip_1), np.cos(flip_1)]] oba_trans_4 = [[np.cos(flip_2), -np.sin(flip_2), 0], [np.sin(flip_2), np.cos(flip_2), 0], [0, 0, 1]] oba_trans = np.matmul( oba_trans_4, np.matmul(oba_trans_3, np.matmul(oba_trans_2, oba_trans_1))) iba_x = np.zeros(len(iba_xp)) iba_y = np.zeros(len(iba_yp)) iba_z = np.zeros(len(iba_zp)) oba_x = np.zeros(len(oba_xp)) oba_y = np.zeros(len(oba_yp)) oba_z = np.zeros(len(oba_zp)) for k in range(len(iba_yp)): iba_vec_1 = [[iba_xp[k]], [iba_yp[k]], [iba_zp[k]]] iba_vec_2 = np.matmul(iba_trans, iba_vec_1) iba_x[k] = iba_vec_2[0] iba_y[k] = iba_vec_2[1] iba_z[k] = iba_vec_2[2] oba_vec_1 = [[oba_xp[k]], [oba_yp[k]], [oba_zp[k]]] oba_vec_2 = np.matmul(oba_trans, oba_vec_1) oba_x[k] = oba_vec_2[0] oba_y[k] = oba_vec_2[1] oba_z[k] = oba_vec_2[2] # store points G.XA1[n_p, i, j, :] = iba_x[:-1] + prop.origin[n_p][0] G.YA1[n_p, i, j, :] = iba_y[:-1] + prop.origin[n_p][1] G.ZA1[n_p, i, j, :] = iba_z[:-1] + prop.origin[n_p][2] G.XA2[n_p, i, j, :] = iba_x[1:] + prop.origin[n_p][0] G.YA2[n_p, i, j, :] = iba_y[1:] + prop.origin[n_p][1] G.ZA2[n_p, i, j, :] = iba_z[1:] + prop.origin[n_p][2] G.XB1[n_p, i, j, :] = oba_x[:-1] + prop.origin[n_p][0] G.YB1[n_p, i, j, :] = oba_y[:-1] + prop.origin[n_p][1] G.ZB1[n_p, i, j, :] = oba_z[:-1] + prop.origin[n_p][2] G.XB2[n_p, i, j, :] = oba_x[1:] + prop.origin[n_p][0] G.YB2[n_p, i, j, :] = oba_y[1:] + prop.origin[n_p][1] G.ZB2[n_p, i, j, :] = oba_z[1:] + prop.origin[n_p][2] return G