def stack_meshes( *meshes: Tuple[np.ndarray, np.ndarray] ): """ Takes in a series of tuples (points, faces) and merges them into a single tuple (points, faces). All (points, faces) tuples are meshes given in standard format. Args: *meshes: Any number of mesh tuples in standard (points, faces) format. Returns: (points, faces) of the combined mesh. """ if len(meshes) == 1: return meshes[0] elif len(meshes) == 2: points1, faces1 = meshes[0] points2, faces2 = meshes[1] faces2 = faces2 + len(points1) points = np.concatenate((points1, points2)) faces = np.concatenate((faces1, faces2)) return points, faces else: points, faces = stack_meshes( meshes[0], meshes[1] ) return stack_meshes( (points, faces), *meshes[2:] )
def stack_meshes(*meshes: Tuple[np.ndarray, np.ndarray]): if len(meshes) == 1: return meshes[0] elif len(meshes) == 2: points1, faces1 = meshes[0] points2, faces2 = meshes[1] faces2 = faces2 + len(points1) points = np.concatenate((points1, points2)) faces = np.concatenate((faces1, faces2)) return points, faces else: points, faces = stack_meshes(meshes[0], meshes[1]) return stack_meshes((points, faces), *meshes[2:])
def randspace(start, stop, n=50): vals = (stop - start) * np.random.rand(n) + start vals = np.concatenate((vals[:-2], np.array([start, stop]))) # vals = np.sort(vals) return vals
def get_kulfan_coordinates( lower_weights=-0.2 * np.ones(5), # type: np.ndarray upper_weights=0.2 * np.ones(5), # type: np.ndarray enforce_continuous_LE_radius=True, TE_thickness=0., # type: float n_points_per_side=_default_n_points_per_side, # type: int N1=0.5, # type: float N2=1.0, # type: float ) -> np.ndarray: """ Calculates the coordinates of a Kulfan (CST) airfoil. To make a Kulfan (CST) airfoil, use the following syntax: asb.Airfoil("My Airfoil Name", coordinates = asb.kulfan_coordinates(*args)) More on Kulfan (CST) airfoils: http://brendakulfan.com/docs/CST2.pdf Notes on N1, N2 (shape factor) combinations: * 0.5, 1: Conventional airfoil * 0.5, 0.5: Elliptic airfoil * 1, 1: Biconvex airfoil * 0.75, 0.75: Sears-Haack body (radius distribution) * 0.75, 0.25: Low-drag projectile * 1, 0.001: Cone or wedge airfoil * 0.001, 0.001: Rectangle, circular duct, or circular rod. :param lower_weights: :param upper_weights: :param enforce_continuous_LE_radius: Enforces a continous leading-edge radius by throwing out the first lower weight. :param TE_thickness: :param n_points_per_side: :param N1: LE shape factor :param N2: TE shape factor :return: """ if enforce_continuous_LE_radius: lower_weights[0] = -1 * upper_weights[0] x_lower = np.cosspace(0, 1, n_points_per_side) x_upper = x_lower[::-1] x_lower = x_lower[ 1:] # Trim off the nose coordinate so there are no duplicates def shape(w, x): # Class function C = x**N1 * (1 - x)**N2 # Shape function (Bernstein polynomials) n = len(w) - 1 # Order of Bernstein polynomials K = comb(n, np.arange(n + 1)) # Bernstein polynomial coefficients S_matrix = (w * K * np.expand_dims(x, 1)**np.arange(n + 1) * np.expand_dims(1 - x, 1)**(n - np.arange(n + 1)) ) # Polynomial coefficient * weight matrix # S = np.sum(S_matrix, axis=1) S = np.array( [np.sum(S_matrix[i, :]) for i in range(S_matrix.shape[0])]) # Calculate y output y = C * S return y y_lower = shape(lower_weights, x_lower) y_upper = shape(upper_weights, x_upper) # TE thickness y_lower -= x_lower * TE_thickness / 2 y_upper += x_upper * TE_thickness / 2 x = np.concatenate([x_upper, x_lower]) y = np.concatenate([y_upper, y_lower]) coordinates = np.vstack((x, y)).T return coordinates
def generate_polars( self, alphas=np.linspace(-15, 15, 21), Res=np.geomspace(1e4, 1e7, 10), cache_filename: str = None, xfoil_kwargs: Dict[str, Any] = None, unstructured_interpolated_model_kwargs: Dict[str, Any] = None, ) -> None: """ Generates airfoil polars (CL, CD, CM functions) and assigns them in-place to this Airfoil's polar functions. In other words, when this function is run, the following functions will be added (or overwritten) to the instance: * Airfoil.CL_function(alpha, Re, mach, deflection) * Airfoil.CD_function(alpha, Re, mach, deflection) * Airfoil.CM_function(alpha, Re, mach, deflection) Where alpha is in degrees. Right now, deflection is not used. Args: alphas: The range of alphas to sample from XFoil at. Res: The range of Reynolds numbers to sample from XFoil at. cache_filename: A path-like filename (ideally a "*.json" file) that can be used to cache the XFoil results, making it much faster to regenerate the results. xfoil_kwargs: Keyword arguments to pass into the AeroSandbox XFoil module. See the aerosandbox.XFoil constructor for options. unstructured_interpolated_model_kwargs: Keyword arguments to pass into the UnstructuredInterpolatedModels that contain the polars themselves. See the aerosandbox.UnstructuredInterpolatedModel constructor for options. Warning: In-place operation! Modifies this Airfoil object by setting Airfoil.CL_function, etc. to the new polars. Returns: None (in-place) """ if self.coordinates is None: raise ValueError( "Cannot generate polars for an airfoil that you don't have the coordinates of!" ) ### Set defaults if xfoil_kwargs is None: xfoil_kwargs = {} if unstructured_interpolated_model_kwargs is None: unstructured_interpolated_model_kwargs = {} xfoil_kwargs = { # See asb.XFoil for documentation on these. "verbose": False, "max_iter": 20, "xfoil_repanel": True, **xfoil_kwargs } unstructured_interpolated_model_kwargs = { # These were tuned heuristically as defaults! "resampling_interpolator_kwargs": { "degree": 0, # "kernel": "linear", "kernel": "multiquadric", "epsilon": 3, "smoothing": 0.01, # "kernel": "cubic" }, **unstructured_interpolated_model_kwargs } ### Retrieve XFoil Polar Data from cache, if it exists. data = None if cache_filename is not None: try: with open(cache_filename, "r") as f: data = {k: np.array(v) for k, v in json.load(f).items()} except FileNotFoundError: pass ### Analyze airfoil with XFoil, if needed if data is None: from aerosandbox.aerodynamics.aero_2D import XFoil def get_run_data( Re ): # Get the data for an XFoil alpha sweep at one specific Re. run_data = XFoil(airfoil=self, Re=Re, **xfoil_kwargs).alpha(alphas) run_data["Re"] = Re * np.ones_like(run_data["alpha"]) return run_data # Data is a dict where keys are figures of merit [str] and values are 1D ndarrays. from tqdm import tqdm run_datas = [ # Get a list of dicts, where each dict is the result of an XFoil run at a particular Re. get_run_data(Re) for Re in tqdm( Res, desc= f"Running XFoil to generate polars for Airfoil '{self.name}':", ) ] data = { # Merge the dicts into one big database of all runs. k: np.concatenate(tuple([run_data[k] for run_data in run_datas])) for k in run_datas[0].keys() } if cache_filename is not None: # Cache the accumulated data for later use, if it doesn't already exist. with open(cache_filename, "w+") as f: json.dump({k: v.tolist() for k, v in data.items()}, f, indent=4) ### Save the raw data as an instance attribute for later use self.xfoil_data = data ### Make the interpolators for attached aerodynamics from aerosandbox.modeling import UnstructuredInterpolatedModel alpha_resample = np.concatenate( [ np.array([-180, -150, -120, -90, -60, -30]), alphas[::2], np.array([30, 60, 90, 120, 150, 180]) ] ) # This is the list of points that we're going to resample from the XFoil runs for our InterpolatedModel, using an RBF. Re_resample = np.concatenate( [ np.array([1e0, 1e1, 1e2, 1e3]), Res, np.array([1e8, 1e9, 1e10, 1e11, 1e12]) ] ) # This is the list of points that we're going to resample from the XFoil runs for our InterpolatedModel, using an RBF. x_data = { "alpha": data["alpha"], "ln_Re": np.log(data["Re"]), } x_data_resample = { "alpha": alpha_resample, "ln_Re": np.log(Re_resample) } CL_attached_interpolator = UnstructuredInterpolatedModel( x_data=x_data, y_data=data["CL"], x_data_resample=x_data_resample, **unstructured_interpolated_model_kwargs) log10_CD_attached_interpolator = UnstructuredInterpolatedModel( x_data=x_data, y_data=np.log10(data["CD"]), x_data_resample=x_data_resample, **unstructured_interpolated_model_kwargs) CM_attached_interpolator = UnstructuredInterpolatedModel( x_data=x_data, y_data=data["CM"], x_data_resample=x_data_resample, **unstructured_interpolated_model_kwargs) ### Determine if separated alpha_stall_positive = np.max(data["alpha"]) # Across all Re alpha_stall_negative = np.min(data["alpha"]) # Across all Re def separation_parameter(alpha, Re=0): """ Positive if separated, negative if attached. This will be an input to a tanh() sigmoid blend via asb.numpy.blend(), so a value of 1 means the flow is ~90% separated, and a value of -1 means the flow is ~90% attached. """ return 0.5 * np.softmax(alpha - alpha_stall_positive, alpha_stall_negative - alpha) ### Make the interpolators for separated aerodynamics from aerosandbox.aerodynamics.aero_2D.airfoil_polar_functions import airfoil_coefficients_post_stall CL_if_separated, CD_if_separated, CM_if_separated = airfoil_coefficients_post_stall( airfoil=self, alpha=alpha_resample) CD_if_separated = CD_if_separated + np.median(data["CD"]) # The line above effectively ensures that separated CD will never be less than attached CD. Not exactly, but generally close. A good heuristic. CL_separated_interpolator = UnstructuredInterpolatedModel( x_data=alpha_resample, y_data=CL_if_separated) log10_CD_separated_interpolator = UnstructuredInterpolatedModel( x_data=alpha_resample, y_data=np.log10(CD_if_separated)) CM_separated_interpolator = UnstructuredInterpolatedModel( x_data=alpha_resample, y_data=CM_if_separated) def CL_function(alpha, Re, mach=0, deflection=0): alpha = np.mod(alpha + 180, 360) - 180 # Keep alpha in the valid range. CL_attached = CL_attached_interpolator({ "alpha": alpha, "ln_Re": np.log(Re), }) CL_separated = CL_separated_interpolator(alpha) return np.blend(separation_parameter(alpha, Re), CL_separated, CL_attached) def CD_function(alpha, Re, mach=0, deflection=0): alpha = np.mod(alpha + 180, 360) - 180 # Keep alpha in the valid range. log10_CD_attached = log10_CD_attached_interpolator({ "alpha": alpha, "ln_Re": np.log(Re), }) log10_CD_separated = log10_CD_separated_interpolator(alpha) return 10**np.blend( separation_parameter(alpha, Re), log10_CD_separated, log10_CD_attached, ) def CM_function(alpha, Re, mach=0, deflection=0): alpha = np.mod(alpha + 180, 360) - 180 # Keep alpha in the valid range. CM_attached = CM_attached_interpolator({ "alpha": alpha, "ln_Re": np.log(Re), }) CM_separated = CM_separated_interpolator(alpha) return np.blend(separation_parameter(alpha, Re), CM_separated, CM_attached) self.CL_function = CL_function self.CD_function = CD_function self.CM_function = CM_function
def mesh_line(self, x_nondim: Union[float, List[float]] = 0.25, y_nondim: Union[float, List[float]] = 0, add_camber: bool = True, spanwise_resolution: int = 1, spanwise_spacing: str = "cosine") -> np.ndarray: """ Meshes a line that goes through each of the WingXSec objects in this wing. Args: x_nondim: The nondimensional (chord-normalized) x-coordinate that the line should go through. Can either be a single value used at all cross sections, or can be an iterable of values to be used at the respective cross sections. y_nondim: The nondimensional (chord-normalized) y-coordinate that the line should go through. Here, y-coordinate means the "vertical" component (think standard 2D airfoil axes). Can either be a single value used at all cross sections, or can be an iterable of values to be used at the respective cross sections. add_camber: Controls whether camber should be added to the line or not. spanwise_resolution: Controls the number of times each WingXSec is subdivided. spanwise_spacing: Controls the spanwise spacing. Either "cosine" or "uniform". Returns: points: a Nx3 np.ndarray that gives the coordinates of each point on the meshed line. Goes from the root to the tip. Ignores any wing symmetry (e.g., only gives one side). """ if spanwise_spacing == "cosine": space = np.cosspace elif spanwise_spacing == "uniform": space = np.linspace else: raise ValueError("Bad value of 'spanwise_spacing'") xsec_points = [] try: if len(x_nondim) != len(self.xsecs): raise ValueError( "If x_nondim is going to be an iterable, it needs to be the same length as Airplane.xsecs." ) except TypeError: pass try: if len(y_nondim) != len(self.xsecs): raise ValueError( "If y_nondim is going to be an iterable, it needs to be the same length as Airplane.xsecs." ) except TypeError: pass for i, xsec in enumerate(self.xsecs): try: xsec_x_nondim = x_nondim[i] except (TypeError, IndexError): xsec_x_nondim = x_nondim try: xsec_y_nondim = y_nondim[i] except (TypeError, IndexError): xsec_y_nondim = y_nondim if add_camber: xsec_y_nondim = xsec_y_nondim + xsec.airfoil.local_camber( x_over_c=x_nondim) xsec_point = self._compute_xyz_of_WingXSec( i, x_nondim=xsec_x_nondim, y_nondim=xsec_y_nondim, ) xsec_points.append(xsec_point) points_sections = [] for i in range(len(xsec_points) - 1): points_section = np.stack([ space(xsec_points[i][dim], xsec_points[i + 1][dim], spanwise_resolution + 1) for dim in range(3) ], axis=1) if not i == len(xsec_points) - 2: points_section = points_section[:-1, :] points_sections.append(points_section) points = np.concatenate(points_sections) return points
def mesh_thin_surface( self, method="tri", chordwise_resolution: int = 36, spanwise_resolution: int = 1, chordwise_spacing: str = "cosine", spanwise_spacing: str = "uniform", add_camber: bool = True, ) -> Tuple[np.ndarray, List[List[int]]]: """ Meshes the mean camber line of the wing as a thin-sheet body. Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in `aerosandbox.geometry.mesh_utilities`. Order of faces: * On the right wing (or, if `Wing.symmetric` is `False`, just the wing itself): * First face is the face nearest the leading edge of the wing root. * Proceeds along a chordwise strip to the trailing edge. * Then, goes to the subsequent spanwise location and does another chordwise strip, et cetera until we get to the wing tip. * On the left wing (applicable only if `Wing.symmetric` is `True`): * Same order: Starts at the root leading edge, goes in chordwise strips. Order of vertices within each face: * On the right wing (or, if `Wing.symmetric` is `False`, just the wing itself): * Front-left * Back-left * Back-right * Front-right * On the left wing (applicable only if `Wing.symmetric` is `True`): * Front-left * Back-left * Back-right * Front-right Args: method: Allows choice between "tri" and "quad" meshing. chordwise_resolution: Controls the chordwise resolution of the meshing. spanwise_resolution: Controls the spanwise resolution of the meshing. chordwise_spacing: Controls the chordwise spacing of the meshing. Can be "uniform" or "cosine". spanwise_spacing: Controls the spanwise spacing of the meshing. Can be "uniform" or "cosine". add_camber: Controls whether to mesh the thin surface with camber (i.e., mean camber line), or just the flat planform. Returns: (points, faces) in standard mesh format. """ if chordwise_spacing == "cosine": space = np.cosspace elif chordwise_spacing == "uniform": space = np.linspace else: raise ValueError("Bad value of 'chordwise_spacing'") x_nondim = space(0, 1, chordwise_resolution + 1) spanwise_strips = [] for x_n in x_nondim: spanwise_strips.append( self.mesh_line(x_nondim=x_n, y_nondim=0, add_camber=add_camber, spanwise_resolution=spanwise_resolution, spanwise_spacing=spanwise_spacing)) points = np.concatenate(spanwise_strips) faces = [] num_i = np.length(spanwise_strips[0]) # spanwise num_j = np.length(spanwise_strips) # chordwise def index_of(iloc, jloc): return iloc + jloc * num_i def add_face(*indices): entry = list(indices) if method == "quad": faces.append(entry) elif method == "tri": faces.append([entry[0], entry[1], entry[3]]) faces.append([entry[1], entry[2], entry[3]]) for i in range(num_i - 1): for j in range(num_j - 1): add_face( # On right wing: index_of(i, j), # Front-left index_of(i, j + 1), # Back-left index_of(i + 1, j + 1), # Back-right index_of(i + 1, j), # Front-right ) if self.symmetric: index_offset = np.length(points) points = np.concatenate( [points, np.multiply(points, np.array([[1, -1, 1]]))]) def index_of(iloc, jloc): return index_offset + iloc + jloc * num_i for i in range(num_i - 1): for j in range(num_j - 1): add_face( # On left wing: index_of(i + 1, j), # Front-left index_of(i + 1, j + 1), # Back-left index_of(i, j + 1), # Back-right index_of(i, j), # Front-right ) faces = np.array(faces) return points, faces
def mesh_body( self, method="quad", chordwise_resolution: int = 36, spanwise_resolution: int = 1, spanwise_spacing: str = "uniform", mesh_surface: bool = True, mesh_tips: bool = True, mesh_trailing_edge: bool = True, ) -> Tuple[np.ndarray, np.ndarray]: """ Meshes the wing as a solid (thickened) body. Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in `aerosandbox.geometry.mesh_utilities`. Args: method: Allows choice between "tri" and "quad" meshing. chordwise_resolution: Controls the chordwise resolution of the meshing. spanwise_resolution: Controls the spanwise resolution of the meshing. spanwise_spacing: Controls the spanwise spacing of the meshing. Can be "uniform" or "cosine". mesh_surface: Controls whether the actual wing surface is meshed. mesh_tips: Control whether the wing tips (both outside and inside) are meshed. mesh_trailing_edge: Controls whether the wing trailing edge is meshed, in the case of open-TE airfoils. Returns: (points, faces) in standard mesh format. """ airfoil_nondim_coordinates = np.array([ xsec.airfoil.repanel(n_points_per_side=chordwise_resolution + 1).coordinates for xsec in self.xsecs ]) x_nondim = airfoil_nondim_coordinates[:, :, 0].T y_nondim = airfoil_nondim_coordinates[:, :, 1].T spanwise_strips = [] for x_n, y_n in zip(x_nondim, y_nondim): spanwise_strips.append( self.mesh_line( x_nondim=x_n, y_nondim=y_n, add_camber=False, spanwise_resolution=spanwise_resolution, spanwise_spacing=spanwise_spacing, )) points = np.concatenate(spanwise_strips) faces = [] num_i = spanwise_resolution * (len(self.xsecs) - 1) num_j = len(spanwise_strips) - 1 def index_of(iloc, jloc): return iloc + jloc * (num_i + 1) def add_face(*indices): entry = list(indices) if method == "quad": faces.append(entry) elif method == "tri": faces.append([entry[0], entry[1], entry[3]]) faces.append([entry[1], entry[2], entry[3]]) if mesh_surface: for i in range(num_i): for j in range(num_j): add_face( index_of(i, j), index_of(i + 1, j), index_of(i + 1, j + 1), index_of(i, j + 1), ) if mesh_tips: for j in range(num_j // 2): add_face( # Mesh the root face index_of(0, num_j - j), index_of(0, j), index_of(0, j + 1), index_of(0, num_j - j - 1), ) add_face( # Mesh the tip face index_of(num_i, j), index_of(num_i, j + 1), index_of(num_i, num_j - j - 1), index_of(num_i, num_j - j), ) if mesh_trailing_edge: for i in range(num_i): add_face( index_of(i + 1, 0), index_of(i + 1, num_j), index_of(i, num_j), index_of(i, 0), ) faces = np.array(faces) if self.symmetric: flipped_points = np.array(points) flipped_points[:, 1] = flipped_points[:, 1] * -1 points, faces = mesh_utils.stack_meshes((points, faces), (flipped_points, faces)) return points, faces
# import matplotlib.pyplot as plt # import aerosandbox.tools.pretty_plots as p from tqdm import tqdm # p.mpl.use('WebAgg') # fig, ax = plt.subplots(figsize=(6.4, 4.8), dpi=150) airfoil = asb.Airfoil("rae2822") Re = 6.5e6 alpha = 1 machs = np.concatenate([ np.arange(0.1, 0.5, 0.05), np.arange(0.5, 0.6, 0.01), np.arange(0.6, 0.8, 0.003), ]) # # ##### XFoil v6 # xfoil6 = {} # for mach in tqdm(machs[machs < 1], desc="XFoil 6"): # xfoil6[mach] = asb.XFoil( # airfoil=airfoil, # Re=Re, # mach=mach, # # verbose=True, # ).alpha(alpha) # xfoil6_Cds = {k: v['CD'] for k, v in xfoil6.items() if len(v['CD']) != 0} # # plt.plot(
def get_NACA_coordinates( name: str = 'naca2412', n_points_per_side: int = _default_n_points_per_side) -> np.ndarray: """ Returns the coordinates of a specified 4-digit NACA airfoil. Args: name: Name of the NACA airfoil. n_points_per_side: Number of points per side of the airfoil (top/bottom). Returns: The coordinates of the airfoil as a Nx2 ndarray [x, y] """ name = name.lower().strip() if not "naca" in name: raise ValueError("Not a NACA airfoil!") nacanumber = name.split("naca")[1] if not nacanumber.isdigit(): raise ValueError("Couldn't parse the number of the NACA airfoil!") if not len(nacanumber) == 4: raise NotImplementedError( "Only 4-digit NACA airfoils are currently supported!") # Parse max_camber = int(nacanumber[0]) * 0.01 camber_loc = int(nacanumber[1]) * 0.1 thickness = int(nacanumber[2:]) * 0.01 # Referencing https://en.wikipedia.org/wiki/NACA_airfoil#Equation_for_a_cambered_4-digit_NACA_airfoil # from here on out # Make uncambered coordinates x_t = np.cosspace(0, 1, n_points_per_side) # Generate some cosine-spaced points y_t = 5 * thickness * ( +0.2969 * x_t**0.5 - 0.1260 * x_t - 0.3516 * x_t**2 + 0.2843 * x_t**3 - 0.1015 * x_t**4 # 0.1015 is original, #0.1036 for sharp TE ) if camber_loc == 0: camber_loc = 0.5 # prevents divide by zero errors for things like naca0012's. # Get camber y_c = np.where( x_t <= camber_loc, max_camber / camber_loc**2 * (2 * camber_loc * x_t - x_t**2), max_camber / (1 - camber_loc)**2 * ((1 - 2 * camber_loc) + 2 * camber_loc * x_t - x_t**2)) # Get camber slope dycdx = np.where(x_t <= camber_loc, 2 * max_camber / camber_loc**2 * (camber_loc - x_t), 2 * max_camber / (1 - camber_loc)**2 * (camber_loc - x_t)) theta = np.arctan(dycdx) # Combine everything x_U = x_t - y_t * np.sin(theta) x_L = x_t + y_t * np.sin(theta) y_U = y_c + y_t * np.cos(theta) y_L = y_c - y_t * np.cos(theta) # Flip upper surface so it's back to front x_U, y_U = x_U[::-1], y_U[::-1] # Trim 1 point from lower surface so there's no overlap x_L, y_L = x_L[1:], y_L[1:] x = np.concatenate((x_U, x_L)) y = np.concatenate((y_U, y_L)) return stack_coordinates(x, y)
def model(fr, p): """ Using this model because it satisfies some things that should be true in asymptotic limits: As the fineness ratio goes to infinity, the drag-divergent Mach should go to 1. As the fineness ratio goes to 0, the drag-divergent Mach should go to some reasonable value in the range of 0 to 1, probably around 0.5? Certainly no more than 0.6, I imagine. (intuition) """ return 1 - (p["a"] / (fr + p["b"]))**p["c"] fit = asb.FittedModel(model=model, x_data=np.concatenate([sub[:, 0], sup[:, 0]]), y_data=np.concatenate([sub[:, 1], sup[:, 1]]), weights=np.concatenate([ np.ones(len(sub)) / len(sub), np.ones(len(sup)) / len(sup), ]), parameter_guesses={ "a": 0.5, "b": 3, "c": 1, }, parameter_bounds={ "a": (0, None), "b": (0, None), "c": (0, None) },
def mesh_thin_surface(self, method="tri", chordwise_resolution: int = 36, spanwise_resolution: int = 1, chordwise_spacing: str = "cosine", spanwise_spacing: str = "uniform", add_camber: bool = True, ) -> Tuple[np.ndarray, List[List[int]]]: """ Meshes the mean camber line of the wing as a thin-sheet body. Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in `aerosandbox.geometry.mesh_utilities`. Args: method: Allows choice between "tri" and "quad" meshing. chordwise_resolution: Controls the chordwise resolution of the meshing. spanwise_resolution: Controls the spanwise resolution of the meshing. chordwise_spacing: Controls the chordwise spacing of the meshing. Can be "uniform" or "cosine". spanwise_spacing: Controls the spanwise spacing of the meshing. Can be "uniform" or "cosine". add_camber: Controls whether to mesh the thin surface with camber (i.e., mean camber line), or just the flat planform. Returns: (points, faces) in standard mesh format. """ if chordwise_spacing == "cosine": space = np.cosspace elif chordwise_spacing == "uniform": space = np.linspace else: raise ValueError("Bad value of 'chordwise_spacing'") x_nondim = space( 0, 1, chordwise_resolution + 1 ) spanwise_strips = [] for x_n in x_nondim: spanwise_strips.append( self.mesh_line( x_nondim=x_n, y_nondim=0, add_camber=add_camber, spanwise_resolution=spanwise_resolution, spanwise_spacing=spanwise_spacing ) ) points = np.concatenate(spanwise_strips) faces = [] num_i = spanwise_resolution * (len(self.xsecs) - 1) num_j = len(spanwise_strips) - 1 def index_of(iloc, jloc): return iloc + jloc * (num_i + 1) def add_face(*indices): entry = list(indices) if method == "quad": faces.append(entry) elif method == "tri": faces.append([entry[0], entry[1], entry[3]]) faces.append([entry[1], entry[2], entry[3]]) for i in range(num_i): for j in range(num_j): add_face( index_of(i, j), index_of(i + 1, j), index_of(i + 1, j + 1), index_of(i, j + 1), ) faces = np.array(faces) if self.symmetric: flipped_points = np.array(points) flipped_points[:, 1] = flipped_points[:, 1] * -1 points, faces = mesh_utils.stack_meshes( (points, faces), (flipped_points, faces) ) return points, faces
def mesh_line(self, x_nondim: Union[float, List[float]] = 0.25, y_nondim: Union[float, List[float]] = 0, add_camber: bool = True, spanwise_resolution: int = 1, spanwise_spacing: str = "cosine" ) -> np.ndarray: if spanwise_spacing == "cosine": space = np.cosspace elif spanwise_spacing == "uniform": space = np.linspace else: raise ValueError("Bad value of 'spanwise_spacing'") xsec_points = [] try: if len(x_nondim) != len(self.xsecs): raise ValueError( "If x_nondim is going to be an iterable, it needs to be the same length as Airplane.xsecs." ) except TypeError: pass try: if len(y_nondim) != len(self.xsecs): raise ValueError( "If y_nondim is going to be an iterable, it needs to be the same length as Airplane.xsecs." ) except TypeError: pass for i, xsec in enumerate(self.xsecs): origin = self._compute_xyz_le_of_WingXSec(i) xg_local, yg_local, zg_local = self._compute_frame_of_WingXSec(i) try: xsec_x_nondim = x_nondim[i] except (TypeError, IndexError): xsec_x_nondim = x_nondim try: xsec_y_nondim = y_nondim[i] except (TypeError, IndexError): xsec_y_nondim = y_nondim if add_camber: xsec_y_nondim = xsec_y_nondim + xsec.airfoil.local_camber(x_over_c=x_nondim) xsec_point = self._compute_xyz_of_WingXSec( i, x_nondim=xsec_x_nondim, y_nondim=xsec_y_nondim, ) xsec_points.append(xsec_point) mesh_sections = [] for i in range(len(xsec_points) - 1): mesh_section = space( xsec_points[i], xsec_points[i + 1], spanwise_resolution + 1 ) if not i == len(xsec_points) - 2: mesh_section = mesh_section[:-1] mesh_sections.append(mesh_section) mesh = np.concatenate(mesh_sections) return mesh
def mesh_thin_surface(self, method="tri", chordwise_resolution: int = 1, spanwise_resolution: int = 1, chordwise_spacing: str = "cosine", spanwise_spacing: str = "uniform", add_camber: bool = True, ) -> Tuple[np.ndarray, List[List[int]]]: if chordwise_spacing == "cosine": space = np.cosspace elif chordwise_spacing == "uniform": space = np.linspace else: raise ValueError("Bad value of 'chordwise_spacing'") x_nondim = space( 0, 1, chordwise_resolution + 1 ) spanwise_strips = [] for x_n in x_nondim: spanwise_strips.append( self.mesh_line( x_nondim=x_n, y_nondim=0, add_camber=add_camber, spanwise_resolution=spanwise_resolution, spanwise_spacing=spanwise_spacing ) ) points = np.concatenate(spanwise_strips) faces = [] num_i = spanwise_resolution * (len(self.xsecs) - 1) num_j = len(spanwise_strips) - 1 def index_of(iloc, jloc): return iloc + jloc * (num_i + 1) def add_face(*indices): entry = list(indices) if method == "quad": faces.append(entry) elif method == "tri": faces.append([entry[0], entry[1], entry[3]]) faces.append([entry[1], entry[2], entry[3]]) for i in range(num_i): for j in range(num_j): add_face( index_of(i, j), index_of(i + 1, j), index_of(i + 1, j + 1), index_of(i, j + 1), ) faces = np.array(faces) if self.symmetric: flipped_points = np.array(points) flipped_points[:, 1] = flipped_points[:, 1] * -1 points, faces = mesh_utils.stack_meshes( (points, faces), (flipped_points, faces) ) return points, faces
def mesh_body(self, method="tri", chordwise_resolution: int = 32, spanwise_resolution: int = 16, spanwise_spacing: str = "uniform", mesh_tips: bool = True, mesh_trailing_edge: bool = True, ) -> Tuple[np.ndarray, np.ndarray]: airfoil_nondim_coordinates = np.array([ xsec.airfoil .repanel(n_points_per_side=chordwise_resolution + 1) .coordinates for xsec in self.xsecs ]) x_nondim = airfoil_nondim_coordinates[:, :, 0].T y_nondim = airfoil_nondim_coordinates[:, :, 1].T spanwise_strips = [] for x_n, y_n in zip(x_nondim, y_nondim): spanwise_strips.append( self.mesh_line( x_nondim=x_n, y_nondim=y_n, add_camber=False, spanwise_resolution=spanwise_resolution, spanwise_spacing=spanwise_spacing, ) ) points = np.concatenate(spanwise_strips) faces = [] num_i = spanwise_resolution * (len(self.xsecs) - 1) num_j = len(spanwise_strips) - 1 def index_of(iloc, jloc): return iloc + jloc * (num_i + 1) def add_face(*indices): entry = list(indices) if method == "quad": faces.append(entry) elif method == "tri": faces.append([entry[0], entry[1], entry[3]]) faces.append([entry[1], entry[2], entry[3]]) for i in range(num_i): for j in range(num_j): add_face( index_of(i, j), index_of(i + 1, j), index_of(i + 1, j + 1), index_of(i, j + 1), ) if mesh_tips: for j in range(num_j // 2): add_face( # Mesh the root face index_of(0, num_j - j), index_of(0, j), index_of(0, j + 1), index_of(0, num_j - j - 1), ) add_face( # Mesh the tip face index_of(num_i, j), index_of(num_i, j + 1), index_of(num_i, num_j - j - 1), index_of(num_i, num_j - j), ) if mesh_trailing_edge: for i in range(num_i): add_face( index_of(i + 1, 0), index_of(i + 1, num_j), index_of(i, num_j), index_of(i, 0), ) faces = np.array(faces) if self.symmetric: flipped_points = np.array(points) flipped_points[:, 1] = flipped_points[:, 1] * -1 points, faces = mesh_utils.stack_meshes( (points, faces), (flipped_points, faces) ) return points, faces