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:])
Exemple #3
0
 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
Exemple #4
0
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
Exemple #5
0
    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
Exemple #6
0
    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
Exemple #7
0
    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
Exemple #8
0
    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
Exemple #9
0
# 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)
Exemple #11
0
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)
                      },
Exemple #12
0
    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
Exemple #13
0
    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
Exemple #14
0
    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
Exemple #15
0
    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