def solar_azimuth_angle(latitude, day_of_year, time): """ Azimuth angle of the sun [degrees] for a local observer. :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time after local solar noon [seconds] :return: Solar azimuth angle [degrees] (the compass direction from which the sunlight is coming). """ # Solar azimuth angle (including seasonality, latitude, and time of day) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/azimuth-angle declination = declination_angle(day_of_year) sdec = np.sind(declination) cdec = np.cosd(declination) slat = np.sind(latitude) clat = np.cosd(latitude) ctime = np.cosd(time / 86400 * 360) elevation = solar_elevation_angle(latitude, day_of_year, time) cele = np.cosd(elevation) cos_azimuth = (sdec * clat - cdec * slat * ctime) / cele cos_azimuth = np.clip(cos_azimuth, -1, 1) azimuth_raw = np.arccosd(cos_azimuth) is_solar_morning = np.mod(time, 86400) > 43200 solar_azimuth_angle = np.where( is_solar_morning, azimuth_raw, 360 - azimuth_raw ) return solar_azimuth_angle
def length_day(latitude, day_of_year): """ For what length of time is the sun above the horizon on a given day? :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :return: Seconds of sunlight in a given day """ dec = declination_angle(day_of_year) constant = -np.sind(dec) * np.sind(latitude) / (np.cosd(dec) * np.cosd(latitude)) constant = np.clip(constant, -1, 1) sun_time_nondim = 2 * np.arccos(constant) sun_time = sun_time_nondim / (2 * np.pi) * 86400 return sun_time
def default_CL_function(alpha, Re, mach=0, deflection=0): """ Lift coefficient. """ print_default_warning() Cl_inc = np.pi * np.sind(2 * alpha) beta = (1 - mach) ** 2 Cl = Cl_inc * beta return Cl
def solar_elevation_angle(latitude, day_of_year, time): """ Elevation angle of the sun [degrees] for a local observer. :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time after local solar noon [seconds] :return: Solar elevation angle [degrees] (angle between horizon and sun). Returns 0 if the sun is below the horizon. """ # Solar elevation angle (including seasonality, latitude, and time of day) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/elevation-angle declination = declination_angle(day_of_year) solar_elevation_angle = np.arcsind( np.sind(declination) * np.sind(latitude) + np.cosd(declination) * np.cosd(latitude) * np.cosd(time / 86400 * 360) ) # in degrees solar_elevation_angle = np.fmax(solar_elevation_angle, 0) return solar_elevation_angle
def incidence_angle_function( latitude: float, day_of_year: float, time: float, panel_azimuth_angle: float = 0, panel_tilt_angle: float = 0, scattering: bool = True, ): """ This website will be useful for accounting for direction of the vertical surface https://www.pveducation.org/pvcdrom/properties-of-sunlight/arbitrary-orientation-and-tilt :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time since (local) solar noon [seconds] :param panel_azimuth_angle: The azimuth angle of the panel normal, in degrees. (0 degrees if pointing North and 90 if East) :param panel_tilt_angle: The angle between the panel normal and vertical, in degrees. (0 if horizontal and 90 if vertical) :param scattering: Boolean: include scattering effects at very low angles? :returns illumination_factor: Fraction of solar insolation received, relative to what it would get if it were perfectly oriented to the sun. """ solar_elevation = solar_elevation_angle(latitude, day_of_year, time) solar_azimuth = solar_azimuth_angle(latitude, day_of_year, time) cosine_factor = ( np.cosd(solar_elevation) * np.sind(panel_tilt_angle) * np.cosd(panel_azimuth_angle - solar_azimuth) + np.sind(solar_elevation) * np.cosd(panel_tilt_angle) ) if scattering: illumination_factor = cosine_factor * scattering_factor(solar_elevation) else: illumination_factor = cosine_factor illumination_factor = np.fmax(illumination_factor, 0) illumination_factor = np.where( solar_elevation < 0, 0, illumination_factor ) return illumination_factor
def airfoil_coefficients_post_stall( airfoil: Airfoil, alpha: float, ): """ Estimates post-stall aerodynamics of an airfoil. Uses methods given in: Truong, V. K. "An analytical model for airfoil aerodynamic characteristics over the entire 360deg angle of attack range". J. Renewable Sustainable Energy. 2020. doi: 10.1063/1.5126055 Args: airfoil: op_point: Returns: """ sina = np.sind(alpha) cosa = np.cosd(alpha) ##### Normal force calulation # Cd90_fp = aerolib.Cd_flat_plate_normal() # TODO implement # Cd90_0 = Cd90_fp - 0.83 * airfoil.LE_radius() - 1.46 / 2 * airfoil.max_thickness() + 1.46 * airfoil.max_camber() # Cd270_0 = Cd90_fp - 0.83 * airfoil.LE_radius() - 1.46 / 2 * airfoil.max_thickness() - 1.46 * airfoil.max_camber() ### Values for NACA0012 Cd90_0 = 2.08 pn2_star = 8.36e-2 pn3_star = 4.06e-1 pt1_star = 9.00e-2 pt2_star = -1.78e-1 pt3_star = -2.98e-1 Cd90 = Cd90_0 + pn2_star * cosa + pn3_star * cosa**2 CN = Cd90 * sina ##### Tangential force calculation CT = (pt1_star + pt2_star * cosa + pt3_star * cosa**3) * sina**2 ##### Conversion to wind axes CL = CN * cosa + CT * sina CD = CN * sina - CT * cosa CM = np.zeros_like(CL) # TODO return CL, CD, CM
def default_CD_function(alpha, Re, mach=0, deflection=0): """ Drag coefficient. """ print_default_warning() Cf = Cf_flat_plate(Re_L=Re, method="hybrid-sharpe-convex") ### Form factor model from Raymer, "Aircraft Design". Section 12.5, Eq. 12.30 t_over_c = 0.12 FF = 1 + 2 * t_over_c * 100 * t_over_c ** 4 Cd_inc = 2 * Cf * FF * ( 1 + (np.sind(alpha) * 180 / np.pi / 5) ** 2 ) beta = (1 - mach) ** 2 Cd = Cd_inc * beta return Cd
def calculate_velocity( self, x_field, y_field, ) -> [np.ndarray, np.ndarray]: ### Analyze the freestream u_freestream = self.op_point.velocity * np.cosd(self.op_point.alpha) v_freestream = self.op_point.velocity * np.sind(self.op_point.alpha) u_field = u_freestream v_field = v_freestream for airfoil in self.airfoils: ### Add in the influence of the vortices and sources on the airfoil surface u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=airfoil.x(), y_panels=airfoil.y(), gamma=airfoil.gamma, sigma=airfoil.sigma, ) u_field = u_field + u_field_induced v_field = v_field + v_field_induced ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=[airfoil.x()[0], airfoil.x()[-1]], y_panels=[airfoil.y()[0], airfoil.y()[-1]], gamma=[0, 0], sigma=[airfoil.gamma[0], airfoil.gamma[-1]]) u_field = u_field + u_field_induced_TE v_field = v_field + v_field_induced_TE if self.ground_effect: ### Add in the influence of the vortices and sources on the airfoil surface u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=airfoil.x(), y_panels=-airfoil.y(), gamma=-airfoil.gamma, sigma=airfoil.sigma, ) u_field = u_field + u_field_induced v_field = v_field + v_field_induced ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=[airfoil.x()[0], airfoil.x()[-1]], y_panels=-1 * np.array( [airfoil.y()[0], airfoil.y()[-1]]), gamma=[0, 0], sigma=[airfoil.gamma[0], airfoil.gamma[-1]]) u_field = u_field + u_field_induced_TE v_field = v_field + v_field_induced_TE return u_field, v_field
def firefly_CLA_and_CDA_fuse_hybrid( # TODO remove fuse_fineness_ratio, fuse_boattail_angle, fuse_TE_diameter, fuse_length, fuse_diameter, alpha, V, mach, rho, mu, ): """ Estimated equiv. lift area and equiv. drag area of the Firefly fuselage, component buildup. :param fuse_fineness_ratio: Fineness ratio of the fuselage nose (length / diameter). 0.5 is hemispherical. :param fuse_boattail_angle: Boattail half-angle [deg] :param fuse_TE_diameter: Diameter of the fuselage's base at the "trailing edge" [m] :param fuse_length: Length of the fuselage [m] :param fuse_diameter: Diameter of the fuselage [m] :param alpha: Angle of attack [deg] :param V: Airspeed [m/s] :param mach: Mach number [unitless] :param rho: Air density [kg/m^3] :param mu: Dynamic viscosity of air [Pa*s] :return: A tuple of (CLA, CDA) [m^2] """ alpha_rad = alpha * np.pi / 180 sin_alpha = np.sind(alpha) cos_alpha = np.cosd(alpha) """ Lift of a truncated fuselage, following slender body theory. """ separation_location = 0.3 # 0 for separation at trailing edge, 1 for separation at start of boat-tail. Calibrate this. diameter_at_separation = ( 1 - separation_location ) * fuse_TE_diameter + separation_location * fuse_diameter fuse_TE_area = np.pi / 4 * diameter_at_separation**2 CLA_slender_body = 2 * fuse_TE_area * alpha_rad # Derived from FVA 6.6.5, Eq. 6.75 """ Crossflow force, following the method of Jorgensen, 1977: "Prediction of Static Aero. Chars. for Slender..." """ V_crossflow = V * sin_alpha Re_crossflow = rho * np.abs(V_crossflow) * fuse_diameter / mu mach_crossflow = mach * np.abs(sin_alpha) eta = 1 # TODO make this a function of overall fineness ratio Cdn = 0.2 # Taken from suggestion for supercritical cylinders in Hoerner's Fluid Dynamic Drag, pg. 3-11 S_planform = fuse_diameter * fuse_length CNA = eta * Cdn * S_planform * sin_alpha * np.abs(sin_alpha) CLA_crossflow = CNA * cos_alpha CDA_crossflow = CNA * sin_alpha CLA = CLA_slender_body + CLA_crossflow r""" Zero-lift_force drag_force Model derived from high-fidelity data at: C:\Projects\GitHub\firefly_aerodynamics\Design_Opt\studies\Circular Firefly Fuse CFD\Zero-Lift Drag """ c = np.array([ 112.57153128951402720758778741583, -7.1720570832587240417410612280946, -0.01765596807595304351679033061373, 0.0026135564778264172743071913629365, 550.8775012129947299399645999074, 3.3166868391027000129156476759817, 11774.081980549422951298765838146, 3073258.204571904614567756652832, 0.0299, ]) # coefficients fuse_Re = rho * np.abs(V) * fuse_length / mu CDA_zero_lift = ( (c[0] * np.exp(c[1] * fuse_fineness_ratio) + c[2] * fuse_boattail_angle + c[3] * fuse_boattail_angle**2 + c[4] * fuse_TE_diameter**2 + c[5]) / c[6] * (fuse_Re / c[7])**(-1 / 7) * (fuse_length * fuse_diameter) / c[8]) r""" Assumes a ring-shaped wake projection into the Trefftz plane with a sinusoidal circulation distribution. This results in a uniform grad(phi) within the ring in the Trefftz plane. Derivation at C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\Ring Wing Potential Flow """ fuse_oswalds_efficiency = 0.5 CDiA_slender_body = CLA**2 / ( diameter_at_separation**2 * np.pi * fuse_oswalds_efficiency) / 2 # or equivalently, use the next line # CDiA_slender_body = fuse_TE_area * alpha_rad ** 2 / 2 CDA = CDA_crossflow + CDiA_slender_body + CDA_zero_lift return CLA, CDA
def display_graph(n_clicks, alpha, height, streamline_density, operating_checklist, *kulfan_inputs): ### Figure out if a button was pressed global n_clicks_last if n_clicks is None: n_clicks = 0 analyze_button_pressed = n_clicks > n_clicks_last n_clicks_last = n_clicks ### Parse the checklist ground_effect = "ground_effect" in operating_checklist ### Start constructing the figure airfoil = asb.Airfoil(coordinates=asb.get_kulfan_coordinates( lower_weights=np.array(kulfan_inputs[n_kulfan_inputs_per_side:]), upper_weights=np.array(kulfan_inputs[:n_kulfan_inputs_per_side]), TE_thickness=0, enforce_continuous_LE_radius=False, n_points_per_side=200, )) ### Do coordinates output coordinates_output = "\n".join( ["```"] + ["AeroSandbox Airfoil"] + ["\t%f\t%f" % tuple(coordinate) for coordinate in airfoil.coordinates] + ["```"]) ### Continue doing the airfoil things airfoil = airfoil.rotate(angle=-np.radians(alpha)) airfoil = airfoil.translate(0, height + 0.5 * np.sind(alpha)) fig = go.Figure() fig.add_trace( go.Scatter( x=airfoil.x(), y=airfoil.y(), mode="lines", name="Airfoil", fill="toself", line=dict(color="blue"), )) ### Default text output text_output = 'Click "Analyze" to compute aerodynamics!' xrng = (-0.5, 1.5) yrng = (-0.6, 0.6) if not ground_effect else (0, 1.2) if analyze_button_pressed: analysis = asb.AirfoilInviscid( airfoil=airfoil.repanel(50), op_point=asb.OperatingPoint( velocity=1, alpha=0, ), ground_effect=ground_effect, ) x = np.linspace(*xrng, 100) y = np.linspace(*yrng, 100) X, Y = np.meshgrid(x, y) u, v = analysis.calculate_velocity(x_field=X.flatten(), y_field=Y.flatten()) U = u.reshape(X.shape) V = v.reshape(Y.shape) streamline_fig = ff.create_streamline( x, y, U, V, arrow_scale=1e-16, density=streamline_density, line=dict(color="#ff82a3"), name="Streamlines", ) fig = go.Figure(data=streamline_fig.data + fig.data) text_output = make_table( pd.DataFrame({ "Engineering Quantity": ["C_L"], "Value": [f"{analysis.Cl:.3f}"] })) fig.update_layout( xaxis_title="x/c", yaxis_title="y/c", showlegend=False, yaxis=dict(scaleanchor="x", scaleratio=1), margin={"t": 0}, title=None, ) fig.update_xaxes(range=xrng) fig.update_yaxes(range=yrng) return fig, text_output, [coordinates_output]
def fuselage_aerodynamics( self, fuselage: Fuselage, ): """ Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation. Assumes: * The fuselage is a body of revolution aligned with the x_b axis. * The angle between the nose and the freestream is less than 90 degrees. Moments are given with the reference at Fuselage [0, 0, 0]. Uses methods from Jorgensen, Leland Howard. "Prediction of Static Aerodynamic Characteristics for Slender Bodies Alone and with Lifting Surfaces to Very High Angles of Attack". NASA TR R-474. 1977. Args: fuselage: A Fuselage object that you wish to analyze. Returns: """ ##### Alias a few things for convenience op_point = self.op_point Re = op_point.reynolds(reference_length=fuselage.length()) fuse_options = self.get_options(fuselage) ####### Reference quantities (Set these 1 here, just so we can follow Jorgensen syntax.) # Outputs of this function should be invariant of these quantities, if normalization has been done correctly. S_ref = 1 # m^2 c_ref = 1 # m ####### Fuselage zero-lift drag estimation ### Forebody drag C_f_forebody = aerolib.Cf_flat_plate(Re_L=Re) ### Base Drag C_D_base = 0.029 / np.sqrt(C_f_forebody) * fuselage.area_base() / S_ref ### Skin friction drag C_D_skin = C_f_forebody * fuselage.area_wetted() / S_ref ### Wave drag if self.include_wave_drag: sears_haack_drag = transonic.sears_haack_drag_from_volume( volume=fuselage.volume(), length=fuselage.length()) C_D_wave = transonic.approximate_CD_wave( mach=op_point.mach(), mach_crit=critical_mach( fineness_ratio_nose=fuse_options["nose_fineness_ratio"]), CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] * sears_haack_drag, ) else: C_D_wave = 0 ### Total zero-lift drag C_D_zero_lift = C_D_skin + C_D_base + C_D_wave ####### Jorgensen model ### First, merge the alpha and beta into a single "generalized alpha", which represents the degrees between the fuselage axis and the freestream. x_w, y_w, z_w = op_point.convert_axes(1, 0, 0, from_axes="body", to_axes="wind") generalized_alpha = np.arccosd(x_w / (1 + 1e-14)) sin_generalized_alpha = np.sind(generalized_alpha) cos_generalized_alpha = x_w # ### Limit generalized alpha to -90 < alpha < 90, for now. # generalized_alpha = np.clip(generalized_alpha, -90, 90) # # TODO make the drag/moment functions not give negative results for alpha > 90. alpha_fractional_component = -z_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of alpha beta_fractional_component = y_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of beta ### Compute normal quantities ### Note the (N)ormal, (A)ligned coordinate system. (See Jorgensen for definitions.) # M_n = sin_generalized_alpha * op_point.mach() Re_n = sin_generalized_alpha * Re # V_n = sin_generalized_alpha * op_point.velocity q = op_point.dynamic_pressure() x_nose = fuselage.xsecs[0].xyz_c[0] x_m = 0 - x_nose x_c = fuselage.x_centroid_projected() - x_nose ##### Potential flow crossflow model C_N_p = ( # Normal force coefficient due to potential flow. (Jorgensen Eq. 2.12, part 1) fuselage.area_base() / S_ref * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) C_m_p = ((fuselage.volume() - fuselage.area_base() * (fuselage.length() - x_m)) / (S_ref * c_ref) * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) ##### Viscous crossflow model C_d_n = np.where( Re_n != 0, aerolib.Cd_cylinder( Re_D=Re_n ), # Replace with 1.20 from Jorgensen Table 1 if not working well 0) eta = jorgensen_eta(fuselage.fineness_ratio()) C_N_v = ( # Normal force coefficient due to viscous crossflow. (Jorgensen Eq. 2.12, part 2) eta * C_d_n * fuselage.area_projected() / S_ref * sin_generalized_alpha**2) C_m_v = (eta * C_d_n * fuselage.area_projected() / S_ref * (x_m - x_c) / c_ref * sin_generalized_alpha**2) ##### Total C_N model C_N = C_N_p + C_N_v C_m_generalized = C_m_p + C_m_v ##### Total C_A model C_A = C_D_zero_lift * cos_generalized_alpha * np.abs( cos_generalized_alpha) ##### Convert to lift, drag C_L_generalized = C_N * cos_generalized_alpha - C_A * sin_generalized_alpha C_D = C_N * sin_generalized_alpha + C_A * cos_generalized_alpha ### Set proper directions C_L = C_L_generalized * alpha_fractional_component C_Y = -C_L_generalized * beta_fractional_component C_l = 0 C_m = C_m_generalized * alpha_fractional_component C_n = -C_m_generalized * beta_fractional_component ### Un-normalize L = C_L * q * S_ref Y = C_Y * q * S_ref D = C_D * q * S_ref l_w = C_l * q * S_ref * c_ref m_w = C_m * q * S_ref * c_ref n_w = C_n * q * S_ref * c_ref ### Convert to axes coordinates for reporting F_w = (-D, Y, -L) F_b = op_point.convert_axes(*F_w, from_axes="wind", to_axes="body") F_g = op_point.convert_axes(*F_b, from_axes="body", to_axes="geometry") M_w = ( l_w, m_w, n_w, ) M_b = op_point.convert_axes(*M_w, from_axes="wind", to_axes="body") M_g = op_point.convert_axes(*M_b, from_axes="body", to_axes="geometry") return { "F_g": F_g, "F_b": F_b, "F_w": F_w, "M_g": M_g, "M_b": M_b, "M_w": M_w, "L": -F_w[2], "Y": F_w[1], "D": -F_w[0], "l_b": M_b[0], "m_b": M_b[1], "n_b": M_b[2] }
def forces_on_fuselage_section( xsec_a: FuselageXSec, xsec_b: FuselageXSec, ): ### Some metrics, like effective force location, are area-weighted. Here, we compute those weights. r_a = xsec_a.radius r_b = xsec_b.radius x_a = xsec_a.xyz_c[0] x_b = xsec_b.xyz_c[0] area_a = xsec_a.xsec_area() area_b = xsec_b.xsec_area() total_area = area_a + area_b a_weight = area_a / total_area b_weight = area_b / total_area delta_x = x_b - x_a mean_geometric_radius = (r_a + r_b) / 2 mean_aerodynamic_radius = r_a * a_weight + r_b * b_weight force_x_location = x_a * a_weight + x_b * b_weight ##### Inviscid Forces force_potential_flow = q * ( # From Munk, via Jorgensen np.sind(2 * generalized_alpha) * (area_b - area_a) ) # Matches Drela, Flight Vehicle Aerodynamics Eqn. 6.75 in the small-alpha limit. # Note that no delta_x should be here; dA/dx * dx = dA. # Direction of force is midway between the normal to the axis of revolution of the body and the # normal to the free-stream velocity, according to: # Ward, via Jorgensen force_normal_potential_flow = force_potential_flow * np.cosd(generalized_alpha / 2) force_axial_potential_flow = -force_potential_flow * np.sind(generalized_alpha / 2) # Reminder: axial force is defined positive-aft ##### Viscous Forces Re_n = sin_generalized_alpha * op_point.reynolds(reference_length=2 * mean_aerodynamic_radius) M_n = sin_generalized_alpha * op_point.mach() C_d_n = np.where( Re_n != 0, aerolib.Cd_cylinder( Re_D=Re_n, mach=M_n ), # Replace with 1.20 from Jorgensen Table 1 if this isn't working well 0, ) force_viscous_flow = delta_x * q * ( 2 * eta * C_d_n * sin_squared_generalized_alpha * mean_geometric_radius ) # Viscous crossflow acts exactly normal to vehicle axis, definitionally. (Axial forces accounted for on a total-body basis) force_normal_viscous_flow = force_viscous_flow force_axial_viscous_flow = 0 normal_force = force_normal_potential_flow + force_normal_viscous_flow axial_force = force_axial_potential_flow + force_axial_viscous_flow return normal_force, axial_force, force_x_location
def convert_axes( self, x_from: Union[float, np.ndarray], y_from: Union[float, np.ndarray], z_from: Union[float, np.ndarray], from_axes: str, to_axes: str, ) -> Tuple[float, float, float]: """ Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to, y_to, z_to], as given in the `to_axes` frame. Both `from_axes` and `to_axes` should be a string, one of: * "geometry" * "body" * "wind" * "stability" This whole function is vectorized, both over the vector and the OperatingPoint (e.g., a vector of `OperatingPoint.alpha` values) Wind axes rotations are taken from Eq. 6.7 in Sect. 6.2.2 of Drela's Flight Vehicle Aerodynamics textbook, with axes corrections to go from [D, Y, L] to true wind axes (and same for geometry to body axes). Args: x_from: x-component of the vector, in `from_axes` frame. y_from: y-component of the vector, in `from_axes` frame. z_from: z-component of the vector, in `from_axes` frame. from_axes: The axes to convert from. to_axes: The axes to convert to. Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple. """ if from_axes == "geometry": x_b = -x_from y_b = y_from z_b = -z_from elif from_axes == "body": x_b = x_from y_b = y_from z_b = z_from elif from_axes == "wind": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) sb = np.sind(self.beta) cb = np.cosd(self.beta) x_b = (cb * ca) * x_from + (-sb * ca) * y_from + (-sa) * z_from y_b = (sb) * x_from + ( cb) * y_from # Note: z term is 0; not forgotten. z_b = (cb * sa) * x_from + (-sb * sa) * y_from + (ca) * z_from elif from_axes == "stability": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) x_b = ca * x_from - sa * z_from y_b = y_from z_b = sa * x_from + ca * z_from else: raise ValueError("Bad value of `from_axes`!") if to_axes == "geometry": x_to = -x_b y_to = y_b z_to = -z_b elif to_axes == "body": x_to = x_b y_to = y_b z_to = z_b elif to_axes == "wind": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) sb = np.sind(self.beta) cb = np.cosd(self.beta) x_to = (cb * ca) * x_b + (sb) * y_b + (cb * sa) * z_b y_to = (-sb * ca) * x_b + (cb) * y_b + (-sb * sa) * z_b z_to = (-sa) * x_b + ( ca) * z_b # Note: y term is 0; not forgotten. elif to_axes == "stability": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) x_to = ca * x_b + sa * z_b y_to = y_b z_to = -sa * x_b + ca * z_b else: raise ValueError("Bad value of `to_axes`!") return x_to, y_to, z_to
naca2412 = asb.Airfoil("naca2412") naca0012 = asb.Airfoil("naca0012") naca2412.generate_polars(cache_filename="assets/naca2412.json") naca0012.generate_polars(cache_filename="assets/naca0012.json") airplane = asb.Airplane( name="Cessna 152", wings=[ asb.Wing(name="Wing", xsecs=[ asb.WingXSec(xyz_le=[0, 0, 0], chord=ft_to_m(5, 4), airfoil=naca2412), asb.WingXSec( xyz_le=[0, ft_to_m(7), ft_to_m(7) * np.sind(1)], chord=ft_to_m(5, 4), airfoil=naca2412), asb.WingXSec(xyz_le=[ ft_to_m(4, 3 / 4) - ft_to_m(3, 8 + 1 / 2), ft_to_m(33, 4) / 2, ft_to_m(33, 4) / 2 * np.sind(1) ], chord=ft_to_m(3, 8 + 1 / 2), airfoil=naca0012) ], symmetric=True), asb.Wing(name="Horizontal Stabilizer", xsecs=[ asb.WingXSec(xyz_le=[0, 0, 0], chord=ft_to_m(3, 8),