def update_forces(segment, state): # unpack conditions = state.conditions # unpack forces wind_lift_force_vector = conditions.frames.wind.lift_force_vector wind_drag_force_vector = conditions.frames.wind.drag_force_vector body_thrust_force_vector = conditions.frames.body.thrust_force_vector inertial_gravity_force_vector = conditions.frames.inertial.gravity_force_vector # unpack transformation matrices T_body2inertial = conditions.frames.body.transform_to_inertial T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial, wind_lift_force_vector) D = orientation_product(T_wind2inertial, wind_drag_force_vector) T = orientation_product(T_body2inertial, body_thrust_force_vector) W = inertial_gravity_force_vector # sum of the forces F = L + D + T + W # like a boss # pack conditions.frames.inertial.total_force_vector[:, :] = F[:, :] return
def update_forces(segment,state): # unpack conditions = state.conditions # unpack forces wind_lift_force_vector = conditions.frames.wind.lift_force_vector wind_drag_force_vector = conditions.frames.wind.drag_force_vector body_thrust_force_vector = conditions.frames.body.thrust_force_vector inertial_gravity_force_vector = conditions.frames.inertial.gravity_force_vector # unpack transformation matrices T_body2inertial = conditions.frames.body.transform_to_inertial T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial,wind_lift_force_vector) D = orientation_product(T_wind2inertial,wind_drag_force_vector) T = orientation_product(T_body2inertial,body_thrust_force_vector) W = inertial_gravity_force_vector # sum of the forces F = L + D + T + W # like a boss # pack conditions.frames.inertial.total_force_vector[:,:] = F[:,:] return
def update_forces(segment,state): """ Summation of forces: lift, drag, thrust, weight. Future versions will include new definitions of dreams, FAA, money, and reality. Assumptions: You only have these 4 forces applied to the vehicle Inputs: state.conditions: frames.wind.lift_force_vector [newtons] frames.wind.drag_force_vector [newtons] frames.inertial.gravity_force_vector [newtons] frames.body.thrust_force_vector [newtons] frames.body.transform_to_inertial [newtons] frames.wind.transform_to_inertial [newtons] Outputs: state.conditions: frames.inertial.total_force_vector [newtons] Properties Used: N/A """ # unpack conditions = state.conditions # unpack forces wind_lift_force_vector = conditions.frames.wind.lift_force_vector wind_drag_force_vector = conditions.frames.wind.drag_force_vector body_thrust_force_vector = conditions.frames.body.thrust_force_vector inertial_gravity_force_vector = conditions.frames.inertial.gravity_force_vector # unpack transformation matrices T_body2inertial = conditions.frames.body.transform_to_inertial T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial,wind_lift_force_vector) D = orientation_product(T_wind2inertial,wind_drag_force_vector) T = orientation_product(T_body2inertial,body_thrust_force_vector) W = inertial_gravity_force_vector # sum of the forces F = L + D + T + W # like a boss # pack conditions.frames.inertial.total_force_vector[:,:] = F[:,:] return
def update_forces(segment): """ Summation of forces: lift, drag, thrust, weight. Future versions will include new definitions of dreams, FAA, money, and reality. Assumptions: You only have these 4 forces applied to the vehicle Inputs: segment.state.conditions: frames.wind.lift_force_vector [newtons] frames.wind.drag_force_vector [newtons] frames.inertial.gravity_force_vector [newtons] frames.body.thrust_force_vector [newtons] frames.body.transform_to_inertial [newtons] frames.wind.transform_to_inertial [newtons] Outputs: segment.state.conditions: frames.inertial.total_force_vector [newtons] Properties Used: N/A """ # unpack conditions = segment.state.conditions # unpack forces wind_lift_force_vector = conditions.frames.wind.lift_force_vector wind_drag_force_vector = conditions.frames.wind.drag_force_vector body_thrust_force_vector = conditions.frames.body.thrust_force_vector inertial_gravity_force_vector = conditions.frames.inertial.gravity_force_vector # unpack transformation matrices T_body2inertial = conditions.frames.body.transform_to_inertial T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial, wind_lift_force_vector) D = orientation_product(T_wind2inertial, wind_drag_force_vector) T = orientation_product(T_body2inertial, body_thrust_force_vector) W = inertial_gravity_force_vector # sum of the forces F = L + D + T + W # like a boss # pack conditions.frames.inertial.total_force_vector[:, :] = F[:, :] return
def compute_ground_forces(segment): """ Compute the rolling friction on the aircraft Assumptions: Does a force balance to calculate the load on the wheels using only lift. Uses only a single friction coefficient. Source: N/A Inputs: conditions: frames.inertial.gravity_force_vector [meters/second^2] ground.friction_coefficient [unitless] frames.wind.lift_force_vector [newtons] Outputs: conditions.frames.inertial.ground_force_vector [newtons] Properties Used: N/A """ # unpack conditions = segment.state.conditions W = conditions.frames.inertial.gravity_force_vector[:, 2, None] friction_coeff = conditions.ground.friction_coefficient wind_lift_force_vector = conditions.frames.wind.lift_force_vector #transformation matrix to get lift in inertial frame T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial, wind_lift_force_vector)[:, 2, None] #compute friction force N = -(W + L) Ff = N * friction_coeff #pack results. Friction acts along x-direction conditions.frames.inertial.ground_force_vector[:, 2] = N[:, 0] conditions.frames.inertial.ground_force_vector[:, 0] = Ff[:, 0]
def compute_ground_forces(segment): """ Compute the rolling friction on the aircraft Assumptions: Does a force balance to calculate the load on the wheels using only lift. Uses only a single friction coefficient. Source: N/A Inputs: conditions: frames.inertial.gravity_force_vector [meters/second^2] ground.friction_coefficient [unitless] frames.wind.lift_force_vector [newtons] Outputs: conditions.frames.inertial.ground_force_vector [newtons] Properties Used: N/A """ # unpack conditions = segment.state.conditions W = conditions.frames.inertial.gravity_force_vector[:,2,None] friction_coeff = conditions.ground.friction_coefficient wind_lift_force_vector = conditions.frames.wind.lift_force_vector #transformation matrix to get lift in inertial frame T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial,wind_lift_force_vector)[:,2,None] #compute friction force N = -(W + L) Ff = N * friction_coeff #pack results. Friction acts along x-direction conditions.frames.inertial.ground_force_vector[:,2] = N[:,0] conditions.frames.inertial.ground_force_vector[:,0] = Ff[:,0]
def compute_ground_forces(segment,state): """ Compute the rolling friction on the aircraft """ # unpack conditions = state.conditions W = conditions.frames.inertial.gravity_force_vector[:,2,None] friction_coeff = conditions.ground.friction_coefficient wind_lift_force_vector = conditions.frames.wind.lift_force_vector #transformation matrix to get lift in inertial frame T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial,wind_lift_force_vector)[:,2,None] #compute friction force N = -(W + L) Ff = N * friction_coeff #pack results. Friction acts along x-direction conditions.frames.inertial.ground_force_vector[:,2] = N[:,0] conditions.frames.inertial.ground_force_vector[:,0] = Ff[:,0]
def compute_ground_forces(segment, state): """ Compute the rolling friction on the aircraft """ # unpack conditions = state.conditions W = conditions.frames.inertial.gravity_force_vector[:, 2, None] friction_coeff = conditions.ground.friction_coefficient wind_lift_force_vector = conditions.frames.wind.lift_force_vector #transformation matrix to get lift in inertial frame T_wind2inertial = conditions.frames.wind.transform_to_inertial # to inertial frame L = orientation_product(T_wind2inertial, wind_lift_force_vector)[:, 2, None] #compute friction force N = -(W + L) Ff = N * friction_coeff #pack results. Friction acts along x-direction conditions.frames.inertial.ground_force_vector[:, 2] = N[:, 0] conditions.frames.inertial.ground_force_vector[:, 0] = Ff[:, 0]
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 Leishman, Gordon J. Principles of helicopter aerodynamics Cambridge university press, 2006. 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.outputs. number_radial_stations [-] number_azimuthal_stations [-] disc_radial_distribution [m] thrust_angle [rad] speed_of_sound [m/s] density [kg/m-3] velocity [m/s] disc_tangential_induced_velocity [m/s] disc_axial_induced_velocity [m/s] disc_tangential_velocity [m/s] disc_axial_velocity [m/s] drag_coefficient [-] lift_coefficient [-] omega [rad/s] disc_circulation [-] blade_dQ_dR [N/m] blade_dT_dr [N] blade_thrust_distribution [N] disc_thrust_distribution [N] thrust_per_blade [N] thrust_coefficient [-] azimuthal_distribution [rad] disc_azimuthal_distribution [rad] blade_dQ_dR [N] blade_dQ_dr [Nm] blade_torque_distribution [Nm] disc_torque_distribution [Nm] torque_per_blade [Nm] torque_coefficient [-] power [W] power_coefficient [-] Properties Used: self. number_of_blades [-] tip_radius [m] hub_radius [m] twist_distribution [radians] chord_distribution [m] mid_chord_alignment [m] thrust_angle [radians] """ #Unpack B = self.number_of_blades R = self.tip_radius Rh = self.hub_radius beta_0 = self.twist_distribution c = self.chord_distribution chi = self.radius_distribution omega = self.inputs.omega a_geo = self.airfoil_geometry a_loc = self.airfoil_polar_stations cl_sur = self.airfoil_cl_surrogates cd_sur = self.airfoil_cd_surrogates tc = self.thickness_to_chord ua = self.induced_hover_velocity VTOL = self.VTOL_flag rho = conditions.freestream.density[:,0,None] mu = conditions.freestream.dynamic_viscosity[:,0,None] Vv = conditions.frames.inertial.velocity_vector a = conditions.freestream.speed_of_sound[:,0,None] T = conditions.freestream.temperature[:,0,None] pitch_c = self.pitch_command theta = self.thrust_angle Na = self.number_azimuthal_stations BB = B*B BBB = BB*B nonuniform_freestream = self.nonuniform_freestream rotation = self.rotation # calculate total blade pitch total_blade_pitch = beta_0 + pitch_c # 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) if VTOL: V = V_thrust[:,0,None] + ua else: V = V_thrust[:,0,None] ut = np.zeros_like(V) #Things that don't change with iteration Nr = len(c) # Number of stations radially ctrl_pts = len(Vv) # set up non dimensional radial distribution chi = self.radius_distribution/R V = V_thrust[:,0,None] omega = np.abs(omega) r = chi*R # Radial coordinate omegar = np.outer(omega,r) pi = np.pi pi2 = pi*pi n = omega/(2.*pi) # Cycles per second nu = mu/rho deltar = (r[1]-r[0]) deltachi = (chi[1]-chi[0]) rho_0 = rho # azimuth distribution psi = np.linspace(0,2*pi,Na+1)[:-1] psi_2d = np.tile(np.atleast_2d(psi).T,(1,Nr)) psi_2d = np.repeat(psi_2d[np.newaxis, :, :], ctrl_pts, axis=0) # 2D radial distribution non dimensionalized chi_2d = np.tile(chi ,(Na,1)) chi_2d = np.repeat(chi_2d[ np.newaxis,:, :], ctrl_pts, axis=0) r_dim_2d = np.tile(r ,(Na,1)) r_dim_2d = np.repeat(r_dim_2d[ np.newaxis,:, :], ctrl_pts, axis=0) # Setup a Newton iteration diff = 1. ii = 0 tol = 1e-6 # Convergence tolerance use_2d_analysis = False if theta !=0: # thrust angle creates disturbances in radial and tangential velocities use_2d_analysis = True # component of freestream in the propeller plane Vz = V_thrust[:,2,None,None] Vz = np.repeat(Vz, Na,axis=1) Vz = np.repeat(Vz, Nr,axis=2) if rotation == None: print("Propeller rotation not specified. Set to 1 (clockwise when viewed from behind).") rotation = 1 # compute resulting radial and tangential velocities in propeller frame ut = ( Vz*np.cos(psi_2d) ) * rotation ur = (-Vz*np.sin(psi_2d) ) ua = np.zeros_like(ut) if nonuniform_freestream: use_2d_analysis = True # account for upstream influences ua = self.axial_velocities_2d ut = self.tangential_velocities_2d ur = self.radial_velocities_2d if use_2d_analysis: # make everything 2D with shape (ctrl_pts,Na,Nr) size = (ctrl_pts,Na,Nr) PSI = np.ones(size) PSIold = np.zeros(size) # 2-D freestream velocity and omega*r V_2d = V_thrust[:,0,None,None] V_2d = np.repeat(V_2d, Na,axis=1) V_2d = np.repeat(V_2d, Nr,axis=2) omegar = (np.repeat(np.outer(omega,r)[:,None,:], Na, axis=1)) # total velocities Ua = V_2d + ua # 2-D blade pitch and radial distributions beta = np.tile(total_blade_pitch,(Na ,1)) beta = np.repeat(beta[np.newaxis,:, :], ctrl_pts, axis=0) r = np.tile(r,(Na ,1)) r = np.repeat(r[np.newaxis,:, :], ctrl_pts, axis=0) # 2-D atmospheric properties a = np.tile(np.atleast_2d(a),(1,Nr)) a = np.repeat(a[:, np.newaxis, :], Na, axis=1) nu = np.tile(np.atleast_2d(nu),(1,Nr)) nu = np.repeat(nu[:, np.newaxis, :], Na, axis=1) rho = np.tile(np.atleast_2d(rho),(1,Nr)) rho = np.repeat(rho[:, np.newaxis, :], Na, axis=1) T = np.tile(np.atleast_2d(T),(1,Nr)) T = np.repeat(T[:, np.newaxis, :], Na, axis=1) else: # uniform freestream ua = np.zeros_like(V) ut = np.zeros_like(V) ur = np.zeros_like(V) # total velocities Ua = np.outer((V + ua),np.ones_like(r)) beta = total_blade_pitch # Things that will change with iteration size = (ctrl_pts,Nr) PSI = np.ones(size) PSIold = np.zeros(size) # total velocities Ut = omegar - ut U = np.sqrt(Ua*Ua + Ut*Ut + ur*ur) # Drela's Theory 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 f[f<0.] = 0. piece = np.exp(-f) arccos_piece = np.arccos(piece) F = 2.*arccos_piece/pi # Prandtl's tip factor Gamma = vt*(4.*pi*r/B)*F*(1.+(4.*lamdaw*R/(pi*B*r))*(4.*lamdaw*R/(pi*B*r)))**0.5 Re = (W*c)/nu # Compute aerodynamic forces based on specified input airfoil or using a surrogate Cl, Cdval = compute_aerodynamic_forces(a_loc, a_geo, cl_sur, cd_sur, ctrl_pts, Nr, Na, Re, Ma, alpha, tc, use_2d_analysis) 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 # omega = 0, do not run BEMT convergence loop if all(omega[:,0]) == 0. : break # If its really not going to converge if np.any(PSI>pi/2) and np.any(dpsi>0.0): print("Propeller BEMT did not converge to a solution (Stall)") break ii+=1 if ii>10000: print("Propeller BEMT did not converge to a solution (Iteration Limit)") break # 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. blade_T_distribution = rho*(Gamma*(Wt-epsilon*Wa))*deltar blade_Q_distribution = rho*(Gamma*(Wa+epsilon*Wt)*r)*deltar if use_2d_analysis: blade_T_distribution_2d = blade_T_distribution blade_Q_distribution_2d = blade_Q_distribution blade_Gamma_2d = Gamma # set 1d blade loadings to be the average: blade_T_distribution = np.mean((blade_T_distribution_2d), axis = 1) blade_Q_distribution = np.mean((blade_Q_distribution_2d), axis = 1) Va_2d = Wa Vt_2d = Wt Va_avg = np.average(Wa, axis=1) # averaged around the azimuth Vt_avg = np.average(Wt, axis=1) # averaged around the azimuth Va_ind_2d = va Vt_ind_2d = vt Vt_ind_avg = np.average(vt, axis=1) Va_ind_avg = np.average(va, axis=1) else: Va_2d = np.repeat(Wa.T[ : , np.newaxis , :], Na, axis=1).T Vt_2d = np.repeat(Wt.T[ : , np.newaxis , :], Na, axis=1).T blade_T_distribution_2d = np.repeat(blade_T_distribution.T[ np.newaxis,: , :], Na, axis=0).T blade_Q_distribution_2d = np.repeat(blade_Q_distribution.T[ np.newaxis,: , :], Na, axis=0).T blade_Gamma_2d = np.repeat(Gamma.T[ : , np.newaxis , :], Na, axis=1).T Vt_avg = Wt Va_avg = Wa Vt_ind_avg = vt Va_ind_avg = va Va_ind_2d = np.repeat(va.T[ : , np.newaxis , :], Na, axis=1).T Vt_ind_2d = np.repeat(vt.T[ : , np.newaxis , :], Na, axis=1).T # thrust and torque derivatives on the blade. blade_dT_dr = rho*(Gamma*(Wt-epsilon*Wa)) blade_dQ_dr = rho*(Gamma*(Wa+epsilon*Wt)*r) thrust = np.atleast_2d((B * np.sum(blade_T_distribution, axis = 1))).T torque = np.atleast_2d((B * np.sum(blade_Q_distribution, axis = 1))).T power = omega*torque # calculate coefficients D = 2*R Cq = torque/(rho_0*(n*n)*(D*D*D*D*D)) Ct = thrust/(rho_0*(n*n)*(D*D*D*D)) Cp = power/(rho_0*(n*n*n)*(D*D*D*D*D)) etap = V*thrust/power # prevent things from breaking Cq[Cq<0] = 0. Ct[Ct<0] = 0. Cp[Cp<0] = 0. 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] thrust[omega==0.0] = 0.0 power[omega==0.0] = 0.0 torque[omega==0.0] = 0.0 Ct[omega==0.0] = 0.0 Cp[omega==0.0] = 0.0 etap[omega==0.0] = 0.0 # assign efficiency to network conditions.propulsion.etap = etap # store data self.azimuthal_distribution = psi results_conditions = Data outputs = results_conditions( number_radial_stations = Nr, number_azimuthal_stations = Na, disc_radial_distribution = r_dim_2d, thrust_angle = theta, speed_of_sound = conditions.freestream.speed_of_sound, density = conditions.freestream.density, velocity = Vv, blade_tangential_induced_velocity = Vt_ind_avg, blade_axial_induced_velocity = Va_ind_avg, blade_tangential_velocity = Vt_avg, blade_axial_velocity = Va_avg, disc_tangential_induced_velocity = Vt_ind_2d, disc_axial_induced_velocity = Va_ind_2d, disc_tangential_velocity = Vt_2d, disc_axial_velocity = Va_2d, drag_coefficient = Cd, lift_coefficient = Cl, omega = omega, disc_circulation = blade_Gamma_2d, blade_dT_dr = blade_dT_dr, blade_thrust_distribution = blade_T_distribution, disc_thrust_distribution = blade_T_distribution_2d, thrust_per_blade = thrust/B, thrust_coefficient = Ct, disc_azimuthal_distribution = psi_2d, blade_dQ_dr = blade_dQ_dr, blade_torque_distribution = blade_Q_distribution, disc_torque_distribution = blade_Q_distribution_2d, torque_per_blade = torque/B, torque_coefficient = Cq, power = power, power_coefficient = Cp, converged_inflow_ratio = lamdaw, disc_local_angle_of_attack = alpha ) return thrust, torque, power, Cp, outputs , etap
def spin(self, conditions): """ Analyzes a propeller given geometry and operating conditions Inputs: hub radius tip radius rotation rate freestream velocity number of blades number of stations chord distribution twist distribution airfoil data Outputs: Power coefficient Thrust coefficient Assumptions: Based on Qprop Theory document """ #Unpack B = self.prop_attributes.number_blades R = self.prop_attributes.tip_radius Rh = self.prop_attributes.hub_radius beta = self.prop_attributes.twist_distribution c = self.prop_attributes.chord_distribution omega1 = self.inputs.omega rho = conditions.freestream.density[:, 0, None] mu = conditions.freestream.dynamic_viscosity[:, 0, None] Vv = conditions.frames.inertial.velocity_vector a = conditions.freestream.speed_of_sound[:, 0, None] T = conditions.freestream.temperature[:, 0, None] theta = self.thrust_angle tc = .12 # Thickness to chord 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) # Velocity transformed to the propulsor frame 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] nu = mu / rho tol = 1e-6 # Convergence tolerance omega = omega1 * 1.0 omega = np.abs(omega) ###### # Enter airfoil data in a better way, there is currently Re and Ma scaling from DAE51 data ###### #Things that don't change with iteration N = len(c) # Number of stations 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) sigma = np.multiply(B * c, 1. / (2. * pi * r)) #I make the assumption that externally-induced velocity at the disk is zero #This can be easily changed if needed in the future: ua = 0.0 ut = 0.0 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) #Setup a Newton iteration psi = np.ones(size) psiold = np.zeros(size) diff = 1. ii = 0 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 #if np.any(Ma> 1.0): #warn('Propeller blade tips are supersonic.', Warning) 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 # Ok, from the airfoil data, given Re, Ma, alpha we need to find Cl Cl = 2. * pi * alpha # By 90 deg, it's totally stalled. #Cl[Cl>Cl1maxp] = Cl1maxp[Cl>Cl1maxp] 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 * 85.0 / 180.)) and np.any(dpsi > 0.0): print 'broke' 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] power = torque * omega D = 2 * R Cp = power / (rho * (n * n * n) * (D * D * D * D * D)) thrust[conditions.propulsion.throttle[:, 0] <= 0.0] = 0.0 power[conditions.propulsion.throttle[:, 0] <= 0.0] = 0.0 thrust[omega1 < 0.0] = -thrust[omega1 < 0.0] etap = V * thrust / power conditions.propulsion.etap = etap # calculating Activity Factor (per blade) #AF = 100000./16. * np.trapz((c/D) * (r/R)**3, x=r/R) AF = (10. / D)**5. * np.trapz(c * r**3, x=r) # store data results_conditions = Results conditions.propulsion.acoustic_outputs = results_conditions( number_sections=N, r0=r, airfoil_chord=c, blades_number=B, propeller_diameter=D, drag_coefficient=Cd, lift_coefficient=Cl, rpm=omega, velocity=V, thrust=thrust, hp=power, blade_activity_factor=AF, ) return thrust, torque, power, Cp
def update_orientations(segment): """ Updates the orientation of the vehicle throughout the mission for each relevant axis Assumptions: This assumes the vehicle has 3 frames: inertial, body, and wind. There also contains bits for stability axis which are not used. Creates tensors and solves for alpha and beta. Inputs: segment.state.conditions: frames.inertial.velocity_vector [meters/second] frames.body.inertial_rotations [Radians] segment.analyses.planet.features.mean_radius [meters] state.numerics.time.integrate [float] Outputs: segment.state.conditions: aerodynamics.angle_of_attack [Radians] aerodynamics.side_slip_angle [Radians] aerodynamics.roll_angle [Radians] frames.body.transform_to_inertial [Radians] frames.wind.body_rotations [Radians] frames.wind.transform_to_inertial [Radians] Properties Used: N/A """ # unpack conditions = segment.state.conditions V_inertial = conditions.frames.inertial.velocity_vector body_inertial_rotations = conditions.frames.body.inertial_rotations # ------------------------------------------------------------------ # Body Frame # ------------------------------------------------------------------ # body frame rotations phi = body_inertial_rotations[:, 0, None] theta = body_inertial_rotations[:, 1, None] psi = body_inertial_rotations[:, 2, None] # body frame tranformation matrices T_inertial2body = angles_to_dcms(body_inertial_rotations, (2, 1, 0)) T_body2inertial = orientation_transpose(T_inertial2body) # transform inertial velocity to body frame V_body = orientation_product(T_inertial2body, V_inertial) # project inertial velocity into body x-z plane V_stability = V_body * 1. V_stability[:, 1] = 0 V_stability_magnitude = np.sqrt(np.sum(V_stability**2, axis=1))[:, None] # calculate angle of attack alpha = np.arctan2(V_stability[:, 2], V_stability[:, 0])[:, None] # calculate side slip beta = np.arctan2(V_body[:, 1], V_stability_magnitude[:, 0])[:, None] # pack aerodynamics angles conditions.aerodynamics.angle_of_attack[:, 0] = alpha[:, 0] conditions.aerodynamics.side_slip_angle[:, 0] = -beta[:, 0] conditions.aerodynamics.roll_angle[:, 0] = phi[:, 0] # pack transformation tensor conditions.frames.body.transform_to_inertial = T_body2inertial # ------------------------------------------------------------------ # Wind Frame # ------------------------------------------------------------------ # back calculate wind frame rotations wind_body_rotations = body_inertial_rotations * 0. wind_body_rotations[:, 0] = 0 # no roll in wind frame wind_body_rotations[:, 1] = alpha[:, 0] # theta is angle of attack wind_body_rotations[:, 2] = beta[:, 0] # psi is side slip angle # wind frame tranformation matricies T_wind2body = angles_to_dcms(wind_body_rotations, (2, 1, 0)) T_body2wind = orientation_transpose(T_wind2body) T_wind2inertial = orientation_product(T_wind2body, T_body2inertial) # pack wind rotations conditions.frames.wind.body_rotations = wind_body_rotations # pack transformation tensor conditions.frames.wind.transform_to_inertial = T_wind2inertial return
def spin(self, conditions): """Analyzes a rotor 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 Leishman, Gordon J. Principles of helicopter aerodynamics Cambridge university press, 2006. 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.outputs. number_radial_stations [-] number_azimuthal_stations [-] disc_radial_distribution [m] thrust_angle [rad] speed_of_sound [m/s] density [kg/m-3] velocity [m/s] disc_tangential_induced_velocity [m/s] disc_axial_induced_velocity [m/s] disc_tangential_velocity [m/s] disc_axial_velocity [m/s] drag_coefficient [-] lift_coefficient [-] omega [rad/s] disc_circulation [-] blade_dQ_dR [N/m] blade_dT_dr [N] blade_thrust_distribution [N] disc_thrust_distribution [N] thrust_per_blade [N] thrust_coefficient [-] azimuthal_distribution [rad] disc_azimuthal_distribution [rad] blade_dQ_dR [N] blade_dQ_dr [Nm] blade_torque_distribution [Nm] disc_torque_distribution [Nm] torque_per_blade [Nm] torque_coefficient [-] power [W] power_coefficient [-] Properties Used: self. number_of_blades [-] tip_radius [m] hub_radius [m] twist_distribution [radians] chord_distribution [m] mid_chord_alignment [m] thrust_angle [radians] """ #Unpack B = self.number_of_blades R = self.tip_radius Rh = self.hub_radius beta_0 = self.twist_distribution c = self.chord_distribution chi = self.radius_distribution omega = self.inputs.omega a_geo = self.airfoil_geometry a_loc = self.airfoil_polar_stations cl_sur = self.airfoil_cl_surrogates cd_sur = self.airfoil_cd_surrogates tc = self.thickness_to_chord ua = self.induced_hover_velocity VTOL = self.VTOL_flag rho = conditions.freestream.density[:, 0, None] mu = conditions.freestream.dynamic_viscosity[:, 0, None] Vv = conditions.frames.inertial.velocity_vector a = conditions.freestream.speed_of_sound[:, 0, None] T = conditions.freestream.temperature[:, 0, None] pitch_c = self.pitch_command theta = self.thrust_angle Na = self.number_azimuthal_stations BB = B * B BBB = BB * B # calculate total blade pitch total_blade_pitch = beta_0 + pitch_c # 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) if VTOL: V = V_thrust[:, 0, None] + ua else: V = V_thrust[:, 0, None] ut = np.zeros_like(V) #Things that don't change with iteration Nr = len(c) # Number of stations radially ctrl_pts = len(Vv) # set up non dimensional radial distribution chi = self.radius_distribution / R omega = np.abs(omega) r = chi * R # Radial coordinate pi = np.pi pi2 = pi * pi n = omega / (2. * pi) # Cycles per second nu = mu / rho # azimuth distribution psi = np.linspace(0, 2 * pi, Na + 1)[:-1] psi_2d = np.tile(np.atleast_2d(psi).T, (1, Nr)) psi_2d = np.repeat(psi_2d[np.newaxis, :, :], ctrl_pts, axis=0) azimuth_2d = np.repeat(np.atleast_2d(psi).T[np.newaxis, :, :], Na, axis=0).T # 2 dimensiona radial distribution non dimensionalized chi_2d = np.tile(chi, (Na, 1)) chi_2d = np.repeat(chi_2d[np.newaxis, :, :], ctrl_pts, axis=0) r_dim_2d = np.tile(r, (Na, 1)) r_dim_2d = np.repeat(r_dim_2d[np.newaxis, :, :], ctrl_pts, axis=0) # Things that will change with iteration size = (len(a), Nr) omegar = np.outer(omega, r) Ua = np.outer((V + ua), np.ones_like(r)) Ut = omegar - ut U = np.sqrt(Ua * Ua + Ut * Ut) beta = total_blade_pitch # Setup a Newton iteration PSI = np.ones(size) PSIold = np.zeros(size) diff = 1. ii = 0 broke = False tol = 1e-6 # Convergence tolerance 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 f[f < 0.] = 0. 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 Re = (W * c) / nu # If rotor airfoils are defined, using airfoil surrogate if a_loc != None: # Compute blade Cl and Cd distribution from the airfoil data Cl = np.zeros((ctrl_pts, Nr)) Cdval = np.zeros((ctrl_pts, Nr)) dim_sur = len(cl_sur) for jj in range(dim_sur): Cl_af = cl_sur[a_geo[jj]](Re, alpha, grid=False) Cdval_af = cd_sur[a_geo[jj]](Re, alpha, grid=False) locs = np.where(np.array(a_loc) == jj) Cl[:, locs] = Cl_af[:, locs] Cdval[:, locs] = Cdval_af[:, locs] else: # Estimate Cl max 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 # 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.] #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. 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 # omega = 0, do not run BEMT convergence loop if all(omega[:, 0]) == 0.: break # If its really not going to converge if np.any(PSI > pi / 2) and np.any(dpsi > 0.0): print("Rotor BEMT did not converge to a solution") break ii += 1 if ii > 10000: broke = True print( "Rotor BEMT did not converge to a solution (Iteration Limit)" ) break # 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]) deltachi = (chi[1] - chi[0]) blade_T_distribution = rho * (Gamma * (Wt - epsilon * Wa)) * deltar blade_Q_distribution = rho * (Gamma * (Wa + epsilon * Wt) * r) * deltar 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] power = omega * torque Va_2d = np.repeat(Wa.T[:, np.newaxis, :], Na, axis=1).T Vt_2d = np.repeat(Wt.T[:, np.newaxis, :], Na, axis=1).T Va_ind_2d = np.repeat(va.T[:, np.newaxis, :], Na, axis=1).T Vt_ind_2d = np.repeat(vt.T[:, np.newaxis, :], Na, axis=1).T blade_T_distribution_2d = np.repeat( blade_T_distribution.T[np.newaxis, :, :], Na, axis=0).T blade_Q_distribution_2d = np.repeat( blade_Q_distribution.T[np.newaxis, :, :], Na, axis=0).T blade_Gamma_2d = np.repeat(Gamma.T[:, np.newaxis, :], Na, axis=1).T # thrust and torque derivatives on the blade. blade_dT_dr = rho * (Gamma * (Wt - epsilon * Wa)) blade_dQ_dr = rho * (Gamma * (Wa + epsilon * Wt) * r) Vt_ind_avg = vt Va_ind_avg = va Vt_avg = Wt Va_avg = Wa # calculate coefficients D = 2 * R Cq = torque / (rho * (n * n) * (D * D * D * D * D)) Ct = thrust / (rho * (n * n) * (D * D * D * D)) Cp = power / (rho * (n * n * n) * (D * D * D * D * D)) etap = V * thrust / power # efficiency # prevent things from breaking Cq[Cq < 0] = 0. Ct[Ct < 0] = 0. Cp[Cp < 0] = 0. 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] thrust[omega == 0.0] = 0.0 power[omega == 0.0] = 0.0 torque[omega == 0.0] = 0.0 Ct[omega == 0.0] = 0.0 Cp[omega == 0.0] = 0.0 etap[omega == 0.0] = 0.0 # assign efficiency to network conditions.propulsion.etap = etap # store data self.azimuthal_distribution = psi results_conditions = Data outputs = results_conditions( number_radial_stations=Nr, number_azimuthal_stations=Na, disc_radial_distribution=r_dim_2d, thrust_angle=theta, speed_of_sound=conditions.freestream.speed_of_sound, density=conditions.freestream.density, velocity=Vv, blade_tangential_induced_velocity=Vt_ind_avg, blade_axial_induced_velocity=Va_ind_avg, blade_tangential_velocity=Vt_avg, blade_axial_velocity=Va_avg, disc_tangential_induced_velocity=Vt_ind_2d, disc_axial_induced_velocity=Va_ind_2d, disc_tangential_velocity=Vt_2d, disc_axial_velocity=Va_2d, drag_coefficient=Cd, lift_coefficient=Cl, omega=omega, disc_circulation=blade_Gamma_2d, blade_dT_dr=blade_dT_dr, blade_thrust_distribution=blade_T_distribution, disc_thrust_distribution=blade_T_distribution_2d, thrust_per_blade=thrust / B, thrust_coefficient=Ct, disc_azimuthal_distribution=azimuth_2d, blade_dQ_dr=blade_dQ_dr, blade_torque_distribution=blade_Q_distribution, disc_torque_distribution=blade_Q_distribution_2d, torque_per_blade=torque / B, torque_coefficient=Cq, power=power, power_coefficient=Cp, ) return thrust, torque, power, Cp, outputs, etap
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 update_orientations(segment, state): # unpack conditions = state.conditions V_inertial = conditions.frames.inertial.velocity_vector body_inertial_rotations = conditions.frames.body.inertial_rotations # ------------------------------------------------------------------ # Body Frame # ------------------------------------------------------------------ # body frame rotations phi = body_inertial_rotations[:, 0, None] theta = body_inertial_rotations[:, 1, None] psi = body_inertial_rotations[:, 2, None] # body frame tranformation matrices T_inertial2body = angles_to_dcms(body_inertial_rotations, (2, 1, 0)) T_body2inertial = orientation_transpose(T_inertial2body) # transform inertial velocity to body frame V_body = orientation_product(T_inertial2body, V_inertial) # project inertial velocity into body x-z plane V_stability = V_body V_stability[:, 1] = 0 V_stability_magnitude = np.sqrt(np.sum(V_stability**2, axis=1))[:, None] #V_stability_direction = V_stability / V_stability_magnitude # calculate angle of attack alpha = np.arctan2(V_stability[:, 2], V_stability[:, 0])[:, None] # calculate side slip beta = np.arctan2(V_body[:, 1], V_stability_magnitude[:, 0])[:, None] # pack aerodynamics angles conditions.aerodynamics.angle_of_attack[:, 0] = alpha[:, 0] conditions.aerodynamics.side_slip_angle[:, 0] = beta[:, 0] conditions.aerodynamics.roll_angle[:, 0] = phi[:, 0] # pack transformation tensor conditions.frames.body.transform_to_inertial = T_body2inertial # ------------------------------------------------------------------ # Wind Frame # ------------------------------------------------------------------ # back calculate wind frame rotations wind_body_rotations = body_inertial_rotations * 0. wind_body_rotations[:, 0] = 0 # no roll in wind frame wind_body_rotations[:, 1] = alpha[:, 0] # theta is angle of attack wind_body_rotations[:, 2] = beta[:, 0] # psi is side slip angle # wind frame tranformation matricies T_wind2body = angles_to_dcms(wind_body_rotations, (2, 1, 0)) T_body2wind = orientation_transpose(T_wind2body) T_wind2inertial = orientation_product(T_wind2body, T_body2inertial) # pack wind rotations conditions.frames.wind.body_rotations = wind_body_rotations # pack transformation tensor conditions.frames.wind.transform_to_inertial = T_wind2inertial return
def spin(self, conditions): """Analyzes a general rotor 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 Leishman, Gordon J. Principles of helicopter aerodynamics Cambridge university press, 2006. 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.outputs. number_radial_stations [-] number_azimuthal_stations [-] disc_radial_distribution [m] speed_of_sound [m/s] density [kg/m-3] velocity [m/s] disc_tangential_induced_velocity [m/s] disc_axial_induced_velocity [m/s] disc_tangential_velocity [m/s] disc_axial_velocity [m/s] drag_coefficient [-] lift_coefficient [-] omega [rad/s] disc_circulation [-] blade_dQ_dR [N/m] blade_dT_dr [N] blade_thrust_distribution [N] disc_thrust_distribution [N] thrust_per_blade [N] thrust_coefficient [-] azimuthal_distribution [rad] disc_azimuthal_distribution [rad] blade_dQ_dR [N] blade_dQ_dr [Nm] blade_torque_distribution [Nm] disc_torque_distribution [Nm] torque_per_blade [Nm] torque_coefficient [-] power [W] power_coefficient [-] Properties Used: self. number_of_blades [-] tip_radius [m] twist_distribution [radians] chord_distribution [m] orientation_euler_angles [rad, rad, rad] """ # Unpack rotor blade parameters B = self.number_of_blades R = self.tip_radius beta_0 = self.twist_distribution c = self.chord_distribution sweep = self.sweep_distribution # quarter chord distance from quarter chord of root airfoil r_1d = self.radius_distribution tc = self.thickness_to_chord # Unpack rotor airfoil data a_geo = self.airfoil_geometry a_loc = self.airfoil_polar_stations cl_sur = self.airfoil_cl_surrogates cd_sur = self.airfoil_cd_surrogates # Unpack rotor inputs and conditions omega = self.inputs.omega Na = self.number_azimuthal_stations nonuniform_freestream = self.nonuniform_freestream use_2d_analysis = self.use_2d_analysis wake_method = self.wake_method rotation = self.rotation pitch_c = self.inputs.pitch_command # Check for variable pitch if np.any(pitch_c != 0) and not self.variable_pitch: print( "Warning: pitch commanded for a fixed-pitch rotor. Changing to variable pitch rotor for weights analysis." ) self.variable_pitch = True # Unpack freestream conditions rho = conditions.freestream.density[:, 0, None] mu = conditions.freestream.dynamic_viscosity[:, 0, None] a = conditions.freestream.speed_of_sound[:, 0, None] T = conditions.freestream.temperature[:, 0, None] Vv = conditions.frames.inertial.velocity_vector nu = mu / rho rho_0 = rho # Helpful shorthands pi = np.pi # Calculate total blade pitch total_blade_pitch = beta_0 + pitch_c # Velocity in the rotor frame T_body2inertial = conditions.frames.body.transform_to_inertial T_inertial2body = orientation_transpose(T_body2inertial) V_body = orientation_product(T_inertial2body, Vv) body2thrust = self.body_to_prop_vel() T_body2thrust = orientation_transpose( np.ones_like(T_body2inertial[:]) * body2thrust) V_thrust = orientation_product(T_body2thrust, V_body) # Check and correct for hover V = V_thrust[:, 0, None] V[V == 0.0] = 1E-6 # Number of radial stations and segment control points Nr = len(c) ctrl_pts = len(Vv) # Non-dimensional radial distribution and differential radius chi = r_1d / R diff_r = np.diff(r_1d) deltar = np.zeros(len(r_1d)) deltar[1:-1] = diff_r[0:-1] / 2 + diff_r[1:] / 2 deltar[0] = diff_r[0] / 2 deltar[-1] = diff_r[-1] / 2 # Calculating rotational parameters omegar = np.outer(omega, r_1d) n = omega / (2. * pi) # Rotations per second # 2 dimensional radial distribution non dimensionalized chi_2d = np.tile(chi[:, None], (1, Na)) chi_2d = np.repeat(chi_2d[None, :, :], ctrl_pts, axis=0) r_dim_2d = np.tile(r_1d[:, None], (1, Na)) r_dim_2d = np.repeat(r_dim_2d[None, :, :], ctrl_pts, axis=0) c_2d = np.tile(c[:, None], (1, Na)) c_2d = np.repeat(c_2d[None, :, :], ctrl_pts, axis=0) # Azimuthal distribution of stations psi = np.linspace(0, 2 * pi, Na + 1)[:-1] psi_2d = np.tile(np.atleast_2d(psi), (Nr, 1)) psi_2d = np.repeat(psi_2d[None, :, :], ctrl_pts, axis=0) # apply blade sweep to azimuthal position if np.any(np.array([sweep]) != 0): use_2d_analysis = True sweep_2d = np.repeat(sweep[:, None], (1, Na)) sweep_offset_angles = np.tan(sweep_2d / r_dim_2d) psi_2d += sweep_offset_angles # Starting with uniform freestream ua = 0 ut = 0 ur = 0 # Include velocities introduced by rotor incidence angles if (np.any(abs(V_thrust[:, 1]) > 1e-3) or np.any(abs(V_thrust[:, 2]) > 1e-3)) and use_2d_analysis: # y-component of freestream in the propeller cartesian plane Vy = V_thrust[:, 1, None, None] Vy = np.repeat(Vy, Nr, axis=1) Vy = np.repeat(Vy, Na, axis=2) # z-component of freestream in the propeller cartesian plane Vz = V_thrust[:, 2, None, None] Vz = np.repeat(Vz, Nr, axis=1) Vz = np.repeat(Vz, Na, axis=2) # check for invalid rotation angle if (rotation == 1) or (rotation == -1): pass else: print("Invalid rotation direction. Setting to 1.") rotation = 1 # compute resulting radial and tangential velocities in polar frame utz = Vz * np.cos(psi_2d * rotation) urz = Vz * np.sin(psi_2d * rotation) uty = Vy * np.sin(psi_2d * rotation) ury = Vy * np.cos(psi_2d * rotation) ut += (utz + uty) ur += (urz + ury) ua += np.zeros_like(ut) # Include external velocities introduced by user if nonuniform_freestream: use_2d_analysis = True # include additional influences specified at rotor sections, shape=(ctrl_pts,Nr,Na) ua += self.axial_velocities_2d ut += self.tangential_velocities_2d ur += self.radial_velocities_2d if use_2d_analysis: # make everything 2D with shape (ctrl_pts,Nr,Na) size = (ctrl_pts, Nr, Na) PSI = np.ones(size) PSIold = np.zeros(size) # 2-D freestream velocity and omega*r V_2d = V_thrust[:, 0, None, None] V_2d = np.repeat(V_2d, Na, axis=2) V_2d = np.repeat(V_2d, Nr, axis=1) omegar = (np.repeat(np.outer(omega, r_1d)[:, :, None], Na, axis=2)) # total velocities Ua = V_2d + ua # 2-D blade pitch and radial distributions if np.size(pitch_c) > 1: # control variable is the blade pitch, repeat around azimuth beta = np.repeat(total_blade_pitch[:, :, None], Na, axis=2) else: beta = np.tile(total_blade_pitch[None, :, None], (ctrl_pts, 1, Na)) r = np.tile(r_1d[None, :, None], (ctrl_pts, 1, Na)) c = np.tile(c[None, :, None], (ctrl_pts, 1, Na)) deltar = np.tile(deltar[None, :, None], (ctrl_pts, 1, Na)) # 2-D atmospheric properties a = np.tile(np.atleast_2d(a), (1, Nr)) a = np.repeat(a[:, :, None], Na, axis=2) nu = np.tile(np.atleast_2d(nu), (1, Nr)) nu = np.repeat(nu[:, :, None], Na, axis=2) rho = np.tile(np.atleast_2d(rho), (1, Nr)) rho = np.repeat(rho[:, :, None], Na, axis=2) T = np.tile(np.atleast_2d(T), (1, Nr)) T = np.repeat(T[:, :, None], Na, axis=2) else: # total velocities r = r_1d Ua = np.outer((V + ua), np.ones_like(r)) beta = total_blade_pitch # Things that will change with iteration size = (ctrl_pts, Nr) PSI = np.ones(size) PSIold = np.zeros(size) # Total velocities Ut = omegar - ut U = np.sqrt(Ua * Ua + Ut * Ut + ur * ur) if wake_method == 'momentum': # Setup a Newton iteration diff = 1. tol = 1e-6 # Convergence tolerance ii = 0 # BEMT Iteration while (diff > tol): # compute velocities 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 # compute blade airfoil forces and properties Cl, Cdval, alpha, Ma, W = compute_airfoil_aerodynamics( beta, c, r, R, B, Wa, Wt, a, nu, a_loc, a_geo, cl_sur, cd_sur, ctrl_pts, Nr, Na, tc, use_2d_analysis) # compute inflow velocity and tip loss factor lamdaw, F, piece = compute_inflow_and_tip_loss(r, R, Wa, Wt, B) # compute Newton residual on circulation Gamma = vt * (4. * pi * r / B) * F * (1. + (4. * lamdaw * R / (pi * B * r)) * (4. * lamdaw * R / (pi * B * r)))**0.5 Rsquiggly = Gamma - 0.5 * W * c * Cl # use analytical derivative to get dR_dpsi dR_dpsi = compute_dR_dpsi(B, beta, r, R, Wt, Wa, U, Ut, Ua, cos_psi, sin_psi, piece) # update inflow angle dpsi = -Rsquiggly / dR_dpsi PSI = PSI + dpsi diff = np.max(abs(PSIold - PSI)) PSIold = PSI # If omega = 0, do not run BEMT convergence loop if all(omega[:, 0]) == 0.: break # If its really not going to converge if np.any(PSI > pi / 2) and np.any(dpsi > 0.0): print("Rotor BEMT did not converge to a solution (Stall)") break ii += 1 if ii > 10000: print( "Rotor BEMT did not converge to a solution (Iteration Limit)" ) break elif wake_method == "helical_fixed_wake": # converge on va for a semi-prescribed wake method ii, ii_max = 0, 50 va_diff, tol = 1, 1e-3 while va_diff > tol: # compute axial wake-induced velocity (a byproduct of the circulation distribution which is an input to the wake geometry) va, vt = compute_HFW_inflow_velocities(self) # compute new blade velocities Wa = va + Ua Wt = Ut - vt # Compute aerodynamic forces based on specified input airfoil or surrogate Cl, Cdval, alpha, Ma, W = compute_airfoil_aerodynamics( beta, c, r, R, B, Wa, Wt, a, nu, a_loc, a_geo, cl_sur, cd_sur, ctrl_pts, Nr, Na, tc, use_2d_analysis) lamdaw, F, _ = compute_inflow_and_tip_loss(r, R, Wa, Wt, B) va_diff = np.max( abs(va - self.outputs.disc_axial_induced_velocity)) # compute HFW circulation at the blade Gamma = 0.5 * W * c * Cl # update the axial disc velocity based on new va from HFW self.outputs.disc_axial_induced_velocity = self.outputs.disc_axial_induced_velocity + 0.5 * ( va - self.outputs.disc_axial_induced_velocity) ii += 1 if ii > ii_max and va_diff > tol: print( "Semi-prescribed helical wake did not converge on axial inflow used for wake shape." ) # tip loss correction for velocities, since tip loss correction is only applied to loads in prior BEMT iteration va = F * va vt = F * vt lamdaw = r * (va + Ua) / (R * (Ut - vt)) # 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. # thrust and torque and their derivatives on the blade. blade_T_distribution = rho * (Gamma * (Wt - epsilon * Wa)) * deltar blade_Q_distribution = rho * (Gamma * (Wa + epsilon * Wt) * r) * deltar blade_dT_dr = rho * (Gamma * (Wt - epsilon * Wa)) blade_dQ_dr = rho * (Gamma * (Wa + epsilon * Wt) * r) if use_2d_analysis: blade_T_distribution_2d = blade_T_distribution blade_Q_distribution_2d = blade_Q_distribution blade_dT_dr_2d = blade_dT_dr blade_dQ_dr_2d = blade_dQ_dr blade_Gamma_2d = Gamma alpha_2d = alpha Va_2d = Wa Vt_2d = Wt Va_avg = np.average(Wa, axis=2) # averaged around the azimuth Vt_avg = np.average(Wt, axis=2) # averaged around the azimuth Va_ind_2d = va Vt_ind_2d = vt Vt_ind_avg = np.average(vt, axis=2) Va_ind_avg = np.average(va, axis=2) # set 1d blade loadings to be the average: blade_T_distribution = np.mean((blade_T_distribution_2d), axis=2) blade_Q_distribution = np.mean((blade_Q_distribution_2d), axis=2) blade_dT_dr = np.mean((blade_dT_dr_2d), axis=2) blade_dQ_dr = np.mean((blade_dQ_dr_2d), axis=2) # compute the hub force / rotor drag distribution along the blade dL_2d = 0.5 * rho * c_2d * Cd * omegar**2 * deltar dD_2d = 0.5 * rho * c_2d * Cl * omegar**2 * deltar rotor_drag_distribution = np.mean(dL_2d * np.sin(psi_2d) + dD_2d * np.cos(psi_2d), axis=2) else: Va_2d = np.repeat(Wa[:, :, None], Na, axis=2) Vt_2d = np.repeat(Wt[:, :, None], Na, axis=2) blade_T_distribution_2d = np.repeat(blade_T_distribution[:, :, None], Na, axis=2) blade_Q_distribution_2d = np.repeat(blade_Q_distribution[:, :, None], Na, axis=2) blade_dT_dr_2d = np.repeat(blade_dT_dr[:, :, None], Na, axis=2) blade_dQ_dr_2d = np.repeat(blade_dQ_dr[:, :, None], Na, axis=2) blade_Gamma_2d = np.repeat(Gamma[:, :, None], Na, axis=2) alpha_2d = np.repeat(alpha[:, :, None], Na, axis=2) Vt_avg = Wt Va_avg = Wa Vt_ind_avg = vt Va_ind_avg = va Va_ind_2d = np.repeat(va[:, :, None], Na, axis=2) Vt_ind_2d = np.repeat(vt[:, :, None], Na, axis=2) # compute the hub force / rotor drag distribution along the blade dL = 0.5 * rho * c * Cd * omegar**2 * deltar dL_2d = np.repeat(dL[:, :, None], Na, axis=2) dD = 0.5 * rho * c * Cl * omegar**2 * deltar dD_2d = np.repeat(dD[:, :, None], Na, axis=2) rotor_drag_distribution = np.mean(dL_2d * np.sin(psi_2d) + dD_2d * np.cos(psi_2d), axis=2) # forces thrust = np.atleast_2d((B * np.sum(blade_T_distribution, axis=1))).T torque = np.atleast_2d((B * np.sum(blade_Q_distribution, axis=1))).T rotor_drag = np.atleast_2d( (B * np.sum(rotor_drag_distribution, axis=1))).T power = omega * torque # calculate coefficients D = 2 * R Cq = torque / (rho_0 * (n * n) * (D * D * D * D * D)) Ct = thrust / (rho_0 * (n * n) * (D * D * D * D)) Cp = power / (rho_0 * (n * n * n) * (D * D * D * D * D)) Crd = rotor_drag / (rho_0 * (n * n) * (D * D * D * D)) etap = V * thrust / power # prevent things from breaking Cq[Cq < 0] = 0. Ct[Ct < 0] = 0. Cp[Cp < 0] = 0. 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 rotor_drag[conditions.propulsion.throttle[:, 0] <= 0.0] = 0.0 thrust[omega < 0.0] = -thrust[omega < 0.0] thrust[omega == 0.0] = 0.0 power[omega == 0.0] = 0.0 torque[omega == 0.0] = 0.0 rotor_drag[omega == 0.0] = 0.0 Ct[omega == 0.0] = 0.0 Cp[omega == 0.0] = 0.0 etap[omega == 0.0] = 0.0 # Make the thrust a 3D vector thrust_prop_frame = np.zeros((ctrl_pts, 3)) thrust_prop_frame[:, 0] = thrust[:, 0] thrust_vector = orientation_product( orientation_transpose(T_body2thrust), thrust_prop_frame) # Assign efficiency to network conditions.propulsion.etap = etap # Store data self.azimuthal_distribution = psi results_conditions = Data outputs = results_conditions( number_radial_stations=Nr, number_azimuthal_stations=Na, disc_radial_distribution=r_dim_2d, speed_of_sound=conditions.freestream.speed_of_sound, density=conditions.freestream.density, velocity=Vv, blade_tangential_induced_velocity=Vt_ind_avg, blade_axial_induced_velocity=Va_ind_avg, blade_tangential_velocity=Vt_avg, blade_axial_velocity=Va_avg, disc_tangential_induced_velocity=Vt_ind_2d, disc_axial_induced_velocity=Va_ind_2d, disc_tangential_velocity=Vt_2d, disc_axial_velocity=Va_2d, drag_coefficient=Cd, lift_coefficient=Cl, omega=omega, disc_circulation=blade_Gamma_2d, blade_dT_dr=blade_dT_dr, disc_dT_dr=blade_dT_dr_2d, blade_thrust_distribution=blade_T_distribution, disc_thrust_distribution=blade_T_distribution_2d, disc_effective_angle_of_attack=alpha_2d, thrust_per_blade=thrust / B, thrust_coefficient=Ct, disc_azimuthal_distribution=psi_2d, blade_dQ_dr=blade_dQ_dr, disc_dQ_dr=blade_dQ_dr_2d, blade_torque_distribution=blade_Q_distribution, disc_torque_distribution=blade_Q_distribution_2d, torque_per_blade=torque / B, torque_coefficient=Cq, power=power, power_coefficient=Cp, converged_inflow_ratio=lamdaw, propeller_efficiency=etap, blade_H_distribution=rotor_drag_distribution, rotor_drag=rotor_drag, rotor_drag_coefficient=Crd, ) return thrust_vector, torque, power, Cp, outputs, etap
def update_orientations(segment,state): """ Updates the orientation of the vehicle throughout the mission for each relevant axis Assumptions: This assumes the vehicle has 3 frames: inertial, body, and wind. There also contains bits for stability axis which are not used. Creates tensors and solves for alpha and beta. Inputs: state.conditions: frames.inertial.velocity_vector [meters/second] frames.body.inertial_rotations [Radians] segment.analyses.planet.features.mean_radius [meters] state.numerics.time.integrate [float] Outputs: state.conditions: aerodynamics.angle_of_attack [Radians] aerodynamics.side_slip_angle [Radians] aerodynamics.roll_angle [Radians] frames.body.transform_to_inertial [Radians] frames.wind.body_rotations [Radians] frames.wind.transform_to_inertial [Radians] Properties Used: N/A """ # unpack conditions = state.conditions V_inertial = conditions.frames.inertial.velocity_vector body_inertial_rotations = conditions.frames.body.inertial_rotations # ------------------------------------------------------------------ # Body Frame # ------------------------------------------------------------------ # body frame rotations phi = body_inertial_rotations[:,0,None] theta = body_inertial_rotations[:,1,None] psi = body_inertial_rotations[:,2,None] # body frame tranformation matrices T_inertial2body = angles_to_dcms(body_inertial_rotations,(2,1,0)) T_body2inertial = orientation_transpose(T_inertial2body) # transform inertial velocity to body frame V_body = orientation_product(T_inertial2body,V_inertial) # project inertial velocity into body x-z plane V_stability = V_body V_stability[:,1] = 0 V_stability_magnitude = np.sqrt( np.sum(V_stability**2,axis=1) )[:,None] #V_stability_direction = V_stability / V_stability_magnitude # calculate angle of attack alpha = np.arctan2(V_stability[:,2],V_stability[:,0])[:,None] # calculate side slip beta = np.arctan2(V_body[:,1],V_stability_magnitude[:,0])[:,None] # pack aerodynamics angles conditions.aerodynamics.angle_of_attack[:,0] = alpha[:,0] conditions.aerodynamics.side_slip_angle[:,0] = beta[:,0] conditions.aerodynamics.roll_angle[:,0] = phi[:,0] # pack transformation tensor conditions.frames.body.transform_to_inertial = T_body2inertial # ------------------------------------------------------------------ # Wind Frame # ------------------------------------------------------------------ # back calculate wind frame rotations wind_body_rotations = body_inertial_rotations * 0. wind_body_rotations[:,0] = 0 # no roll in wind frame wind_body_rotations[:,1] = alpha[:,0] # theta is angle of attack wind_body_rotations[:,2] = beta[:,0] # psi is side slip angle # wind frame tranformation matricies T_wind2body = angles_to_dcms(wind_body_rotations,(2,1,0)) T_body2wind = orientation_transpose(T_wind2body) T_wind2inertial = orientation_product(T_wind2body,T_body2inertial) # pack wind rotations conditions.frames.wind.body_rotations = wind_body_rotations # pack transformation tensor conditions.frames.wind.transform_to_inertial = T_wind2inertial return
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 update_orientations(segment,state): # unpack conditions = state.conditions V_inertial = conditions.frames.inertial.velocity_vector body_inertial_rotations = conditions.frames.body.inertial_rotations # ------------------------------------------------------------------ # Body Frame # ------------------------------------------------------------------ # body frame rotations phi = body_inertial_rotations[:,0,None] theta = body_inertial_rotations[:,1,None] psi = body_inertial_rotations[:,2,None] # body frame tranformation matrices T_inertial2body = angles_to_dcms(body_inertial_rotations,(2,1,0)) T_body2inertial = orientation_transpose(T_inertial2body) # transform inertial velocity to body frame V_body = orientation_product(T_inertial2body,V_inertial) # project inertial velocity into body x-z plane V_stability = V_body V_stability[:,1] = 0 V_stability_magnitude = np.sqrt( np.sum(V_stability**2,axis=1) )[:,None] #V_stability_direction = V_stability / V_stability_magnitude # calculate angle of attack alpha = np.arctan2(V_stability[:,2],V_stability[:,0])[:,None] # calculate side slip beta = np.arctan2(V_body[:,1],V_stability_magnitude[:,0])[:,None] # pack aerodynamics angles conditions.aerodynamics.angle_of_attack[:,0] = alpha[:,0] conditions.aerodynamics.side_slip_angle[:,0] = beta[:,0] conditions.aerodynamics.roll_angle[:,0] = phi[:,0] # pack transformation tensor conditions.frames.body.transform_to_inertial = T_body2inertial # ------------------------------------------------------------------ # Wind Frame # ------------------------------------------------------------------ # back calculate wind frame rotations wind_body_rotations = body_inertial_rotations * 0. wind_body_rotations[:,0] = 0 # no roll in wind frame wind_body_rotations[:,1] = alpha[:,0] # theta is angle of attack wind_body_rotations[:,2] = beta[:,0] # psi is side slip angle # wind frame tranformation matricies T_wind2body = angles_to_dcms(wind_body_rotations,(2,1,0)) T_body2wind = orientation_transpose(T_wind2body) T_wind2inertial = orientation_product(T_wind2body,T_body2inertial) # pack wind rotations conditions.frames.wind.body_rotations = wind_body_rotations # pack transformation tensor conditions.frames.wind.transform_to_inertial = T_wind2inertial return
def compute_propeller_nonuniform_freestream(prop, upstream_wake, conditions): """ Computes the inflow velocities in the frame of the rotating propeller Inputs: prop. SUAVE propeller tip_radius - propeller radius [m] rotation - propeller rotation direction [-] thrust_angle - thrust angle of prop [rad] number_radial_stations - number of propeller radial stations [-] number_azimuthal_stations - number of propeller azimuthal stations [-] upstream_wake. u_velocities - Streamwise velocities from upstream wake v_velocities - Spanwise velocities from upstream wake w_velocities - Downwash velocities from upstream wake VD - Vortex distribution from upstream wake conditions. frames Outputs: Va Axial velocities at propeller [m/s] Vt Tangential velocities at propeller [m/s] Vr Radial velocities at propeller [m/s] """ # unpack propeller parameters Vv = conditions.frames.inertial.velocity_vector R = prop.tip_radius rotation = prop.rotation theta = prop.thrust_angle c = prop.chord_distribution Na = prop.number_azimuthal_stations Nr = len(c) ua_wing = upstream_wake.u_velocities uv_wing = upstream_wake.v_velocities uw_wing = upstream_wake.w_velocities VD = upstream_wake.VD # 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) # azimuth distribution psi = np.linspace(0, 2 * np.pi, Na + 1)[:-1] psi_2d = np.tile(np.atleast_2d(psi).T, (1, Nr)) # 2 dimensiona radial distribution non dimensionalized chi = prop.radius_distribution / R # Reframe the wing induced velocities: y_center = prop.origin[0][1] # New points to interpolate data: (corresponding to r,phi locations on propeller disc) points = np.array([[VD.YC[i], VD.ZC[i]] for i in range(len(VD.YC))]) ycoords = np.reshape(R * chi * np.cos(psi_2d), (Nr * Na, )) zcoords = prop.origin[0][2] + np.reshape(R * chi * np.sin(psi_2d), (Nr * Na, )) xi = np.array([[y_center + ycoords[i], zcoords[i]] for i in range(len(ycoords))]) ua_w = sp.interpolate.griddata(points, ua_wing, xi, method='linear') uv_w = sp.interpolate.griddata(points, uv_wing, xi, method='linear') uw_w = sp.interpolate.griddata(points, uw_wing, xi, method='linear') ua_wing = np.reshape(ua_w, (Na, Nr)) uw_wing = np.reshape(uw_w, (Na, Nr)) uv_wing = np.reshape(uv_w, (Na, Nr)) if rotation == [1]: Vt_2d = V_thrust[:, 0] * ( -np.array(uw_wing) * np.cos(psi_2d) + np.array(uv_wing) * np.sin(psi_2d) ) # velocity tangential to the disk plane, positive toward the trailing edge eqn 6.34 pg 165 Vr_2d = V_thrust[:, 0] * (-np.array(uw_wing) * np.sin(psi_2d) - np.array(uv_wing) * np.cos(psi_2d) ) # radial velocity , positive outward Va_2d = V_thrust[:, 0] * np.array( ua_wing ) # velocity perpendicular to the disk plane, positive downward eqn 6.36 pg 166 else: Vt_2d = V_thrust[:, 0] * ( np.array(uw_wing) * np.cos(psi_2d) - np.array(uv_wing) * np.sin(psi_2d) ) # velocity tangential to the disk plane, positive toward the trailing edge Vr_2d = V_thrust[:, 0] * (-np.array(uw_wing) * np.sin(psi_2d) - np.array(uv_wing) * np.cos(psi_2d) ) # radial velocity , positive outward Va_2d = V_thrust[:, 0] * np.array( ua_wing ) # velocity perpendicular to the disk plane, positive downward # Append velocities to propeller prop.tangential_velocities_2d = Vt_2d prop.radial_velocities_2d = Vr_2d prop.axial_velocities_2d = Va_2d return prop
def spin(self,conditions): """Analyzes a propeller given geometry and operating conditions. Assumptions: per source Source: Qprop theory document 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.prop_attributes. 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) self.thrust_angle [radians] """ #Unpack B = self.prop_attributes.number_blades R = self.prop_attributes.tip_radius Rh = self.prop_attributes.hub_radius beta = self.prop_attributes.twist_distribution c = self.prop_attributes.chord_distribution omega1 = self.inputs.omega rho = conditions.freestream.density[:,0,None] mu = conditions.freestream.dynamic_viscosity[:,0,None] Vv = conditions.frames.inertial.velocity_vector a = conditions.freestream.speed_of_sound[:,0,None] T = conditions.freestream.temperature[:,0,None] theta = self.thrust_angle 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) # Velocity transformed to the propulsor frame 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] nu = mu/rho tol = 1e-5 # Convergence tolerance omega = omega1*1.0 omega = np.abs(omega) ###### # Enter airfoil data in a better way, there is currently Re and Ma scaling from DAE51 data ###### #Things that don't change with iteration N = len(c) # Number of stations 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) sigma = np.multiply(B*c,1./(2.*pi*r)) #I make the assumption that externally-induced velocity at the disk is zero #This can be easily changed if needed in the future: ua = 0.0 ut = 0.0 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) #Setup a Newton iteration psi = np.ones(size) psiold = np.zeros(size) diff = 1. ii = 0 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 #if np.any(Ma> 1.0): #warn('Propeller blade tips are supersonic.', Warning) 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 # Ok, from the airfoil data, given Re, Ma, alpha we need to find Cl Cl = 2.*pi*alpha # By 90 deg, it's totally stalled. 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*85.0/180.)) and np.any(dpsi>0.0): break #This is an atrocious fit of DAE51 data at RE=50k for Cd #There is also RE scaling Re = (W*c)/nu 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] power = torque*omega D = 2*R Cp = power/(rho*(n*n*n)*(D*D*D*D*D)) thrust[conditions.propulsion.throttle[:,0] <=0.0] = 0.0 power[conditions.propulsion.throttle[:,0] <=0.0] = 0.0 thrust[omega1<0.0] = - thrust[omega1<0.0] etap = V*thrust/power conditions.propulsion.etap = etap # store data results_conditions = Data conditions.propulsion.acoustic_outputs = results_conditions( number_sections = N, r0 = r, airfoil_chord = c, blades_number = B, propeller_diameter = D, drag_coefficient = Cd, lift_coefficient = Cl, omega = omega, velocity = V, thrust = thrust, power = power, mid_chord_aligment = self.prop_attributes.mid_chord_aligment ) return thrust, torque, power, Cp