Source code for aerosandbox.geometry.airplane

from aerosandbox import AeroSandboxObject
from aerosandbox.geometry.common import *
from typing import List, Dict, Any, Union, Optional, Tuple
import aerosandbox.geometry.mesh_utilities as mesh_utils
from aerosandbox.geometry.wing import Wing
from aerosandbox.geometry.fuselage import Fuselage
from aerosandbox.geometry.propulsor import Propulsor
from aerosandbox.weights.mass_properties import MassProperties
import copy
from pathlib import Path

[docs]class Airplane(AeroSandboxObject): """ Definition for an airplane. Anatomy of an Airplane: An Airplane consists chiefly of a collection of wings and fuselages. These can be accessed with `Airplane.wings` and `Airplane.fuselages`, which gives a list of those respective components. Each wing is a Wing object, and each fuselage is a Fuselage object. """ def __init__(self, name: Optional[str] = None, xyz_ref: Union[np.ndarray, List] = None, wings: Optional[List[Wing]] = None, fuselages: Optional[List[Fuselage]] = None, propulsors: Optional[List[Propulsor]] = None, s_ref: Optional[float] = None, c_ref: Optional[float] = None, b_ref: Optional[float] = None, analysis_specific_options: Optional[Dict[type, Dict[str, Any]]] = None ): """ Defines a new airplane. Args: name: Name of the airplane [optional]. It can help when debugging to give the airplane a sensible name. xyz_ref: An array-like that gives the x-, y-, and z- reference point of the airplane, used when computing moments and stability derivatives. Generally, this should be the center of gravity. # In a future version, this will be deprecated and replaced with asb.MassProperties. wings: A list of Wing objects that are a part of the airplane. fuselages: A list of Fuselage objects that are a part of the airplane. propulsors: A list of Propulsor objects that are a part of the airplane. s_ref: Reference area. If undefined, it's set from the area of the first Wing object. # Note: will be deprecated c_ref: Reference chord. If undefined, it's set from the mean aerodynamic chord of the first Wing object. # Note: will be deprecated b_ref: Reference span. If undefined, it's set from the span of the first Wing object. # Note: will be deprecated analysis_specific_options: Analysis-specific options are additional constants or modeling assumptions that should be passed on to specific analyses and associated with this specific geometry object. This should be a dictionary where: * Keys are specific analysis types (typically a subclass of asb.ExplicitAnalysis or asb.ImplicitAnalysis), but if you decide to write your own analysis and want to make this key something else (like a string), that's totally fine - it's just a unique identifier for the specific analysis you're running. * Values are a dictionary of key:value pairs, where: * Keys are strings. * Values are some value you want to assign. This is more easily demonstrated / understood with an example: >>> analysis_specific_options = { >>> asb.AeroBuildup: dict( >>> include_wave_drag=True, >>> ) >>> } """ ### Set defaults if name is None: name = "Untitled" if xyz_ref is None: xyz_ref = np.array([0., 0., 0.]) if wings is None: wings: List[Wing] = [] if fuselages is None: fuselages: List[Fuselage] = [] if propulsors is None: propulsors: List[Propulsor] = [] if analysis_specific_options is None: analysis_specific_options = {} ### Initialize self.name = name self.xyz_ref = np.array(xyz_ref) self.wings = wings self.fuselages = fuselages self.propulsors = propulsors self.analysis_specific_options = analysis_specific_options ### Assign reference values try: main_wing = self.wings[0] except IndexError: main_wing = None try: main_fuse = self.fuselages[0] except IndexError: main_fuse = None if s_ref is not None: self.s_ref = s_ref else: if main_wing is not None: self.s_ref = main_wing.area() else: if main_fuse is not None: self.s_ref = main_fuse.area_projected() else: raise ValueError( "`s_ref` was not provided, and a value cannot be inferred automatically from wings or fuselages.\n" "You must set this manually when instantiating your asb.Airplane object.") if c_ref is not None: self.c_ref = c_ref else: if main_wing is not None: self.c_ref = main_wing.mean_aerodynamic_chord() else: if main_fuse is not None: self.c_ref = main_fuse.length() else: raise ValueError( "`c_ref` was not provided, and a value cannot be inferred automatically from wings or fuselages.\n" "You must set this manually when instantiating your asb.Airplane object." ) if b_ref is not None: self.b_ref = b_ref else: if main_wing is not None: self.b_ref = main_wing.span(include_centerline_distance=True) else: if main_fuse is not None: self.b_ref = main_fuse.area_projected() / main_fuse.length() else: raise ValueError( "`b_ref` was not provided, and a value cannot be inferred automatically from wings or fuselages.\n" "You must set this manually when instantiating your asb.Airplane object." )
[docs] def __repr__(self): n_wings = len(self.wings) n_fuselages = len(self.fuselages) return f"Airplane '{self.name}' " \ f"({n_wings} {'wing' if n_wings == 1 else 'wings'}, " \ f"{n_fuselages} {'fuselage' if n_fuselages == 1 else 'fuselages'})"
# TODO def add_wing(wing: 'Wing') -> None
[docs] def mesh_body(self, method="quad", thin_wings=False, stack_meshes=True, ): """ Returns a surface mesh of the Airplane, in (points, faces) format. For reference on this format, see the documentation in `aerosandbox.geometry.mesh_utilities`. Args: method: thin_wings: Controls whether wings should be meshed as thin surfaces, rather than full 3D bodies. stack_meshes: Controls whether the meshes should be merged into a single mesh or not. * If True, returns a (points, faces) tuple in standard mesh format. * If False, returns a list of (points, faces) tuples in standard mesh format. Returns: """ if thin_wings: wing_meshes = [ wing.mesh_thin_surface( method=method, ) for wing in self.wings ] else: wing_meshes = [ wing.mesh_body( method=method, ) for wing in self.wings ] fuse_meshes = [ fuse.mesh_body( method=method ) for fuse in self.fuselages ] meshes = wing_meshes + fuse_meshes if stack_meshes: points, faces = mesh_utils.stack_meshes(*meshes) return points, faces else: return meshes
[docs] def draw(self, backend: str = "pyvista", thin_wings: bool = False, ax=None, use_preset_view_angle: str = None, set_background_pane_color: Union[str, Tuple[float, float, float]] = None, set_background_pane_alpha: float = None, set_lims: bool = True, set_equal: bool = True, set_axis_visibility: bool = None, show: bool = True, show_kwargs: Dict = None, ): """ Produces an interactive 3D visualization of the airplane. Args: backend: The visualization backend to use. Options are: * "matplotlib" for a Matplotlib backend * "pyvista" for a PyVista backend * "plotly" for a Plot.ly backend * "trimesh" for a trimesh backend thin_wings: A boolean that determines whether to draw the full airplane (i.e. thickened, 3D bodies), or to use a thin-surface representation for any Wing objects. show: A boolean that determines whether to display the object after plotting it. If False, the object is returned but not displayed. If True, the object is displayed and returned. Returns: The plotted object, in its associated backend format. Also displays the object if `show` is True. """ if show_kwargs is None: show_kwargs = {} points, faces = self.mesh_body(method="quad", thin_wings=thin_wings) if backend == "matplotlib": import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p from mpl_toolkits.mplot3d.art3d import Poly3DCollection if ax is None: _, ax = p.figure3d(figsize=(8, 8), computed_zorder=False) else: if not p.ax_is_3d(ax): raise ValueError("`ax` must be a 3D axis.") plt.sca(ax) ### Set the view angle if use_preset_view_angle is not None: p.set_preset_3d_view_angle(use_preset_view_angle) ### Set the background pane color if set_background_pane_color is not None: ax.xaxis.pane.set_facecolor(set_background_pane_color) ax.yaxis.pane.set_facecolor(set_background_pane_color) ax.zaxis.pane.set_facecolor(set_background_pane_color) ### Set the background pane alpha if set_background_pane_alpha is not None: ax.xaxis.pane.set_alpha(set_background_pane_alpha) ax.yaxis.pane.set_alpha(set_background_pane_alpha) ax.zaxis.pane.set_alpha(set_background_pane_alpha) ax.add_collection( Poly3DCollection( points[faces], facecolors='lightgray', edgecolors=(0, 0, 0, 0.1), linewidths=0.5, alpha=0.8, shade=True, ), ) for prop in self.propulsors: ### Disk if prop.length == 0: ax.add_collection( Poly3DCollection( np.stack([np.stack( prop.get_disk_3D_coordinates(), axis=1 )], axis=0), facecolors='darkgray', edgecolors=(0, 0, 0, 0.2), linewidths=0.5, alpha=0.35, shade=True, zorder=4, ) ) if set_lims: ax.set_xlim(points[:, 0].min(), points[:, 0].max()) ax.set_ylim(points[:, 1].min(), points[:, 1].max()) ax.set_zlim(points[:, 2].min(), points[:, 2].max()) if set_equal: p.equal() if set_axis_visibility is not None: if set_axis_visibility: ax.set_axis_on() else: ax.set_axis_off() if show: p.show_plot() elif backend == "plotly": from aerosandbox.visualization.plotly_Figure3D import Figure3D fig = Figure3D() for f in faces: fig.add_quad(( points[f[0]], points[f[1]], points[f[2]], points[f[3]], ), outline=True) show_kwargs = { "show": show, **show_kwargs } return fig.draw(**show_kwargs) elif backend == "pyvista": import pyvista as pv fig = pv.PolyData( *mesh_utils.convert_mesh_to_polydata_format(points, faces) ) show_kwargs = { "show_edges": True, "show_grid" : True, **show_kwargs, } if show: fig.plot(**show_kwargs) return fig elif backend == "trimesh": import trimesh as tri fig = tri.Trimesh(points, faces) if show: fig.show(**show_kwargs) return fig else: raise ValueError("Bad value of `backend`!")
[docs] def draw_wireframe(self, ax=None, color="k", thin_linewidth=0.2, thick_linewidth=0.5, fuselage_longeron_theta=None, use_preset_view_angle: str = None, set_background_pane_color: Union[str, Tuple[float, float, float]] = None, set_background_pane_alpha: float = None, set_lims: bool = True, set_equal: bool = True, set_axis_visibility: bool = None, show: bool = True, ): """ Draws a wireframe of the airplane on a Matplotlib 3D axis. Args: ax: The axis to draw on. Must be a 3D axis. If None, creates a new axis. color: The color of the wireframe. thin_linewidth: The linewidth of the thin lines. """ ### Set defaults if fuselage_longeron_theta is None: fuselage_longeron_theta = np.linspace(0, 2 * np.pi, 8 + 1)[:-1] import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p if ax is None: _, ax = p.figure3d(figsize=(8, 8), computed_zorder=False) else: if not p.ax_is_3d(ax): raise ValueError("`ax` must be a 3D axis.") plt.sca(ax) ### Set the view angle if use_preset_view_angle is not None: p.set_preset_3d_view_angle(use_preset_view_angle) ### Set the background pane color if set_background_pane_color is not None: ax.xaxis.pane.set_facecolor(set_background_pane_color) ax.yaxis.pane.set_facecolor(set_background_pane_color) ax.zaxis.pane.set_facecolor(set_background_pane_color) ### Set the background pane alpha if set_background_pane_alpha is not None: ax.xaxis.pane.set_alpha(set_background_pane_alpha) ax.yaxis.pane.set_alpha(set_background_pane_alpha) ax.zaxis.pane.set_alpha(set_background_pane_alpha) def plot_line( xyz: np.ndarray, symmetric: bool = False, color=color, linewidth=0.4, **kwargs ): if symmetric: xyz = np.concatenate([ xyz, np.array([[np.nan] * 3]), xyz * np.array([[1, -1, 1]]) ], axis=0) ax.plot( xyz[:, 0], xyz[:, 1], xyz[:, 2], color=color, linewidth=linewidth, **kwargs ) def reshape(x): return np.reshape(np.array(x), (1, 3)) ##### Wings for wing in self.wings: try: if wing.color is not None: color_to_use = wing.color else: color_to_use = color except AttributeError: color_to_use = color ### LE and TE lines for xy in [ (0, 0), # Leading Edge (1, 0), # Trailing Edge ]: plot_line( np.stack(wing.mesh_line(x_nondim=xy[0], z_nondim=xy[1]), axis=0), symmetric=wing.symmetric, linewidth=thick_linewidth, color=color_to_use ) ### Top and Bottom lines x = 0.4 afs = [xsec.airfoil for xsec in wing.xsecs] thicknesses = np.array([af.local_thickness(x_over_c=x) for af in afs]) plot_line( np.stack(wing.mesh_line(x_nondim=x, z_nondim=thicknesses / 2, add_camber=True), axis=0), symmetric=wing.symmetric, linewidth=thin_linewidth, color=color_to_use ) plot_line( np.stack(wing.mesh_line(x_nondim=x, z_nondim=-thicknesses / 2, add_camber=True), axis=0), symmetric=wing.symmetric, linewidth=thin_linewidth, color=color_to_use ) ### Airfoils for i, xsec in enumerate(wing.xsecs): xg_local, yg_local, zg_local = wing._compute_frame_of_WingXSec(i) xg_local = reshape(xg_local) yg_local = reshape(yg_local) zg_local = reshape(zg_local) origin = reshape(xsec.xyz_le) scale = xsec.chord line_upper = origin + ( xsec.airfoil.upper_coordinates()[:, 0].reshape((-1, 1)) * scale * xg_local + xsec.airfoil.upper_coordinates()[:, 1].reshape((-1, 1)) * scale * zg_local ) line_lower = origin + ( xsec.airfoil.lower_coordinates()[:, 0].reshape((-1, 1)) * scale * xg_local + xsec.airfoil.lower_coordinates()[:, 1].reshape((-1, 1)) * scale * zg_local ) for line in [line_upper, line_lower]: plot_line( line, symmetric=wing.symmetric, linewidth=thick_linewidth if i == 0 or i == len(wing.xsecs) - 1 else thin_linewidth, color=color_to_use ) ##### Fuselages for fuse in self.fuselages: try: if fuse.color is not None: color_to_use = fuse.color else: color_to_use = color except AttributeError: color_to_use = color ### Bulkheads perimeters_xyz = [ xsec.get_3D_coordinates(theta=np.linspace(0, 2 * np.pi, 121)) for xsec in fuse.xsecs ] for i, perim in enumerate(perimeters_xyz): plot_line( np.stack(perim, axis=1), linewidth=thick_linewidth if i == 0 or i == len(fuse.xsecs) - 1 else thin_linewidth, color=color_to_use ) ### Centerline plot_line( np.stack( fuse.mesh_line(y_nondim=0, z_nondim=0), axis=0, ), linewidth=thin_linewidth, color=color_to_use ) ### Longerons for theta in fuselage_longeron_theta: plot_line( np.stack([ np.array(xsec.get_3D_coordinates(theta=theta)) for xsec in fuse.xsecs ], axis=0), linewidth=thick_linewidth, color=color_to_use ) ##### Propulsors for prop in self.propulsors: try: if prop.color is not None: color_to_use = prop.color else: color_to_use = color except AttributeError: color_to_use = color ### Disk if prop.length == 0: plot_line( np.stack( prop.get_disk_3D_coordinates(), axis=1 ), color=color_to_use ) if set_lims: points, _ = self.mesh_body() ax.set_xlim(points[:, 0].min(), points[:, 0].max()) ax.set_ylim(points[:, 1].min(), points[:, 1].max()) ax.set_zlim(points[:, 2].min(), points[:, 2].max()) if set_equal: p.equal() if set_axis_visibility is not None: if set_axis_visibility: ax.set_axis_on() else: ax.set_axis_off() if show: p.show_plot()
[docs] def draw_three_view(self, style: str = "shaded", show: bool = True, ): """ Draws a standard 4-panel three-view diagram of the airplane using Matplotlib backend. Creates a new figure. Args: style: Determines what drawing style to use for the three-view. A string, one of: * "shaded" * "wireframe" show: A boolean of whether to show the figure after creating it, or to hold it so that the user can modify the figure further before showing. Returns: """ import matplotlib.pyplot as plt import aerosandbox.tools.pretty_plots as p preset_view_angles = np.array([ ["XZ", "-YZ"], ["XY", "left_isometric"] ], dtype="O") fig, axs = p.figure3d( nrows=preset_view_angles.shape[0], ncols=preset_view_angles.shape[1], figsize=(8, 8), computed_zorder=False, ) for i in range(axs.shape[0]): for j in range(axs.shape[1]): ax = axs[i, j] preset_view = preset_view_angles[i, j] if style == "shaded": self.draw( backend="matplotlib", ax=ax, set_axis_visibility=False if 'isometric' in preset_view else None, show=False ) elif style == "wireframe": if preset_view == "XZ": fuselage_longeron_theta = [np.pi / 2, 3 * np.pi / 2] elif preset_view == "XY": fuselage_longeron_theta = [0, np.pi] else: fuselage_longeron_theta = None self.draw_wireframe( ax=ax, set_axis_visibility=False if 'isometric' in preset_view else None, fuselage_longeron_theta=fuselage_longeron_theta, show=False ) p.set_preset_3d_view_angle(preset_view) xres = np.diff(ax.get_xticks())[0] yres = np.diff(ax.get_yticks())[0] zres = np.diff(ax.get_zticks())[0] p.set_ticks( xres, xres / 4, yres, yres / 4, zres, zres / 4, ) ax.xaxis.set_tick_params(color='white', which='minor') ax.yaxis.set_tick_params(color='white', which='minor') ax.zaxis.set_tick_params(color='white', which='minor') if preset_view == 'XY' or preset_view == '-XY': ax.set_zticks([]) if preset_view == 'XZ' or preset_view == '-XZ': ax.set_yticks([]) if preset_view == 'YZ' or preset_view == '-YZ': ax.set_xticks([]) axs[1, 0].set_xlabel("$x_g$ [m]") axs[1, 0].set_ylabel("$y_g$ [m]") axs[0, 0].set_zlabel("$z_g$ [m]") axs[0, 0].set_xticklabels([]) axs[0, 1].set_yticklabels([]) axs[0, 1].set_zticklabels([]) plt.subplots_adjust( left=-0.08, right=1.08, bottom=-0.08, top=1.08, wspace=-0.38, hspace=-0.38, ) if show: p.show_plot( tight_layout=False, )
[docs] def is_entirely_symmetric(self): """ Returns a boolean describing whether the airplane is geometrically entirely symmetric across the XZ-plane. :return: [boolean] """ for wing in self.wings: if not wing.is_entirely_symmetric(): return False # TODO add in logic for fuselages return True
[docs] def aerodynamic_center(self, chord_fraction: float = 0.25): """ Computes the approximate location of the aerodynamic center of the wing. Uses the generalized methodology described here: https://core.ac.uk/download/pdf/79175663.pdf Args: chord_fraction: The position of the aerodynamic center along the MAC, as a fraction of MAC length. Typically, this value (denoted `h_0` in the literature) is 0.25 for a subsonic wing. However, wing-fuselage interactions can cause a forward shift to a value more like 0.1 or less. Citing Cook, Michael V., "Flight Dynamics Principles", 3rd Ed., Sect. 3.5.3 "Controls-fixed static stability". PDF: https://www.sciencedirect.com/science/article/pii/B9780080982427000031 Returns: The (x, y, z) coordinates of the aerodynamic center of the airplane. """ wing_areas = [wing.area(type="projected") for wing in self.wings] ACs = [wing.aerodynamic_center() for wing in self.wings] wing_AC_area_products = [ AC * area for AC, area in zip( ACs, wing_areas ) ] aerodynamic_center = sum(wing_AC_area_products) / sum(wing_areas) return aerodynamic_center
[docs] def with_control_deflections(self, control_surface_deflection_mappings: Dict[str, float] ) -> "Airplane": """ Returns a copy of the airplane with the specified control surface deflections applied. Args: control_surface_deflection_mappings: A dictionary mapping control surface names to deflections. * Keys: Control surface names. * Values: Deflections, in degrees. Downwards-positive, following typical convention. Returns: A copy of the airplane with the specified control surface deflections applied. """ deflected_airplane = copy.deepcopy(self) for name, deflection in control_surface_deflection_mappings.items(): for wi, wing in enumerate(deflected_airplane.wings): for xi, xsec in enumerate(wing.xsecs): for csi, surf in enumerate(xsec.control_surfaces): if surf.name == name: surf.deflection = deflection return deflected_airplane
[docs] def generate_cadquery_geometry(self, minimum_airfoil_TE_thickness: float = 0.001, fuselage_tol: float = 1e-4, ) -> "Workplane": """ Uses the CADQuery library (OpenCASCADE backend) to generate a 3D CAD model of the airplane. Args: minimum_airfoil_TE_thickness: The minimum thickness of the trailing edge of the airfoils, as a fraction of each airfoil's chord. This will be enforced by thickening the trailing edge of the airfoils if necessary. This is useful for avoiding numerical issues in CAD software that can arise from extremely thin (i.e., <1e-6 meters) trailing edges. tol: The geometric tolerance (meters) to use when generating the CAD geometry. This is passed directly to the CADQuery Returns: A CADQuery Workplane object containing the CAD geometry of the airplane. """ try: import cadquery as cq except ModuleNotFoundError: raise ModuleNotFoundError( "The `cadquery` library is required to use this function. Please install it with `pip install cadquery`.") solids = [] for wing in self.wings: xsec_wires = [] for i, xsec in enumerate(wing.xsecs): csys = wing._compute_frame_of_WingXSec(i) af = xsec.airfoil if af.TE_thickness() < minimum_airfoil_TE_thickness: af = af.set_TE_thickness( thickness=minimum_airfoil_TE_thickness ) LE_index = af.LE_index() xsec_wires.append( cq.Workplane( inPlane=cq.Plane( origin=tuple(xsec.xyz_le), xDir=tuple(csys[0]), normal=tuple(-csys[1]) ) ).spline( listOfXYTuple=[ tuple(xy * xsec.chord) for xy in af.coordinates[:LE_index, :] ] ).spline( listOfXYTuple=[ tuple(xy * xsec.chord) for xy in af.coordinates[LE_index:, :] ] ).close() ) wire_collection = xsec_wires[0] for s in xsec_wires[1:]: wire_collection.ctx.pendingWires.extend(s.ctx.pendingWires) loft = wire_collection.loft(ruled=True, clean=False) solids.append(loft) if wing.symmetric: loft = loft.mirror( mirrorPlane='XZ', union=False ) solids.append(loft) for fuse in self.fuselages: xsec_wires = [] for i, xsec in enumerate(fuse.xsecs): if xsec.height < fuselage_tol or xsec.width < fuselage_tol: # If the xsec is so small as to effectively be a point xsec = copy.deepcopy(xsec) # Modify the xsec to be big enough to not error out. xsec.height = np.maximum(xsec.height, fuselage_tol) xsec.width = np.maximum(xsec.width, fuselage_tol) xsec_wires.append( cq.Workplane( inPlane=cq.Plane( origin=tuple(xsec.xyz_c), xDir=(0, 1, 0), normal=(-1, 0, 0) ) ).spline( listOfXYTuple=[ (y - xsec.xyz_c[1], z - xsec.xyz_c[2]) for x, y, z in zip(*xsec.get_3D_coordinates( theta=np.linspace( np.pi / 2, np.pi / 2 + 2 * np.pi, 181 ) )) ] ).close() ) wire_collection = xsec_wires[0] for s in xsec_wires[1:]: wire_collection.ctx.pendingWires.extend(s.ctx.pendingWires) loft = wire_collection.loft(ruled=True, clean=False) solids.append(loft) solid = solids[0] for s in solids[1:]: solid.add(s) return solid.clean()
[docs] def export_cadquery_geometry(self, filename: Union[Path, str], minimum_airfoil_TE_thickness: float = 0.001 ) -> None: """ Exports the airplane geometry to a STEP file. Args: filename: The filename to export to. Should include the ".step" extension. minimum_airfoil_TE_thickness: The minimum thickness of the trailing edge of the airfoils, as a fraction of each airfoil's chord. This will be enforced by thickening the trailing edge of the airfoils if necessary. This is useful for avoiding numerical issues in CAD software that can arise from extremely thin (i.e., <1e-6 meters) trailing edges. Returns: None, but exports the airplane geometry to a STEP file. """ solid = self.generate_cadquery_geometry( minimum_airfoil_TE_thickness=minimum_airfoil_TE_thickness, ) solid.objects = [ o.scale(1000) for o in solid.objects ] from cadquery import exporters exporters.export( solid, fname=filename )
[docs] def export_AVL(self, filename, include_fuselages: bool = True ): # TODO include option for mass file export as well # Use MassProperties.export_AVL_mass... from aerosandbox.aerodynamics.aero_3D.avl import AVL avl = AVL( airplane=self, op_point=None, xyz_ref=self.xyz_ref ) avl.write_avl(filepath=filename)
[docs] def export_XFLR(self, *args, **kwargs) -> str: import warnings warnings.warn( "`Airplane.export_XFLR()` has been renamed to `Airplane.export_XFLR5_xml()`, to clarify\n" "that it exports to XFLR5's XML format, not to a XFL file.\n" "\n" "Please update your code to use `Airplane.export_XFLR5_xml()` instead.\n" "\n" "This function will be removed in a future version of AeroSandbox.", PendingDeprecationWarning ) return self.export_XFLR5_xml(*args, **kwargs)
[docs] def export_XFLR5_xml(self, filename: Union[Path, str], mass_props: MassProperties = None, include_fuselages: bool = False, mainwing: Wing = None, elevator: Wing = None, fin: Wing = None, ) -> str: """ Exports the airplane geometry to an XFLR5 `.xml` file. To import the `.xml` file into XFLR5, go to File -> Import -> Import from XML. Args: filename: The filename to export to. Should include the ".xml" extension. mass_props: The MassProperties object to use when exporting the airplane. If not specified, will default to a 1 kg point mass at the origin. - Note: XFLR5 does not natively support user-defined inertia tensors, so we have to synthesize an equivalent set of point masses to represent the inertia tensor. include_fuselages: Whether to include fuselages in the export. mainwing: The main wing of the airplane. If not specified, will default to the first wing in the airplane. elevator: The elevator of the airplane. If not specified, will default to the second wing in the airplane. fin: The fin of the airplane. If not specified, will default to the third wing in the airplane. Returns: None, but exports the airplane geometry to an XFLR5 `.xml` file. To import the `.xml` file into XFLR5, go to File -> Import -> Import from XML. """ ### Handle default arguments if mass_props is None: mass_props = MassProperties( mass=1, x_cg=0, y_cg=0, z_cg=0, ) ### Identify which wings are the main wing, elevator, and fin. wings_specified = [ mainwing is not None, elevator is not None, fin is not None, ] if all(wings_specified): pass elif any(wings_specified): raise ValueError( "If any wings are specified (`mainwing`, `elevator`, `fin`), then all wings must be specified.") else: n_wings = len(self.wings) if n_wings == 0: pass else: import warnings warnings.warn( "No wings were specified (`mainwing`, `elevator`, `fin`). Automatically assigning the first wing " "to `mainwing`, the second wing to `elevator`, and the third wing to `fin`. If this is not " "correct, manually specify these with (`mainwing`, `elevator`, and `fin`) arguments." ) if n_wings == 1: mainwing = self.wings[0] elif n_wings == 2: mainwing = self.wings[0] elevator = self.wings[1] elif n_wings == 3: mainwing = self.wings[0] elevator = self.wings[1] fin = self.wings[2] else: raise ValueError( "Could not automatically parse which wings should be assigned to which XFLR5 lifting surfaces, " "since there are too many. Manually assign these with (`mainwing`, `elevator`, and `fin`) " "arguments." ) ### Determine where point masses should be in order to yield the specified mass properties. point_masses = mass_props.generate_possible_set_of_point_masses() ### Handle the fuselage if include_fuselages: raise NotImplementedError( "Fuselage export to XFLR5 is not yet implemented." ) ### Write the XML file. import xml.etree.ElementTree as ET base_xml = f"""\ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE explane> <explane version="1.0"> <Units> <length_unit_to_meter>1</length_unit_to_meter> <mass_unit_to_kg>1</mass_unit_to_kg> </Units> <Plane> <Name>{self.name}</Name> <Description></Description> <Inertia> </Inertia> <has_body>false</has_body> </Plane> </explane> """ root = ET.fromstring(base_xml) plane = root.find("Plane") ### Add point masses inertia = plane.find("Inertia") for i, point_mass in enumerate(point_masses): point_mass_xml = ET.SubElement(inertia, "Point_Mass") for k, v in { "Tag" : f"pm{i}", "Mass" : point_mass.mass, "coordinates": ",".join([str(x) for x in point_mass.xyz_cg]), }.items(): subelement = ET.SubElement(point_mass_xml, k) subelement.text = str(v) ### Add the wings if mainwing is not None: wing = mainwing wingxml = ET.SubElement(plane, "wing") xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { "Name" : wing.name, "Type" : "MAINWING", "Position" : ",".join([str(x) for x in xyz_le_root]), "Tilt_angle": 0., "Symetric" : wing.symmetric, # This tag is a typo in XFLR... "isFin" : "false", "isSymFin" : "false", }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root for i in range(len(wing.xsecs)) ] for i, xsec in enumerate(wing.xsecs): sect = ET.SubElement(sections, "Section") if i == len(wing.xsecs) - 1: dihedral = 0 else: dihedral = np.arctan2d( xyz_le_sects_rel[i + 1][2] - xyz_le_sects_rel[i][2], xyz_le_sects_rel[i + 1][1] - xyz_le_sects_rel[i][1], ) for k, v in { "y_position" : xyz_le_sects_rel[i][1], "Chord" : xsec.chord, "xOffset" : xyz_le_sects_rel[i][0], "Dihedral" : dihedral, "Twist" : xsec.twist, "Left_Side_FoilName" : xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, "x_number_of_panels" : 8, "y_number_of_panels" : 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) if elevator is not None: wing = elevator wingxml = ET.SubElement(plane, "wing") xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { "Name" : wing.name, "Type" : "ELEVATOR", "Position" : ",".join([str(x) for x in xyz_le_root]), "Tilt_angle": 0., "Symetric" : wing.symmetric, # This tag is a typo in XFLR... "isFin" : "false", "isSymFin" : "false", }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root for i in range(len(wing.xsecs)) ] for i, xsec in enumerate(wing.xsecs): sect = ET.SubElement(sections, "Section") if i == len(wing.xsecs) - 1: dihedral = 0 else: dihedral = np.arctan2d( xyz_le_sects_rel[i + 1][2] - xyz_le_sects_rel[i][2], xyz_le_sects_rel[i + 1][1] - xyz_le_sects_rel[i][1], ) for k, v in { "y_position" : xyz_le_sects_rel[i][1], "Chord" : xsec.chord, "xOffset" : xyz_le_sects_rel[i][0], "Dihedral" : dihedral, "Twist" : xsec.twist, "Left_Side_FoilName" : xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, "x_number_of_panels" : 8, "y_number_of_panels" : 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) if fin is not None: wing = fin wingxml = ET.SubElement(plane, "wing") xyz_le_root = wing._compute_xyz_of_WingXSec(index=0, x_nondim=0, z_nondim=0) for k, v in { "Name" : wing.name, "Type" : "FIN", "Position" : ",".join([str(x) for x in xyz_le_root]), "Tilt_angle": 0., "Symetric" : "true", # This tag is a typo in XFLR... "isFin" : "true", "isSymFin" : wing.symmetric, }.items(): subelement = ET.SubElement(wingxml, k) subelement.text = str(v) sections = ET.SubElement(wingxml, "Sections") xyz_le_sects_rel = [ wing._compute_xyz_of_WingXSec(index=i, x_nondim=0, z_nondim=0) - xyz_le_root for i in range(len(wing.xsecs)) ] for i, xsec in enumerate(wing.xsecs): sect = ET.SubElement(sections, "Section") if i == len(wing.xsecs) - 1: dihedral = 0 else: dihedral = np.arctan2d( xyz_le_sects_rel[i + 1][1] - xyz_le_sects_rel[i][1], xyz_le_sects_rel[i + 1][2] - xyz_le_sects_rel[i][2], ) for k, v in { "y_position" : xyz_le_sects_rel[i][2], "Chord" : xsec.chord, "xOffset" : xyz_le_sects_rel[i][0], "Dihedral" : dihedral, "Twist" : xsec.twist, "Left_Side_FoilName" : xsec.airfoil.name, "Right_Side_FoilName": xsec.airfoil.name, "x_number_of_panels" : 8, "y_number_of_panels" : 8, }.items(): subelement = ET.SubElement(sect, k) subelement.text = str(v) ### Indents the XML file properly def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i indent(root) xml_string = ET.tostring( root, encoding="UTF-8", xml_declaration=True ).decode() with open(filename, "w+") as f: f.write(xml_string) return xml_string
[docs] def export_OpenVSP_vspscript(self, filename: Union[Path, str], ) -> str: """ Exports the airplane geometry to a `*.vspscript` file compatible with OpenVSP. To import the `.vspscript` file into OpenVSP: Open OpenVSP, then File -> Run Script -> Select the `.vspscript` file. Args: filename: The filename to export to, given as a string or Path. Should include the ".vspscript" extension. Returns: A string of the file contents, and also saves the file to the specified filename """ from aerosandbox.geometry.openvsp_io.asb_to_openvsp.airplane_vspscript_generator import generate_airplane vspscript_code = generate_airplane(self) with open(filename, "w+") as f: f.write(vspscript_code) return vspscript_code
if __name__ == '__main__': import aerosandbox as asb # import aerosandbox.numpy as np import aerosandbox.tools.units as u
[docs] def ft(feet, inches=0): # Converts feet (and inches) to meters return feet * u.foot + inches * u.inch
naca2412 = asb.Airfoil("naca2412") naca0012 = asb.Airfoil("naca0012") airplane = Airplane( name="Cessna 152", wings=[ asb.Wing( name="Wing", xsecs=[ asb.WingXSec( xyz_le=[0, 0, 0], chord=ft(5, 4), airfoil=naca2412 ), asb.WingXSec( xyz_le=[0, ft(7), ft(7) * np.sind(1)], chord=ft(5, 4), airfoil=naca2412, control_surfaces=[ asb.ControlSurface( name="aileron", symmetric=False, hinge_point=0.8, deflection=0 ) ] ), asb.WingXSec( xyz_le=[ ft(4, 3 / 4) - ft(3, 8 + 1 / 2), ft(33, 4) / 2, ft(33, 4) / 2 * np.sind(1) ], chord=ft(3, 8 + 1 / 2), airfoil=naca0012 ) ], symmetric=True ), asb.Wing( name="Horizontal Stabilizer", xsecs=[ asb.WingXSec( xyz_le=[0, 0, 0], chord=ft(3, 8), airfoil=naca0012, twist=-2, control_surfaces=[ asb.ControlSurface( name="elevator", symmetric=True, hinge_point=0.75, deflection=0 ) ] ), asb.WingXSec( xyz_le=[ft(1), ft(10) / 2, 0], chord=ft(2, 4 + 3 / 8), airfoil=naca0012, twist=-2 ) ], symmetric=True ).translate([ft(13, 3), 0, ft(-2)]), asb.Wing( name="Vertical Stabilizer", xsecs=[ asb.WingXSec( xyz_le=[ft(-5), 0, 0], chord=ft(8, 8), airfoil=naca0012, ), asb.WingXSec( xyz_le=[ft(0), 0, ft(1)], chord=ft(3, 8), airfoil=naca0012, control_surfaces=[ asb.ControlSurface( name="rudder", hinge_point=0.75, deflection=0 ) ] ), asb.WingXSec( xyz_le=[ft(0, 8), 0, ft(5)], chord=ft(2, 8), airfoil=naca0012, ), ] ).translate([ft(16, 11) - ft(3, 8), 0, ft(-2)]) ], fuselages=[ asb.Fuselage( xsecs=[ asb.FuselageXSec( xyz_c=[0, 0, ft(-1)], radius=0, ), asb.FuselageXSec( xyz_c=[0, 0, ft(-1)], radius=ft(1.5), shape=3, ), asb.FuselageXSec( xyz_c=[ft(3), 0, ft(-0.85)], radius=ft(1.7), shape=7, ), asb.FuselageXSec( xyz_c=[ft(5), 0, ft(0)], radius=ft(2.7), shape=7, ), asb.FuselageXSec( xyz_c=[ft(10, 4), 0, ft(0.3)], radius=ft(2.3), shape=7, ), asb.FuselageXSec( xyz_c=[ft(21, 11), 0, ft(0.8)], radius=ft(0.3), shape=3, ), ] ).translate([ft(-5), 0, ft(-3)]) ] ) airplane.draw_three_view() # airplane.export_XFLR5_xml("test.xml", mass_props=asb.MassProperties(mass=1, Ixx=1, Iyy=1, Izz=1))